乘风破浪,遇见Stylet超清爽WPF御用MVVM框架,爱不释手的.Net Core轻量级MVVM框架
什么是Stylet
Stylet
是受Caliburn.Micro
启发的最小但功能强大的MVVM框架。其目的是进一步降低复杂性和魔力,使不熟悉任何MVVM框架的人员可以更快地加快速度。
它还提供了Caliburn.Micro
不具备的功能,包括其自己的IoC容器,简便的ViewModel
验证,甚至是与MVVM兼容的MessageBox
。
低的LOC数量和非常全面的测试套件使其成为使用和验证/验证SOUP的项目费用高昂的项目的有吸引力的选择,其模块化工具包启发式体系结构意味着您可以轻松地仅使用所需的位或替换你不会的。
不仅能支持.Net Framework
,还支持最新的.Net Core 3.1
和.Net Core 5.x
版本。
实践Stylet
安装Stylet模板
通过DotNet-Cli
的New命令来安装Stylet.Templates
项目模板。
dotnet new -i Stylet.Templates
创建名为HelloStylet的解决方案
通过Dotnet-Cli
创建一个名为HelloStylet
的解决方案。
dotnet new sln -o HelloStylet
切换到它的目录中。
cd .\HelloStylet\
创建名为HelloStyletClient的示例项目
通过Dotnet-Cli
创建一个基于stylet
模板,名为HelloStyletClient
的项目。
dotnet new stylet -o HelloStyletClient
将其加入HelloStylet
解决方案中。
dotnet sln add .\HelloStyletClient\HelloStyletClient.csproj
切换到它目录。
cd .\HelloStyletClient\
通过DotNet-Cli
的Run
命令来运行它。
dotnet watch run
通过vsc打开,我们看到默认创建的是.Net Core 5.0
目标的项目。
如果要指定.Net Core 3.1
,还可以指定目标版本。
dotnet new stylet -F netcoreapp3.1 -o HelloStyletClient3.1
注意,这里的-F
必须是大写。
将其加入HelloStylet
解决方案中。
dotnet sln add .\HelloStyletClient3.1\HelloStyletClient3.1.csproj
运行看看。
dotnet watch run --project .\HelloStyletClient3.1\HelloStyletClient3.1.csproj
通过vsc打开看看,确实是.Net Core 3.1
的项目哈。
现有项目添加Stylet支持
先创建一个示例项目,名为HelloWpf
。
dotnet new wpf -o HelloWpf
将其加入HelloStylet
解决方案中。
dotnet sln add .\HelloWpf\HelloWpf.csproj
切换到项目中。
cd .\HelloWpf\
添加Stylet
的Nuget包。
dotnet add package Stylet
如果是.Net Framework
项目,也可以这样处理。
dotnet add package Stylet
.NET Framework (<= .NET 4)
dotnet add package Stylet.Start
Stylet的单向绑定
我们会看到HelloStyletClient
这个项目模板创建的项目,它为我们做了一个示范,我们继续它探索MVVM的绑定功能。
我们看到Pages
文件夹中已经有了一个示例页面Shell
,这里已经生成好了ShellView.xaml
、ShellView.xaml.cs
、ShellViewModel.cs
这三个文件。
打开ShellView.xaml
可以看到,这个页面已经帮我们指定了一个MVVM对象,也就是指向ShellViewModel
。
d:DataContext="{d:DesignInstance local:ShellViewModel}"
同时,我们查看到ShellViewModel.cs
是一个继承自Stylet.Screen
的MVVM文件。
using Stylet;
namespace HelloStyletClient.Pages
{
public class ShellViewModel : Screen
{
}
}
我们试试把TextBlock这个文字,从MVVM那里绑定过来。
先在ShellViewModel.cs
中添加一个WelcomeWord
的属性字段。
/// <summary>
/// 欢迎词
/// </summary>
/// <value></value>
public string WelcomeWord { get; set; } = "Hello Stylet!";
然后在ShellView.xaml
中添加对应的绑定,这里用到了Binding
这个关键词。
<Grid>
<TextBlock
FontSize="30"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding WelcomeWord}"
/>
</Grid>
运行看看,果然效果如预期,最终显示了Hello Stylet!
的字眼,说明单向绑定肯定是成功了。
Stylet的事件绑定
说到事件绑定,大家一定很熟悉了,就是我可以将界面上的控件事件绑定到MVVM层,按以前估计要写一堆东西,那么Stylet
下怎么做呢?
既然前面已经单向绑定了WelcomeWord
,那我们就在它上面做点改进吧,我们设计一个按钮点击事件来修改它。
我们先在ShellViewModel.cs
中添加一个ClickMe
的事件方法。
/// <summary>
/// 点我事件
/// </summary>
public void ClickMe()
{
WelcomeWord += " 点我";
}
它要实现的逻辑就简单一点,在原有WelcomeWord
内容的基础上,直接追加 点我
字符串吧。
然后我们回到ShellView.xaml
中,稍微改造下页面布局,我们需要加入一个按钮进来,之前已经存在一个文字了,为了和按钮并存,我们引入一个布局控件,叫StackPanel
,最终改造后的XAML内容如下:
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock
FontSize="30"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding WelcomeWord}"
/>
<Button
Margin="0,20,0,0"
Content="点我"
FontSize="24"
Command="{s:Action ClickMe}"
/>
</StackPanel>
</Grid>
这里通过一个StackPanel
布局控件包括原来的TextBlock
控件和新增的Button
控件,然后Button
之上,我们直接用Command
关键词来做事件绑定,这里写法很简单:Command="{s:Action ClickMe}"
,s
是复用了头部的一个空间引用xmlns:s="https://github.com/canton7/Stylet"
,Action
代表了动作绑定,ClickMe
便是我们定义的绑定事件方法的名称。
保存并运行它,我们来试试成功与否。
结果发现没反应!好家伙,难道我们写错了?其实没有,之前用过MVVM框架的都知道,是因为我们没有把改动后的WelcomeWord
值通知到界面上来,以前我们是需要通过INotifyPropertyChanged
来实现PropertyChanged
通知的。
那在Stylet
中我们怎么来做到这一点呢?实际上超级简单,这一步是少不了的,但是其实我们可以啥都不做,只需要引用一个神奇的包PropertyChanged.Fody
,它会自动在编译时给已知属性注入IL代码,以达到PropertyChanged
通知的效果。
dotnet add package PropertyChanged.Fody
好了,完成添加之后,重新运行,再点击试试!没看错,就是这样通知了。
这里多说一下,像前面的Button
控件控件,我们直接使用了Command
来绑定它的单击事件,但是很多时候,如果一个控件具有多种事件,我们需要区分绑定的时候怎么办?打个比如,对TextBox
控件来说,我们想绑定的TextChanged
事件,那么我们来实现下这个场景。
我们先在ShellViewModel.cs
中添加一个WelcomeWordTextChanged
的事件方法来接收TextChanged
事件。
/// <summary>
/// 欢迎词变更事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
public void WelcomeWordTextChanged(object sender, EventArgs e)
{
Debug.WriteLine(((System.Windows.Controls.TextBox)sender)?.Text ?? string.Empty);
}
我们设计响应动作是将事件触发对象拿到后转成它的原始控件System.Windows.Controls.TextBox
,然后安全输入它的数值。
然后我们回到ShellView.xaml
中,我们试试直接绑定TextBox
的TextChanged
事件看看。
<TextBox
FontSize="30"
Width="400"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding WelcomeWord, UpdateSourceTrigger=PropertyChanged}"
TextChanged="{s:Action WelcomeWordTextChanged}"
/>
运行之后,我们看看Debug
输出,实际效果符合预期。
在事件绑定的场景中,通常还有一个广泛的述求,就是通知事件的同时,还需要传递界面的参数过去,我们看看应该怎么写。
<Button
Margin="0,20,0,0"
Content="提交"
FontSize="24"
Command="{s:Action SubmitMe}"
CommandParameter="Hello"
IsEnabled="{Binding CurrentWorkRecord.IsCanSubmitMe}"
/>
这里在通过Command
绑定SubmitMe
的同时,我们还通过CommandParameter
传递了一个参数过去Hello
。
在接收方法SubmitMe
中,我们可以增加一个参数入参。
/// <summary>
/// 提交事件
/// </summary>
public void SubmitMe(string argument)
{
CurrentWorkRecord.WelcomeWord += $" {argument}";
}
就这样我们实现了绑定事件,同时传递参数的写法。
Stylet的双向绑定
说到MVVM,除了从VM层往界面通知,对于用户输入的场景,我们往往还需要双向绑定通知,这样VM层可以实时拿到界面上用户输入的值,这里我们来举个例子。
我们先注释掉前面的
TextBlock
控件,引入一个用户可输入的TextBox
控件,这样更好的满足双向绑定的场景需求,为了更加形象表达这个场景的效果,我们做一个设计,那就是当TextBox
控件的内容被清空时,我们就自动禁用Button
,来阻止用户的界面提交,这里我们引用一个名叫IsCanSubmitMe
的属性值来绑定Button
控件的IsEnabled
属性,来演示当输入框内容清空时,按钮也会不可用,当有内容时,按钮自动恢复。
我们先在ShellViewModel.cs
中改造原来的ClickMe
的事件方法为SubmitMe
方法,并且我们引入Boolean
类型的属性IsCanSubmitMe
,它将实时计算WelcomeWord
的值以得到是否禁用的状态。
/// <summary>
/// 能否提交
/// </summary>
/// <value></value>
public bool IsCanSubmitMe => !string.IsNullOrEmpty(WelcomeWord);
/// <summary>
/// 提交事件
/// </summary>
public void SubmitMe()
{
WelcomeWord += " 提交";
}
接下来,我们回到ShellView.xaml
中,注释掉原来的TextBlock
控件,引入一个可供输入交互的TextBox
控件,并且适当改造原来的Button
控件文案,以满足场景诉求。
与此同时,我们将Button
控件的IsEnabled
属性绑定到VM中的IsCanSubmitMe
上,让它实时接收IsCanSubmitMe
的值。
接下来,最关键的一步是,如何让TextBox
控件的输入数值实时通知到VM层的WelcomeWord
属性上呢?我们只需要给绑定加上关键词UpdateSourceTrigger=PropertyChanged
,这是一个熟悉的用法。
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- <TextBlock
FontSize="30"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding WelcomeWord}"
/> -->
<TextBox
FontSize="30"
Width="400"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding WelcomeWord, UpdateSourceTrigger=PropertyChanged}"
/>
<Button
Margin="0,20,0,0"
Content="提交"
FontSize="24"
Command="{s:Action SubmitMe}"
IsEnabled="{Binding IsCanSubmitMe}"
/>
</StackPanel>
</Grid>
保存并运行,是的,如我们所设计的那样,提交按钮的可用状态会随着输入框中的内容是否存在而实时变化。
Stylet的对象绑定
前面我们绑定都是放在了VM文件中,但是随着实际项目变大,我们肯定不会全部这样设计,因为这样VM就太庞大了,通常我们会新建很多对象,需要绑定到对象上去,那么我们继续探索,如果把前面的效果用对象来实现怎么玩?
我们先在ShellViewModel.cs
中新增一个全局的工作记录对象模型WorkRecord
。
/// <summary>
/// 工作记录
/// </summary>
public class WorkRecord
{
/// <summary>
/// 欢迎词
/// </summary>
/// <value></value>
public string WelcomeWord { get; set; } = "Hello Stylet!";
/// <summary>
/// 能否提交
/// </summary>
/// <value></value>
public bool IsCanSubmitMe => !string.IsNullOrEmpty(WelcomeWord);
}
然后在VM里面定义一个WorkRecord
类型的新属性值CurrentWorkRecord
,并且做好初始化。
/// <summary>
/// 当前工作记录
/// </summary>
/// <returns></returns>
public WorkRecord CurrentWorkRecord { get; set; } = new WorkRecord();
接下来,我们回到ShellView.xaml
中,修改之前的两个属性绑定,从原来直接绑定到VM层,改成绑定到VM中CurrentWorkRecord
对象中的属性值去。
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBox
FontSize="30"
Width="400"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Text="{Binding CurrentWorkRecord.WelcomeWord, UpdateSourceTrigger=PropertyChanged}"
/>
<Button
Margin="0,20,0,0"
Content="提交"
FontSize="24"
Command="{s:Action SubmitMe}"
IsEnabled="{Binding CurrentWorkRecord.IsCanSubmitMe}"
/>
</StackPanel>
</Grid>
运行之后,试试看,结果发现没效果?咋回事,哈哈,因为WorkRecord
类型中并没有自动通知机制,为啥VM中有呢,我们看看VM有啥不一样,VM继承了Stylet.Screen
,而Stylet.Screen
继承了Stylet.ValidatingModelBase
,然后它还继承了Stylet.PropertyChangedBase
,最终继承并实现了INotifyPropertyChanged
和INotifyPropertyChangedDispatcher
接口。
所以,这里我们要把WorkRecord
继承下Stylet.PropertyChangedBase
才行。
/// <summary>
/// 工作记录
/// </summary>
public class WorkRecord : PropertyChangedBase
{
/// <summary>
/// 欢迎词
/// </summary>
/// <value></value>
public string WelcomeWord { get; set; } = "Hello Stylet!";
/// <summary>
/// 能否提交
/// </summary>
/// <value></value>
public bool IsCanSubmitMe => !string.IsNullOrEmpty(WelcomeWord);
}
再次运行试试看,OK,这次完美生效。
更多高阶实践
官方Github的Wiki文档是一个好的指引。
https://github.com/canton7/Stylet/wiki