<0x00> 介绍下ModernWPF
这个其实是个Nuget包,里面有很多现代化设计的WPF组件
不是之前说嘛,在WPF里面做现代化的UI设计很复杂
现在差不多就是别人已经把最复杂的实现写完了,我们只要调用就好了
这个包的实现风格是UWP风格(也就是WinUI2)
里面的很多用法是参考UWP的
所以有些组件的用法跟WPF的常见实现不一样(就比如这里讲的导航界面)
安装的话用VS的Nuget包管理器就可以
(Nuget上有很多包叫ModernWPF,我用的是这个,别的实现我就不清楚了,最好一样吧)
Github项目主页
在Github上也有这个项目的Wiki,所有组件的使用都有列举一点
因为差不多是完全复刻UWP的,所以有些样例会直接跳转到微软的文档
照样看就是了,代码实现是差不多的(跳转的文档是英文的,最好就直接看英文,这样最准确)
<0x01> 如果是一般的WPF项目,我们该怎么实现导航
还是先稍微讲讲一般的WPF项目里怎么实现导航
假设我们有MainWindow.xaml,PageViewModel.cs,SomePage.xaml
<!--MainWindow.xaml-->
<window ...>
<window.DataContent>
<local:PageViewModel/>
</Window.DataContent>
<Grid>
<Grid.ColumnDefinitions>
<!--第一列放导航的按钮-->
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<StackPanel>
<!--若干按钮,具体就不设计了-->
<Button Command="{Binding NavigateToPage}"/>
</StackPanel>
<!--内容展示的页面-->
<UserControl Grid.Column="1" Content="{Binding CurrentPage}"/>
</Grid>
</window>
//PageViewModel.cs
//常见的引用
namespace TestProject.ViewModel
{
internal partial class PageViewModel:ObservableObject
{
[ObservableProperty]
public object? currentPage;
public ICommand NavigateToPage{get;set;}
public PageViewModel()
{
CurrentPage=new SomePage();
NavigateToPage=new RelayCommand(()=>CurrentPage=new SomePage());
}
}
}
(这里的一些没见过的东西是在Community.MVVM包里面的,上一篇结尾有介绍)
<!--SomePage.xaml-->
<UserControl ...>
<TextBlock
Text="Emmm"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="32"/>
</UserControl>
这里通过UseControl控件来存放我们自己写的SomePage
导航栏的每个按钮会绑定一个Command,来控制UserControl的Content
因为这个Content就存放具体的页面嘛,所以只要改这个Content就相当于切换页面了
之后有什么多的页面就是先新建一个UserControl控件并完成设计
然后再在PageViewModel.cs里面实现切换代码(就是写一行改CurrentPage的代码)
最后在MainWindow.xaml里面为对应按钮绑定Command就好
其实最好是在PageViewModel里用一个object数组存储不同的page,我这里就偷懒了
还有就是导航栏我也没咋设计,基本上就是用Triger配合Setter做就可以了,这里也摸了
<0x02> 用ModernWPF里面的NavigationView实现导航
(虽然是介绍怎么在ModernWPF里面实现导航,因为用法极像UWP,也算介绍UWP的开发了)
在ModernWPF中用NavigationView做导航就没有类似按钮绑定Command的用法了
准确来说,用ModernWPF甚至都不需要写一个对应的VM
那么怎么切换页面呢
差不多就是靠.xaml附带的.cs里面实现了
先上代码
(ModernWPF有使用前的一些步骤,在它的readme有写,就两步,我就不介绍了)
<!--ModernUITest.xaml-->
<Window x:Class="TestProject.ModernUITest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:local="clr-namespace:TestProject"
mc:Ignorable="d"
ui:WindowHelper.UseModernWindowStyle="True"
Title="ModernUITest" Height="800" Width="800">
<ui:NavigationView
IsBackButtonVisible="Collapsed"
IsTitleBarAutoPaddingEnabled="False"
PaneTitle="Test"
PaneDisplayMode="Auto"
ItemInvoked="NavigationView_ItemInvoked">
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem
Icon="Home"
Tag="TestProject.Page.PageTest1"
Content="Sample Item 1"
IsSelected="True"/>
<ui:NavigationViewItem
Icon="Keyboard"
Content="Sample Item 2"
Tag="TestProject.Page.PageTest2"/>
</ui:NavigationView.MenuItems>
<UserControl Name="UC"/>
</ui:NavigationView>
</Window>
//ModernUITest.xaml.cs
//常见的引用
namespace TestProject
{
public partial class ModernUITest : Window
{
public ModernUITest()
{
InitializeComponent();
UC.Content = new PageTest1();//给一个默认的页面
}
private void NavigationView_ItemInvoked(ModernWpf.Controls.NavigationView sender, ModernWpf.Controls.NavigationViewItemInvokedEventArgs args)
{
if(args.IsSettingsInvoked==true)
{
NavigationView_Navigate(typeof(int), args.RecommendedNavigationTransitionInfo);
//我这里没写Setting的page,所以就随便写了typeof(int)
}
else if(args.InvokedItemContainer!=null)
{
NavigationView_Navigate(Type.GetType(args.InvokedItemContainer.Tag.ToString()), args.RecommendedNavigationTransitionInfo);
}
}
private void NavigationView_Navigate(Type navPageType, NavigationTransitionInfo transitionInfo)
{
Type preNavPageType = UC.Content.GetType();
if(navPageType is not null && !Type.Equals(navPageType, preNavPageType))
{
if(navPageType==typeof(PageTest1))
{
UC.Content = new PageTest1();
}
if (navPageType == typeof(PageTest2))
{
UC.Content = new PageTest2();
}
}
}
}
}
(PageTest1和PageTest2的代码就不贴了,就纯纯的一行字)
先讲下包里的东西
ui:WindowHelper.UseModernWindowStyle=true表示使用ModernWPF的窗口样式window控件下面只有ui:NavigationView
先在ui:NavigationView里面设置ui:NavigationView.MenuItems
再在里面设置ui:NavigationViewItem,要几个就设置几个ui:NavigationView下面有个UserControl,这个就是我们要切换的页面
ui:NavigationView里面有些属性IsBackButtonVisible="Collapsed"是关闭默认的返回按钮IsTitleBarAutoPaddingEnabled="False"是关闭顶栏的自动排布PaneTitle="Test"就是会显示在最上面的那行字,展开时会显示PaneDisplayMode="Auto"这里显示了自动展开,就是最大化窗口时会自动展开ItemInvoked="NavigationView_ItemInvoked"这个是实现导航最关键的部分,后面讲
ui:NavigationViewItem里别的应该都能看懂
就是Tag这个是最关键的,规范的话里面要填上要导航页面对应的类型全名
就是对应页面的namespace.类名,因为按规范导航会用到Type来判断
(估计是因为微软文档写的是用Frame实现页面导航)
.xaml里要干的事
在ui:NavigationView里写上ItemInvoked="NavigationView_ItemInvoked",名字随意
在ui:NavigationViewItem里正确写上Tag
给UserControl起个名,好让我们在.cs拿到对象
剩下的事就交给.cs了
.cs里要干的事
按上面的样式写就可以了
大致的调用过程
在NavigationView里面切换页面会触发ItemInvoked绑定的事件
在ItemInvoked绑定的方法中,先判定是不是要切换到Setting的页面(args.IsSettingsInvoked==true)
(因为这里的NavigationView没有设置IsSettingsVisible="False")
然后判定args.InvokedItemContainer!=null
这两条路径都会调用导航方法NavigationView_Navigate
这个导航方法会要求传入一个Type和一个NavigationTransitionInfo
(NavigationTransitionInfo在这里确实没啥用)
这个Type用来判断具体要切换到哪个页面
其实Tag随便写也没啥问题,主要看写的导航方法是什么
这里我写的代码极致精简的话完全可以初始化一个Dictionary<string,object>
然后直接在ItemInvoked绑定的方法中切换就可以了
像这样
namespace TestProject
{
public partial class ModernUITest : Window
{
Dictionary<string, object> pages;
public ModernUITest()
{
InitializeComponent();
pages = new Dictionary<string, object>
{
{"Page1",new PageTest1()},
{"Page2",new PageTest2()},
{"Setting",new SettingPage()},
};
UC.Content = pages["Page1"];
}
private void NavigationView_ItemInvoked(ModernWpf.Controls.NavigationView sender, ModernWpf.Controls.NavigationViewItemInvokedEventArgs args)
{
if (args.IsSettingsInvoked == true)
{
UC.Content = pages["Setting"];
}
else if (args.InvokedItemContainer != null)
{
UC.Content = pages[args.InvokedItemContainer.Tag.ToString()];
}
}
}
}
对应的Tag再改改就好
<0x03> 两种做法的区别
我个人来看的话,按第一种实现会更加优雅
毕竟前端只要有按钮绑定Command就好了
但这么做的话就是控件要自己开发了(也确实不是大问题,主要是WPF现在只有维护了)
第二种方式相对来说没那么优雅,但真正做到了前端的事前端干
按照MVVM的理论,VM是用来连接前后端的
但第一种方法我们仅仅是为了前端切换页面就写了个PageViewModel实现
某种程度上确实不大符合MVVM的定义
(我知道有别的方式可以不单独写VM,但别的方式确实没单独写VM直观)
而第二种方法把前端的事情聚合在一起,那些新的页面都是在前端new出来的
有啥好处呢,主要还是方便管理吧,功能更加聚合了