WPF学习笔记05-怎么用ModernWPF实现带导航的界面

<0x00> 介绍下ModernWPF

这个其实是个Nuget包,里面有很多现代化设计的WPF组件
不是之前说嘛,在WPF里面做现代化的UI设计很复杂
现在差不多就是别人已经把最复杂的实现写完了,我们只要调用就好了

这个包的实现风格是UWP风格(也就是WinUI2)
里面的很多用法是参考UWP
所以有些组件的用法跟WPF的常见实现不一样(就比如这里讲的导航界面)

安装的话用VSNuget包管理器就可以
认准Nuget包
(Nuget上有很多包叫ModernWPF,我用的是这个,别的实现我就不清楚了,最好一样吧)
Github项目主页
在Github上也有这个项目的Wiki,所有组件的使用都有列举一点
因为差不多是完全复刻UWP的,所以有些样例会直接跳转到微软的文档
照样看就是了,代码实现是差不多的(跳转的文档是英文的,最好就直接看英文,这样最准确)

<0x01> 如果是一般的WPF项目,我们该怎么实现导航

还是先稍微讲讲一般的WPF项目里怎么实现导航
假设我们有MainWindow.xamlPageViewModel.csSomePage.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,来控制UserControlContent
因为这个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出来的
有啥好处呢,主要还是方便管理吧,功能更加聚合了

Licensed under CC BY-NC-SA 4.0