Stylet是一个轻量级的框架,旨在简化WPF-MVVM应用的创建过程,通过减少实现某些MVVM特性所需的代码量。本文将简要介绍这些特性,但请注意,本文不是WPF或MVVM的入门介绍。如果想要舒适地跟随本文,应该已经了解这两个概念。
之前写过关于Prism和MvvmCross的文章,使用了一个展示一些虚构员工卡片的示例应用程序。本文延续了这一趋势:示例应用程序有卡片,上面有一些员工的详细信息,点击卡片会导航到显示特定员工更多详细信息的页面。
示例应用程序有三个项目:一个.NET CoreWPF应用程序项目和两个.NET Core类库项目;一个包含共享代码,另一个是Stylet模块。可以从GitHub克隆或下载示例项目。
要在WPF应用程序项目中使用Stylet,必须引用Stylet NuGet包。然后,必须创建一个根视图和根视图模型,它们将根据命名约定相互关联。根视图,它必须是一个Window,作为应用程序的外壳。在示例项目中,ShellViewModel是根视图模型。
using Stylet;
namespace StaffStuff.ViewModels
{
public class ShellViewModel : Conductor<IScreen>.StackNavigation
{
public ShellViewModel(StaffViewModel staffViewModel)
{
this.DisplayName = string.Empty;
this.ActivateItem(staffViewModel);
}
}
}
ShellViewModel派生自Stylet的Conductor<T>.StackNavigation。导体管理它拥有的视图模型的生命周期。它决定一个视图模型是被激活、停用还是关闭。Conductor<T>.StackNavigation是一个提供基于栈的导航的导体。在示例应用程序中,导体将启用从卡片视图到详细信息视图的导航。
在ShellViewModel的构造函数中,一个视图模型作为依赖项传递,并设置为活动项。由于ShellViewModel是一个导体,它现在拥有StaffViewModel,并将管理其生命周期。在ShellView中,活动项,或者说与活动项关联的视图,将被放置在一个ContentControl中。
<Window x:Class="StaffStuff.Views.ShellView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="https://github.com/canton7/Stylet"
Background="#FF0D2738"
Height="480"
Width="800"
WindowStartupLocation="CenterScreen">
<Grid>
<ContentControl s:View.Model="{Binding ActiveItem}" />
</Grid>
</Window>
ActiveItem是导体的一个属性,通过调用导体的ActivateItem()方法设置。
有了根视图和视图模型,Stylet要求创建一个bootstrapper,在这里指定根视图模型。
namespace StaffStuff
{
public class Bootstrapper : Bootstrapper<ShellViewModel>
{
protected override void ConfigureIoC(IStyletIoCBuilder builder)
{
builder.AddModule(new ServicesModule());
}
}
}
然后将Stylet的ApplicationLoader添加为应用程序资源,并设置bootstrapper为加载项。
<Application x:Class="StaffStuff.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:StaffStuff"
xmlns:s="https://github.com/canton7/Stylet">
<Application.Resources>
<s:ApplicationLoader>
<s:ApplicationLoader.Bootstrapper>
<local:Bootstrapper/>
</s:ApplicationLoader.Bootstrapper>
</s:ApplicationLoader>
</Application.Resources>
</Application>
除了设置根视图模型,bootstrapper也是可以在Stylet的IoC容器中注册类型的地方。这是在ConfigureIoC()中使用StyleIoCBuilder完成的,所以可以做类似builder.Bind<IStaffData>().To<StaffData>()的事情。在示例项目的bootstrapper中,向构建器添加了一个模块,builder.AddModule(new ServicesModule())。
如引言中提到的,Stylet支持开发模块化的WPF-MVVM应用程序。要创建一个Stylet模块,创建一个.NET Core类库项目,引用Stylet NuGet包,并在项目的根目录添加一个派生自StyletIoCModule的类。然后可以在模块的Load()方法中将类型注册到IoC容器中。
using StaffStuff.Common.Interfaces;
using StaffStuff.Services.Services;
using StyletIoC;
namespace StaffStuff.Services
{
public class ServicesModule : StyletIoCModule
{
protected override void Load()
{
Bind<IStaffData>().To<StaffData>().InSingletonScope();
}
}
}
屏幕为派生自它的视图模型提供了验证、属性更改通知和活动状态监控。它还包括一个Parent属性,使视图模型能够知道哪个Conductor正在管理它,并允许它请求导体关闭它或激活不同的视图模型。在示例项目StaffViewModel中,它是由ShellViewModel激活的第一个视图模型,派生自Screen类。
using StaffStuff.Common.Interfaces;
using StaffStuff.Common.Models;
using Stylet;
using System.Collections.Generic;
namespace StaffStuff.ViewModels
{
public class StaffViewModel : Screen
{
private readonly IStaffData _staffData;
public StaffViewModel(IStaffData staffData)
{
_staffData = staffData;
}
private List<Employee> _employees;
public List<Employee> Employees
{
get => _employees;
set => SetAndNotify(ref _employees, value);
}
protected override void OnInitialActivate()
{
Employees = _staffData.GetEmployees();
}
public void StaffDetails(Employee employee)
{
var staffDetailsVM = new StaffDetailsViewModel { Employee = employee };
((ShellViewModel)this.Parent).ActivateItem(staffDetailsVM);
}
}
}
OnInitialActivate()方法仅在视图模型首次激活时调用一次。StaffViewModel的StaffDetails()方法调用导体的ActivateItem()方法,请求它激活StaffDetailsViewModel,触发导航到其关联的视图。
using StaffStuff.Common.Models;
using Stylet;
namespace StaffStuff.ViewModels
{
public class StaffDetailsViewModel : Screen
{
public StaffDetailsViewModel() { }
private Employee _employee;
public Employee Employee
{
get => _employee;
set => SetAndNotify(ref _employee, value);
}
public void GoBack()
{
this.RequestClose();
}
}
}
GoBack()调用屏幕的RequestClose()方法,这反过来导致导体关闭视图模型。这触发导航回到上一个视图,通过重新激活导体的上一个活动项——由于导体是Conductor<T>.StackNavigation类型,也可以通过调用导体的GoBack()方法触发导航回到上一个活动项。
public void GoBack()
{
((ShellViewModel)this.Parent).GoBack();
}
可能已经注意到StaffViewModel和StaffDetailsViewModel都没有ICommand属性。这是因为Stylet消除了这样的属性,而是允许视图模型中的一个方法被设置为Button的Command属性的值。Stylet通过Actions实现这一点。
<Button Command="{s:Action GoBack}">
...
</Button>
<DataTemplate x:Key="StaffDataTemplate" DataType="{x:Type models:Employee}">
<Border Margin="10" Cursor="Hand" BorderThickness="1" Background="#FF16394F" BorderBrush="#FF3F5666" CornerRadius="8" Width="200" Height="240">
...
<behaviors:Interaction.Triggers>
<behaviors:EventTrigger EventName="MouseLeftButtonUp">
<behaviors:InvokeCommandAction Command="{s:Action StaffDetails}" CommandParameter="{Binding}" />
</behaviors:EventTrigger>
</behaviors:Interaction.Triggers>
</Border>
</DataTemplate>
注意,也可以将CommandParameter的值传递给方法,如果它需要传递参数。