【WPF on .NET Core 3.0】 Stylet演示項目 – 簡易圖書管理系統(1)
- 2019 年 10 月 17 日
- 筆記
.NET Core 3.0已經發布了,除了一大堆令人激動的功能以外,也增加了對WPF的正式支援, 那麼WPF在.NET Core 3.0下的開發體驗如何呢?
本文利用了Stylet框架開發.NET Core 3.0上的WPF應用程式.關於Stylet框架, 可能大家比較陌生, 它是一個輕量級(但是非常優秀!)的WPF框架, 最近也更新了對.NET Core 3.0的支援, 關於Stylet的介紹可以瀏覽我之前的一篇博文: ViewModel從未如此清爽 – 輕量級WPF MVVM框架Stylet.本文也包含了使用MVVM和Stylet的一些最佳實踐.
為了簡單起見, 示例項目主要演示了使用Stylet開發WPF應用程式中常用的功能,如綁定,窗口彈出,表單驗證等.沒有使用資料庫,也不包含如用戶認證管理等一般的業務功能.
項目全部程式碼都利用.NET的最新技術棧完成, 包括:
- VS2019
- .NET Core 3.0
- C# 8
文中也會對其中的一些新特性做出說明.
下面就來跟隨我體會一下這個船新的版本吧!
前期準備
- VS2019 16.3.2
- .NET Core SDK 3.0.100
-
安裝stylet模板
打開cmd或powershell運行以下命令(確保.NET Core 3.0的SDK已安裝):
dotnet new -i Stylet.Templates
-
創建一個Stylet工程
dotnet new stylet -o StyletBookStore
這樣一個使用Stylet的WPF應用程式就創建好了!
VS2019打開StyletBookStore.csproj, 直接按F5運行:
OK. 感覺不錯!
增加登錄功能
雖然是個演示項目,但是我們還是需要登錄介面的:), Stylet中一個介面至少需要兩部分: "ViewModel"和"View". 我們來分別實現它們.
ViewModel
在Pages文件夾下創建一個名為LoginViewModel.cs
的類,用來實現登錄的介面邏輯.並輸入以下程式碼:
public class LoginViewModel : Screen { /// <summary> /// 用戶名 /// </summary> public string UserName { get; set; } /// <summary> /// 密碼 /// </summary> public string Password { get; set; } /// <summary> /// 登錄 /// </summary> public void Login() { // 在這實現登錄邏輯 } /// <summary> /// 登錄的防護屬性 /// </summary> public bool CanLogin => !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password); }
- 該類繼承了
Stylet.Screen
, 這是Stylet中一個常用的ViewModel基類. - 定義了兩個public屬性, 分別代表用戶名和密碼.
- 定義了一個名為
Login
的方法,用來實現登錄的邏輯.我們會在稍後實現它. -
定義了一個名為
CanLogin
的防護屬性, 用來檢查Login
方法是否可以運行. 只有當用戶名和密碼都輸入時,才允許運行登錄方法.防護屬性(Guard Properties)是Stylet的一個功能, 是一個返回布爾型的只讀屬性. 屬性名的命名約定為Can + 防護的方法名. 更多資訊請瀏覽Guard Properties.
啟用可空引用類型
下面我們試用一下C#8中新增加的特性: 可空引用類型.
使用可空引用類型可在編譯時就檢查潛在的空引用問題.關於可空引用類型,請參考我之前翻譯的一篇文章初試C# 8.0中"可空的引用類型"章節.
右鍵點擊StyletBookStore
工程,選擇"Edit Project File":
在打開的StyletBookStore.csproj文件中增加一行配置:
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>netcoreapp3.0</TargetFramework> <RootNamespace>StyletBookStore</RootNamespace> <UseWPF>true</UseWPF> <Nullable>enable</Nullable> <!-- 啟用可空引用類型 --> </PropertyGroup> ... </Project>
在啟用可空引用類型後,再次編譯工程會出現兩個警告:
警告我們UserName
和Password
未初始化. 可能在之後的使用中出現空引用異常.解決辦法有兩個:
-
為屬性設置初始值
public string UserName { get; set; } = String.Empty; public string Password { get; set; } = String.Empty;
為用戶名和密碼設置初始值: 空字元串
-
將屬性聲明為可空類型
public string? UserName { get; set; } public string? Password { get; set; }
使用?標識用戶名為密碼是可空類型
這裡我們使用第1種方法,將用戶名和密碼初始化為空字元串.再次編譯,警告就消失了.
View
在Pages文件夾下創建一個為名LoginView.xaml
的文件,用來定義登錄的UI. 其中主要的程式碼如下:
<Window ... d:DataContext="{d:DesignInstance pages:LoginViewModel}" > <Grid> ... <TextBlock Grid.Row="0" Grid.Column="0" Text="用戶名" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Right"></TextBlock> <TextBlock Grid.Row="1" Grid.Column="0" Text="密碼" Margin="5" VerticalAlignment="Center" HorizontalAlignment="Right"></TextBlock> <TextBox Grid.Row="0" Grid.Column="1" Margin="5" Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"></TextBox> <PasswordBox Grid.Row="1" Grid.Column="1" Margin="5" wpf:PasswordHelper.Attach="True" wpf:PasswordHelper.Password="{Binding Path=Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"></PasswordBox> <Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Content="登錄" Margin="5" Padding="15 5 15 5" Command="{s:Action Login}"></Button> </Grid> </Window>
- 使用
d:DataContext="{d:DesignInstance ...}"
為XAML設置設計時的數據源, 以在編寫XAML時獲得智慧提示. - 出於安全性考慮, WPF中密碼框不支援綁定.這裡為了簡單起見,使用了一個擴展的附加屬性類
PasswordHelper
,用來實現Password的綁定.具體程式碼請瀏覽PasswordHelper - 使用
{s:Action Login}
將登錄按鈕的Command綁定到Login
方法中, 這樣按下登錄按鈕就會執行ViewModel中的Login方法. 這是Stylet提供的功能, 名為Action. 更多資訊請瀏覽Actions and Methods
HotReload
VS2019為XAML增加了一個便利功能HotReload, 該功能可允許開發者在應用程式運行時修改XAML程式碼,並立即看到修改後的效果:
這樣我們在設計UI時會方便很多:)
實現登錄邏輯
修改Login
方法,實現登錄功能:
/// <summary> /// 登錄 /// </summary> public void Login() { // 設計的非常健壯的用戶驗證機制:) if (UserName != "waku" || Password != "123") { _windowManager.ShowMessageBox("用戶名或密碼不正確", "登錄失敗", MessageBoxButton.OK, MessageBoxImage.Exclamation); return; } RequestClose(true); }
-
當用戶輸入的用戶名和密碼不符合我們非常健壯的驗證規則時,彈出錯誤資訊.
這裡使用了
WindowManager.ShowMessageBox
來顯示消息對話框,WindowManager
是Stylet提供的功能, 專門用來管理窗口的顯示.為了使用
WindowManager
, 在ViewModel中聲明一個IWindowManager
介面類型的成員變數,並使用Stylet內置的IoC功能將它的實例通過構造方法注入進去.private readonly IWindowManager _windowManager; public LoginViewModel(IWindowManager windowManager) { _windowManager = windowManager; }
這樣就可在ViewModel中使用WindowManager了
也許你更習慣使用
MessageBox.Show
來顯示消息對話框, 但這麼做你的ViewModel就和UI組件耦合在一起了,而這是違反MVVM設計模式的. 想像一下你需要為你的ViewModel編寫測試程式碼時, 因為其中使用了MessageBox.Show
, 那麼在測試中處理與UI組件的交互就變成了很困難的事. 而使用Stylet中的WindowManager
,你可以在測試中Mock它的介面IWindowManager
來模擬消息框的行為. -
驗證通過後,我們使用
RequestClose(true)
通知Stylet關閉Login窗口,並返回結果true
.
至此我們的登錄ViewModel就完成了, 完整的程式碼如下:
using System; using System.Windows; using Stylet; namespace StyletBookStore.Pages { public class LoginViewModel : Screen { private readonly IWindowManager _windowManager; /// <summary> /// 用戶名 /// </summary> public string UserName { get; set; } = String.Empty; /// <summary> /// 密碼 /// </summary> public string Password { get; set; } = String.Empty; public LoginViewModel(IWindowManager windowManager) { _windowManager = windowManager; } /// <summary> /// 登錄 /// </summary> public void Login() { // 設計的非常安全的用戶驗證機制:) if (UserName != "waku" || Password != "123") { _windowManager.ShowMessageBox("用戶名或密碼不正確", "登錄失敗", MessageBoxButton.OK, MessageBoxImage.Exclamation); return; } RequestClose(true); } /// <summary> /// 登錄的防護屬性 /// </summary> public bool CanLogin => !string.IsNullOrEmpty(UserName) && !string.IsNullOrEmpty(Password); } }
此時工程結構應該是這樣的:
顯示登錄窗口
我們已經準備好登錄窗口了, 但是如何顯示它呢? 還記得Stylet為我們準備好的ShellView
和ShellViewModel
嗎? 它們就是負責程式的主窗口的,就從它們入手.
按以往的經驗, 可能在ShellView.xaml.cs
中寫下這樣的程式碼:
public ShellView() { InitializeComponent(); Loaded += OnLoaded; } private void OnLoaded(object sender, RoutedEventArgs e) { var window = new LoginView(); window.ShowDialog(); }
實際上這也是違反MVVM的, 因為我們直接操作了視圖, 正確的做法是以ViewModel為中心, 由ViewModel來驅動各視圖的顯示.
實際上你可以直接從工程中刪除所有的.xaml.cs文件, Stylet不需要它們!
我們來看看使用Stylet該如何實現.
在ShellViewModel
類中增加OnViewLoaded
的重寫方法:
protected override void OnViewLoaded() { var loginViewModel = _container.Get<LoginViewModel>(); var result = _windowManager.ShowDialog(loginViewModel); if (result != true) { RequestClose(); } }
- OnViewLoaded方法是基類
Screen
中定義的一個方法, 當ViewModel綁定的View載入完成後會調用該方法. - 使用
Container.Get
方法獲取LoginViewModel
的實例.Container
是Stylet中IoC的容器, 通過它的Get
方法我們可以取得所有註冊到IoC中類的實例. - 然後調用
WindowManager.ShowDialog
, 並將LoginViewModel
實例做為參數傳遞進行.Stylet就會為我們顯示子窗口了. - 當登錄窗口關閉後, 判斷窗口的返回值如果不是true, 代表登錄失敗. 這裡和登錄一樣,我們仍然調用
RequestClose
進行關閉操作, 但是因為Shell是主窗口, 該窗口關閉後整個應用程式也隨之退出了.
為了使用Container
和WindowManager
, 我們同樣在構造方法中將其注入,並使用類成員變數來接收它們:
private readonly IContainer _container; private readonly IWindowManager _windowManager; public ShellViewModel(IContainer container, IWindowManager windowManager) { _container = container; _windowManager = windowManager; }
這與在登錄中注入WindowManager
基本類似, 就不再解釋了.
注入
Container
並使用Get
方法, 被稱為"服務定位模式"(Service Locator Pattern), 對於它是否也違反了MVVM有很多爭議. Stylet的WIKI上對此也有討論和解決辦法. 但是我個人很喜歡這種方式.
完整的ShellViewModel
類程式碼如下:
using Stylet; using StyletIoC; namespace StyletBookStore.Pages { public class ShellViewModel : Screen { private readonly IContainer _container; private readonly IWindowManager _windowManager; public ShellViewModel(IContainer container, IWindowManager windowManager) { _container = container; _windowManager = windowManager; } protected override void OnViewLoaded() { var loginViewModel = _container.Get<LoginViewModel>(); var result = _windowManager.ShowDialog(loginViewModel); if (result != true) { RequestClose(); } } } }
運行
好了, 我們的第一個功能 – "登錄"就已經完成了, 編譯運行應用程式:
我們需要確認以下4個功能點:
- 當"用戶名"或"密碼"為空時, 是不允許登錄的("登錄"按鈕處于禁用狀態).
- 用戶名輸入"waku", 並且密碼輸入"123", 登錄窗口關閉, 回到主窗口.
- 否則顯示"用戶名或密碼不正確"的消息框.
- 點擊登錄窗口右上角的"X"按鈕,整個應用程式退出.
需要確認的功能點很少, 所以我們手動確認也可很快完成, 但是對於複雜一些的應用程式, 手動確認就很麻煩了, 而且在頻繁的迭代過程中, 回歸測試也很必要. 所以下一篇文章中我們會學習如何為ViewModel編寫單元測試程式碼.
本篇到此為止, 希望朋友們能多多留言. 源碼託管在GITHUB上.
Happy Coding~