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中添加一些行为代码,即可实现区域导航,相对来说还是比较方便的,后续再研究如何用更简洁的代码实现。

感谢观看,互相学习,一起进步!

 

posted @ 2023-03-14 19:57  JustWantToStudy  阅读(2299)  评论(8编辑  收藏  举报