什么是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
