乘风破浪,遇见Stylet超清爽WPF御用MVVM框架,爱不释手的.Net Core轻量级MVVM框架

什么是Stylet

https://github.com/canton7/Stylet

Stylet是受Caliburn.Micro启发的最小但功能强大的MVVM框架。其目的是进一步降低复杂性和魔力,使不熟悉任何MVVM框架的人员可以更快地加快速度。

image

它还提供了Caliburn.Micro不具备的功能,包括其自己的IoC容器,简便的ViewModel验证,甚至是与MVVM兼容的MessageBox

低的LOC数量和非常全面的测试套件使其成为使用和验证/验证SOUP的项目费用高昂的项目的有吸引力的选择,其模块化工具包启发式体系结构意味着您可以轻松地仅使用所需的位或替换你不会的。

不仅能支持.Net Framework,还支持最新的.Net Core 3.1.Net Core 5.x版本。

实践Stylet

https://github.com/TaylorShi/HelloStylet

安装Stylet模板

通过DotNet-Cli的New命令来安装Stylet.Templates项目模板。

dotnet new -i Stylet.Templates

image

创建名为HelloStylet的解决方案

通过Dotnet-Cli创建一个名为HelloStylet的解决方案。

dotnet new sln -o HelloStylet

image

切换到它的目录中。

cd .\HelloStylet\

image

创建名为HelloStyletClient的示例项目

通过Dotnet-Cli创建一个基于stylet模板,名为HelloStyletClient的项目。

dotnet new stylet -o HelloStyletClient

image

将其加入HelloStylet解决方案中。

dotnet sln add .\HelloStyletClient\HelloStyletClient.csproj

image

切换到它目录。

cd .\HelloStyletClient\

通过DotNet-CliRun命令来运行它。

dotnet watch run

image

通过vsc打开,我们看到默认创建的是.Net Core 5.0目标的项目。

image

如果要指定.Net Core 3.1,还可以指定目标版本。

dotnet new stylet -F netcoreapp3.1 -o HelloStyletClient3.1

注意,这里的-F必须是大写。

image

将其加入HelloStylet解决方案中。

dotnet sln add .\HelloStyletClient3.1\HelloStyletClient3.1.csproj

image

运行看看。

dotnet watch run --project .\HelloStyletClient3.1\HelloStyletClient3.1.csproj

image

通过vsc打开看看,确实是.Net Core 3.1的项目哈。

image

现有项目添加Stylet支持

先创建一个示例项目,名为HelloWpf

dotnet new wpf -o HelloWpf

image

将其加入HelloStylet解决方案中。

dotnet sln add .\HelloWpf\HelloWpf.csproj

image

切换到项目中。

cd .\HelloWpf\

https://www.nuget.org/packages/Stylet

添加Stylet的Nuget包。

dotnet add package Stylet

image

image

如果是.Net Framework项目,也可以这样处理。

dotnet add package Stylet

.NET Framework (<= .NET 4)

https://www.nuget.org/packages/Stylet.Start

dotnet add package Stylet.Start

Stylet的单向绑定

我们会看到HelloStyletClient这个项目模板创建的项目,它为我们做了一个示范,我们继续它探索MVVM的绑定功能。

image

我们看到Pages文件夹中已经有了一个示例页面Shell,这里已经生成好了ShellView.xamlShellView.xaml.csShellViewModel.cs这三个文件。

image

打开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!";

image

然后在ShellView.xaml中添加对应的绑定,这里用到了Binding这个关键词。

<Grid>
    <TextBlock
        FontSize="30"
        HorizontalAlignment="Center"
        VerticalAlignment="Center"
        Text="{Binding WelcomeWord}"
    />
</Grid>

运行看看,果然效果如预期,最终显示了Hello Stylet!的字眼,说明单向绑定肯定是成功了。

image

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>

image

这里通过一个StackPanel布局控件包括原来的TextBlock控件和新增的Button控件,然后Button之上,我们直接用Command关键词来做事件绑定,这里写法很简单:Command="{s:Action ClickMe}"s是复用了头部的一个空间引用xmlns:s="https://github.com/canton7/Stylet"Action代表了动作绑定,ClickMe便是我们定义的绑定事件方法的名称。

保存并运行它,我们来试试成功与否。

image

结果发现没反应!好家伙,难道我们写错了?其实没有,之前用过MVVM框架的都知道,是因为我们没有把改动后的WelcomeWord值通知到界面上来,以前我们是需要通过INotifyPropertyChanged来实现PropertyChanged通知的。

那在Stylet中我们怎么来做到这一点呢?实际上超级简单,这一步是少不了的,但是其实我们可以啥都不做,只需要引用一个神奇的包PropertyChanged.Fody,它会自动在编译时给已知属性注入IL代码,以达到PropertyChanged通知的效果。

https://www.nuget.org/packages/PropertyChanged.Fody

dotnet add package PropertyChanged.Fody

image

好了,完成添加之后,重新运行,再点击试试!没看错,就是这样通知了。

image

这里多说一下,像前面的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中,我们试试直接绑定TextBoxTextChanged事件看看。

<TextBox
    FontSize="30"
    Width="400"
    HorizontalAlignment="Center"
    VerticalAlignment="Center"
    Text="{Binding WelcomeWord, UpdateSourceTrigger=PropertyChanged}"
    TextChanged="{s:Action WelcomeWordTextChanged}"
    />

运行之后,我们看看Debug输出,实际效果符合预期。

image

在事件绑定的场景中,通常还有一个广泛的述求,就是通知事件的同时,还需要传递界面的参数过去,我们看看应该怎么写。

<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}";
}

就这样我们实现了绑定事件,同时传递参数的写法。

image

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 += " 提交";
}

image

接下来,我们回到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>

image

保存并运行,是的,如我们所设计的那样,提交按钮的可用状态会随着输入框中的内容是否存在而实时变化。

image

image

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,最终继承并实现了INotifyPropertyChangedINotifyPropertyChangedDispatcher接口。

image

image

image

所以,这里我们要把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,这次完美生效。

image

更多高阶实践

官方Github的Wiki文档是一个好的指引。

https://github.com/canton7/Stylet/wiki

参考

posted @ 2021-07-24 18:05  TaylorShi  阅读(3311)  评论(2编辑  收藏  举报