wpf mvvm 用行为以及依赖注入的方式实现导航功能
最近在学习MVVM模式,使用的框架是微软的Community toolkit mvvm,但是这个框架好像没有导航的功能,又不想用别的框架,只能自己试着搞一搞,就当作学习了。
简单的学习、研究了下Prism是怎么实现导航之后
Prism 源码解读1-Bootstrapper和Region的创建 - 阿杜聊编程 - 博客园 (cnblogs.com)
大概整理出一个思路,跟Prism有些区别。思路如下:
整体主要借助行为和依赖注入实现;实现过程分为3个部分:
1、往DI容器注册导航目的地Region(主要是ContentControl控件);
2、往DI容器注册被导航目标View(主要是UserControl);
3、实现导航;
对了,要下载3个Nuget包
一、往DI容器注册导航目的地Region
(严格来说,我是先往DI容器里面注册了一个RegionManager区域管理类,这个类里面有一个字典<string,ContentControl>,用于存放作为区域的ContentControl控件,对应的key就是下文中的RegionName)
(但是这个RegionManager的注册动作是在第二步里面完成的,所以这第一步提到的注册,实际上是先通过DI容器拿到RegionManager之后,往它的字典里面Add成员)
注册这个动作我是借助行为实现的,1、新建一个行为类 :public class RegionRegisterBehavior : Behavior<ContentControl>
2、添加一个依赖属性RegionName用来设置区域的名字,
3、重写OnAttached(),为附加方控件增加Loaded事件 :AssociatedObject.Loaded += AssociatedObject_Loaded
4、在AssociatedObject_Loaded中,通过DI容器拿到RegionManager之后,往它的字典里面Add成员,key就是依赖属性RegionName,Value则是Sender as ContentControl
完整代码如下:
using Microsoft.Xaml.Behaviors; using System.Windows.Controls; using System.Windows; using CommunityToolkit.Mvvm.DependencyInjection; using MyBehavior.BaseClass; using System; namespace MyBehavior.AllBehaviors { /// <summary> /// 区域导航功能-注册区域行为:将附加该行为的ContenControl控件添加到RegionManager容器中 /// </summary> public class RegionRegisterBehavior : Behavior<ContentControl> { /// <summary> /// 设置区域名字 /// </summary> public string RegionName { get { return (string)GetValue(RegionNameProperty); } set { SetValue(RegionNameProperty, value); } } public static readonly DependencyProperty RegionNameProperty = DependencyProperty.Register("RegionName", typeof(string), typeof(RegionRegisterBehavior)); protected override void OnAttached() { base.OnAttached(); AssociatedObject.Loaded += AssociatedObject_Loaded; } private void AssociatedObject_Loaded(object sender, RoutedEventArgs e) { if (AssociatedObject.GetType().Name != "ContentControl") { return; } var a = (sender as ContentControl); if (string.IsNullOrEmpty(RegionName)) { throw new Exception($"区域注册时未设置RegionName"); } Ioc.Default.GetService<RegionManager>()?.RegionMaps.TryAdd(RegionName, a); } } }
二、往DI容器注册被导航目标View(主要是UserControl)
这一步也是用行为实现,1、新建行为类 NavigateInitializeBehavior : Behavior<Window>
2、增加依赖属性 NameOfRegisterView ===> View的类名(字符串)
3、重写OnAttached(),直接往DI容器里面添加View类单例,这里需要被添加View的Type。主要是通过反射的方法,从程序集里面拿到类名(字符串)对应的Type。
这里的NameOfRegisterView属性,可以同时写多个View类名,用逗号隔开,方便分割;
using CommunityToolkit.Mvvm.DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Xaml.Behaviors; using MyBehavior.BaseClass; using System; using System.Windows; namespace MyBehavior.AllBehaviors { /// <summary> /// 区域导航功能-初始化行为:往DI容器注册一个或多个视图View,以及注册用于存放Region的RegionManager容器 /// </summary> public class NavigateInitializeBehavior : Behavior<Window> { /// <summary> /// 需导航的视图View的类名,用于注入DI容器 /// 可以同时注册多个View,只需在各自类名中间加上逗号 /// </summary> public string NameOfRegisterView { get { return (string)GetValue(NameOfRegisterViewProperty); } set { SetValue(NameOfRegisterViewProperty, value); } } public static readonly DependencyProperty NameOfRegisterViewProperty = DependencyProperty.Register("NameOfRegisterView", typeof(string), typeof(NavigateInitializeBehavior)); protected override void OnAttached() { base.OnAttached(); ServiceCollection services = new ServiceCollection(); services.AddSingleton<RegionManager>();//注册用于存放Region的RegionManager容器 if (!string.IsNullOrEmpty(NameOfRegisterView)) { var splitArray = NameOfRegisterView.Split(','); foreach (string item in splitArray) { Type type = CommonTool.GetTypeBaseonName(item); if (type != null) { services.AddSingleton(type); } } } Ioc.Default.ConfigureServices(services.BuildServiceProvider()); } } }
三、实现导航
这一步也是用行为实现。新建行为类,添加依赖属性,重写OnAttached,在OnAttached中为控件的Click事件附加函数,在函数中实现导航。
using Microsoft.Xaml.Behaviors; using MyBehavior.BaseClass; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; namespace MyBehavior.AllBehaviors { public class ClickToNavigateBehavior : Behavior<Button> { public string TargetRegionName { get { return (string)GetValue(TargetRegionNameProperty); } set { SetValue(TargetRegionNameProperty, value); } } public static readonly DependencyProperty TargetRegionNameProperty = DependencyProperty.Register("TargetRegionName", typeof(string), typeof(ClickToNavigateBehavior)); public string TargetViewName { get { return (string)GetValue(TargetViewNameProperty); } set { SetValue(TargetViewNameProperty, value); } } public static readonly DependencyProperty TargetViewNameProperty = DependencyProperty.Register("TargetViewName", typeof(string), typeof(ClickToNavigateBehavior)); protected override void OnAttached() { base.OnAttached(); AssociatedObject.Click += AssociatedObject_Click; } private void AssociatedObject_Click(object sender, RoutedEventArgs e) { CommonTool.Navigate(TargetRegionName, TargetViewName); } } }
四、工具类:通过类名获取Type、导航步骤实现
using CommunityToolkit.Mvvm.DependencyInjection; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; using System.Threading.Tasks; namespace MyBehavior.BaseClass { internal static class CommonTool { internal static Type GetTypeBaseonName(string name) { var types = Assembly.GetEntryAssembly()?.GetTypes(); Type? type = null; foreach (var temp in types) { if (temp.Name.Equals(name)) { type = temp; } } return type; } /// <summary> /// /// </summary> /// <param name="regionName"></param> /// <param name="targetUserControlName"></param> /// <returns></returns> internal static void Navigate(string regionName, string targetViewName) { if (string.IsNullOrEmpty(regionName)) { throw new Exception($"导航动作未设置目标Region"); } if (string.IsNullOrEmpty(targetViewName)) { throw new Exception($"导航动作未设置目标View"); } RegionManager regionCollection = Ioc.Default.GetService<RegionManager>() ?? throw new Exception($"DI容器中找不到区域容器RegionManager"); if (regionCollection.RegionMaps.ContainsKey(regionName)) { Type type = GetTypeBaseonName(targetViewName) ?? throw new Exception($"在程序集{Assembly.GetEntryAssembly()}中找不到名为{targetViewName}的类型"); var targetView = Ioc.Default.GetService(type)?? throw new Exception($"DI容器中找不到类型{type.FullName}"); regionCollection.RegionMaps[regionName].Content = targetView; return; } else { throw new Exception($"区域容器中找不到名为{regionName}的区域"); } } } }
还有RegionManager类
using System.Collections.Generic; using System.Windows.Controls; namespace MyBehavior.BaseClass { public class RegionManager : IRegionService { public Dictionary<string, ContentControl> RegionMaps { get; set; } = new(); } }
五、一个简单的例子
1、项目结构
2、MainWindow.xaml
加粗部分就是附加行为,从上往下依次是:为window附加 NavigateInitializeBehavior ,注册RegionManager以及UserControl1;
为Button附加ClickToNavigateBehavior,指定导航的区域和视图之外,为该button的click事件附加导航功能;
为区域ContentControl附加RegionRegisterBehavior,往RegionManager的字典中添加名为Region的ContentControl;
<Window x:Class="test.MainWindow" 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:be="clr-namespace:MyBehavior.AllBehaviors;assembly=MyBehavior" xmlns:i="http://schemas.microsoft.com/xaml/behaviors" xmlns:local="clr-namespace:test" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <i:Interaction.Behaviors> <be:NavigateInitializeBehavior NameOfRegisterView="UserControl1"/> </i:Interaction.Behaviors> <Grid> <StackPanel> <Button Width="100" Height="30"> <i:Interaction.Behaviors> <be:ClickToNavigateBehavior TargetRegionName="Region" TargetViewName="UserControl1"/> </i:Interaction.Behaviors> </Button> <Border Width="500" Height="500" Background="Beige"> <ContentControl> <i:Interaction.Behaviors> <be:RegionRegisterBehavior RegionName="Region"/> </i:Interaction.Behaviors> </ContentControl> </Border> </StackPanel> </Grid> </Window>
3、随便新建一个UserControl1。
4、直接启动
按钮点击前:
按钮点击后:
完成!
可以看出,只需要在xaml中添加一些行为代码,即可实现区域导航,相对来说还是比较方便的,后续再研究如何用更简洁的代码实现。
感谢观看,互相学习,一起进步!