Prism-AutoWireViewModel

发布时间 2023-08-23 23:09:30作者: euv

静态类ViewModelLocationProvider维护一张View.GetType().ToString() --> ViewModel实例的映射表,以及一张View.GetType().ToString() --> ViewModel Type的映射表,以及根据View的FullName推算出ViewModel Type 的FullName的策略,还有一个根据ViewModel的Type构造出1个ViewModel实例的策略,最终得到适配View的ViewModel实例赋值给其DataContext.

快速实践

在Application的OnStartup(StartupEventArgs startupEventArgs)中

 ViewModelLocationProvider.SetDefaultViewModelFactory((vmType) =>
 {
     // 返回一个ViewModel实例。一般会从DI容器取出ViewModel实例。实现这个则下面的不用实现了。
 });

 ViewModelLocationProvider.SetDefaultViewModelFactory((view, vmType) =>
 {
     // 返回一个ViewModel实例。一般会从DI容器取出ViewModel实例。实现这个则上面的不用实现了。
 });

 ViewModelLocationProvider.Register<View>(()=>new ViewModel()); // 注册工厂委托到字典
 ViewModelLocationProvider.Register<View,ViewModelType>();      // 注册类型到字典,不"劳烦"ViewTypeToViewModelTypeResolver解析。

 ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
 {
     // 返回1个ViewModel Type。
	 // 可以覆盖Prism 默认的确定ViewModel Type的策略。
	 // 一般不会选择重新定制此策略,而是使用Prism默认的策略
 });

在每个View中的XAML中

<Window x:Class="Demo.Views.MainWindow"
    ...
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">

或在每个View的CS文件合适的位置(一般是构造函数或Load函数)中

ViewModelLocator.SetAutoWireViewModel(view,true);

View.DataContext = GetViewModelInstance()的时机

View的附加属性ViewModelLocator.AutoWireViewModel设置成True时,会触发ViewModelLocationProvider去为View寻找合适的ViewModel实例。

若View要利用Prism自动定位ViewModel功能,需添加附加属性ViewModelLocator.AutoWireViewModel并设置成True。

<Window x:Class="Demo.Views.MainWindow"
    ...
    xmlns:prism="http://prismlibrary.com/"
    prism:ViewModelLocator.AutoWireViewModel="True">

附加属性的定义细节。被定义在ViewModelLocator类,重点是其元数据中的回调函数PropertyChangedCallback,它会在AutoWireViewModelProperty=True后被触发完成ViewModel的构建和DataContext的赋值。

public static class ViewModelLocator
{
    public static DependencyProperty AutoWireViewModelProperty = DependencyProperty.RegisterAttached("AutoWireViewModel", typeof(bool?), typeof(ViewModelLocator), new PropertyMetadata(defaultValue: null, propertyChangedCallback: AutoWireViewModelChanged));
}

分析ViewModelLocator的方法AutoWireViewModelChanged

private static void AutoWireViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
_WINUI
    if (!DesignerProperties.GetIsInDesignMode(d))

    {
        var value = (bool?)e.NewValue;
        if (value.HasValue && value.Value) // View的依赖属性AutoWireViewModelProperty被赋值成True时才会真的去寻找合适ViewModel实例作为其DataContext。View不添加附加属性或添加附加属性但初值是False,Prism都不会为View自动构建ViewModel.
        {
            ViewModelLocationProvider.AutoWireViewModelChanged(d, Bind); // d是View实例,根据View.GetType().ToString()和View的FullName构建出适配的ViewModel,然后Bind方法完成View.DataContext=ViewModel.
        }
    }
}

static void Bind(object view, object viewModel)
{
    if (view is FrameworkElement element) // 只要是FrameworkElement,就可以做View。(WPF元素继承链从FrameworkElemt开始才具有DataContext属性)。
        element.DataContext = viewModel;
}

View的附加属性ViewModelLocator.AutoWireViewModel设置成True时,才会触发构建ViewModel实例并完成View.DataContetx=ViewModel。按照习惯,通常在XAML中添加prism:ViewModelLocator.AutoWireViewModel="True",所以一般是在View的构造函数中完成DataContext的赋值的。我们完全可以挪到View的Load函数中:ViewModelLocator.SetAutoWireViewModel(view,true);

自动为View构建合适的ViewModel的策略

public static void AutoWireViewModelChanged(object view, Action<object, object> setDataContextCallback)
{
    // Try mappings first
    object viewModel = GetViewModelForView(view); // 检查委托工厂

    // try to use ViewModel type
    if (viewModel == null)
    {
        //check type mappings
        var viewModelType = GetViewModelTypeForView(view.GetType()); // 检查ViewModel Type 缓存

        // fallback to convention based
        if (viewModelType == null)
            viewModelType = _defaultViewTypeToViewModelTypeResolver(view.GetType()); // 利用定位功能

        if (viewModelType == null)
            return;
        // 实例化ViewModel
        viewModel = _defaultViewModelFactoryWithViewParameter != null ? _defaultViewModelFactoryWithViewParameter(view, viewModelType) : _defaultViewModelFactory(viewModelType);
    }

    // 完成DataContext = ViewModel
    setDataContextCallback(view, viewModel);
}

以View.GetType().ToString()为Key,在字典(Dictionary<string, Func<object>>())中寻找返回ViewModel实例的Func,拿到ViewModel实例。

上一步成功,直接View.DataContext = ViewModel. 上一步失败,下一步先确定View所需要的ViewModel的类型,拿到类型再实例化。

以View.GetType().ToString()为Key,去字典(Dictionary<string, Type>())中找到ViewModel的Type.

上一步成功,直接跳到根据Type实例化ViewModel的步骤,否则,利用Prism确认ViewModel类型规则继续确认ViewModel的Type.

如果默认规则成功确定Type,则实例化完成Wire,否则,因无法得到ViewModel实例导致一张DataContext为NULL的View诞生。(并不会抛出异常提醒,这是合理的,可能有的View没遵守MVVM开发,后台逻辑放到View的XAML和CS文件中了)。

Prism自动确认ViewModel Type的规则

static Func<Type, Type> _defaultViewTypeToViewModelTypeResolver =
    viewType =>
    {
        var viewName = viewType.FullName;
        viewName = viewName.Replace(".Views.", ".ViewModels.");
        var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
        var suffix = viewName.EndsWith("View") ? "Model" : "ViewModel";
        var viewModelName = String.Format(CultureInfo.InvariantCulture, "{0}{1}, {2}", viewName, suffix, viewAssemblyName);
        return Type.GetType(viewModelName);
    };

先确定ViewModel的FullName字符串,即Namespace和Name.

ViewModel的名称

  1. View如果以View结尾,追加Model,否则追加ViewModel。

例:

EmployeeList,则ViewModel是EmployeeListViewModel

EmployeeListView,则ViewModel是EmployeeListViewModel

ViewModel命名空间

将View命名空间中的Views替换成ViewModels即可。如果View的命名空间没有Views字眼,那么ViewModel的命名空间和View的一模一样。

例:

A.B.Views.EmployeeList A.B.ViewModels.EmployeeListViewModel

A.B.EmployeeListView A.B.EmployeeListViewModel

  1. 在View所在程序集中寻找FullName是上一步得到的FullName字符串的Type。存在这样的Type,则实例化作为DataContext,否则View的DataContext为NULL。

小结

View的名称可以带View也可以不带View,但ViewModel一定要以ViewModel结尾。

Prism默认的确认ViewModel策略与文件夹无关,只与命名空间有关,只是命名空间受文件夹影响而已。

实例化策略

viewModel = _defaultViewModelFactoryWithViewParameter != null ? _defaultViewModelFactoryWithViewParameter(view, viewModelType) : _defaultViewModelFactory(viewModelType);
static Func<Type, object> _defaultViewModelFactory = type => Activator.CreateInstance(type);
static Func<object, Type, object> _defaultViewModelFactoryWithViewParameter;

得到ViewModel的Type,下一步就是构造ViewModel实例。上面代码是默认实现。

defaultViewModelFactoryWithViewParameter始终是NULL,所以一直采用defaultViewModelFactory构造。但defaultViewModelFactory只是利用反射调用ViewModel的无参构造函数实例化,缺点就是如果ViewModel的不含无参构造函数,或者想利用有参构造函数实例化,又或者ViewModel的构造函数参数有接口类型想借助依赖注入容器实例化,这些_defaultViewModelFactory都做不到。

所以定制实例化策略是很经常很有必要的基操。

ViewModelLocationProvider.SetDefaultViewModelFactory(viewModelType) =>
    {
        return viewModel instance;
    });


ViewModelLocationProvider.SetDefaultViewModelFactory((view, viewModelType) =>
    {
        switch (view)
        {
            case Window window:
                //your logic
                break;
            case UserControl userControl:
                //your logic
                break;
        }
           return viewModel instance;
    }

上述代码完成对委托类型的静态字段defaultViewModelFactoryWithViewParameter和defaultViewModelFactory的赋值。实现其中1个就可以了,如果2个都实现,Prism只会优先使用defaultViewModelFactoryWithViewParameter。2者之间的差别就是前者的参数多个View,如果需要的话,在内部可利用View做一些额外的判断,仅此而已。

依赖注入无关性

回顾一下Prism自动定位ViewModel的全貌,只与两个静态类ViewModelLocator和ViewModelLocationProvider有关,前者提供附加属性用于触发定位机制和负责将DataContext=ViewModel实例,后者负责存储View和ViewModel的映射关系以及实例化ViewModel,全程与依赖注入无关。

所以,即使当前项目已经基于其他MVVM开发,也能单独使用Prism的自动定位功能。

Prism的定位功能可以分成3个部分,查找工厂委托字典、定位ViewModel Type、实例化ViewModel。我们甚至可以将所有View的ViewModel构造方法存储到字典中,根本不会使用Prism定位功能的后半部分:智能确认ViewModel Type和实例化策略。

要不要在项目中使用自动定位呢? 建议用,可以强制团队成员规范项目文件夹结构。

开发规范

  1. 尽量所有的View和其ViewModel都遵守命名规约

    View和ViewModel的命名空间完全相同,除了Views和ViewModels字符串不匹配是允许的,且ViewModel的命名空间不能包含.Views.;View的名称无要求,但习惯是以View结尾,但ViewModel名称必须以ViewModel结尾。

    假设

    View命名空间: A.B.C.D.E

    ViewModel命名空间: a.b.c.d.e

    必须满足:View点分成5段,ViewModel也必须要点分成5段。对应序号的段相互比较(A和a比较,B和b比较...),必须完全相同,除非View段是Views,ViewModel段是ViewModels也被认为段相同,其他情况一律是段不同匹配失败!


    下面的例子是合规的,即使Views和ViewModels不在命名空间的末尾。

​ View : A.B.Views.C.EmployeeListView.xaml

​ ViewModel : A.B.ViewModels.C.EmployeeListViewModel.cs

​ 下面的例子也是合规的,即使View文件不以View结尾。

​ View : A.B.EmployeeList.xaml

​ ViewModel : A.B.EmployeeListViewModel.cs

  1. 所有的View开启自动确认ViewModel功能,不遵守命名规约的可以将其ViewModel的Type添加到字典,不让Prism费劲去分析FullName。

ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), typeof(CustomViewModel));

ViewModelLocationProvider.Register<MainWindow, CustomViewModel>();

​ 不遵守命名规约或虽然遵守但是ViewModel的实例化比较异类特殊,也可以将ViewModel的工厂委托添加到字典。

ViewModelLocationProvider.Register(typeof(MainWindow).ToString(), () => Container.Resolve<CustomViewModel>());

ViewModelLocationProvider.Register<MainWindow>(() => Container.Resolve<CustomViewModel>());

如果根据ViewModel Type进行实例化的defaultViewModelFactoryWithViewParameter和defaultViewModelFactory以及工厂委托使用依赖注入容器解析ViewModel的话,别忘了先注册ViewModel到容器。另外,记得工厂委托会优先于实例化策略来提供ViewModel实例。