【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>

在啟用可空引用類型後,再次編譯工程會出現兩個警告:

警告我們UserNamePassword未初始化. 可能在之後的使用中出現空引用異常.解決辦法有兩個:

  1. 為屬性設置初始值

    public string UserName { get; set; } = String.Empty;  public string Password { get; set; } = String.Empty;

    為用戶名和密碼設置初始值: 空字元串

  2. 將屬性聲明為可空類型

    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為我們準備好的ShellViewShellViewModel嗎? 它們就是負責程式的主窗口的,就從它們入手.

按以往的經驗, 可能在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是主窗口, 該窗口關閉後整個應用程式也隨之退出了.

為了使用ContainerWindowManager, 我們同樣在構造方法中將其注入,並使用類成員變數來接收它們:

    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~