循序漸進介紹基於CommunityToolkit.Mvvm 和HandyControl的WPF應用端開發(2)

2023-09-11 12:00:23

在前面隨筆《循序漸進介紹基於CommunityToolkit.Mvvm 和HandyControl的WPF應用端開發(1)》中介紹了Mvvm 的開發,以及一些介面效果,本篇隨筆繼續深入探討基於CommunityToolkit.Mvvm 和HandyControl的WPF應用端開發,介紹如何整合SqlSugar框架的基礎介面,通過基礎類別繼承的方式,簡化實際專案的開發程式碼處理。

1、View模組中的XAML格式說明

在介紹MVVM幾個部分內容之前,我們先連線一下View模組中的Xaml格式的說明,我們知道Xaml也是一個xml的擴充套件,屬於標示語言的一種,編輯器為了更好的驗證格式以及提出上下文的智慧提示,必然需要確定對應標記元素的格式,這個就是通過Xaml的頭部定義確立了,如下所示。

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WHC.SugarProject.WpfUI.Views.Pages"

    x:Class="WHC.SugarProject.WpfUI.Views.Pages.UserListPage"
    d:DataContext="{d:DesignInstance local:UserListPage,
                                     IsDesignTimeCreatable=False}"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">

其中紅色部分基本上是約定需要輸入的定義了,主要是通過xmlns來定義定義XML的校驗格式,類似我們常用的名稱空間的參照地址了。

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"

而下面 xmlns:local 也是定義了原生的XML名稱空間。

xmlns:local="clr-namespace:WHC.SugarProject.WpfUI.Views.Pages"

便於在介面元素標記中參照對應的控制元件等。而x:Class則是確定XAML的後臺程式碼檔案的全名稱,使用過早期ASPX的都知道,ASPX檔案有一個後臺程式碼檔案,同理XAML也是類似的概念。

x:Class="WHC.SugarProject.WpfUI.Views.Pages.UserListPage"

而d:開始的那些設定,是指設計樣式下的一些屬性定義,如下所示定義設計表單的大小,實際執行可能和這個不一樣,因為WPF是屬於向量的尺寸標記的。

    d:DesignHeight="450"
    d:DesignWidth="800"

d:DataContext則是宣告View和模型繫結的一個重要的說明,介面檢視View可以繫結模型的命令Command,也可以繫結對應的集合或者屬性等。其中local:UserListPage,就是通過簡寫命名控制元件local和UserListPage類的組合,實現一個全名稱的快速定義。而IsDesignTimeCreatable則是說明設計狀態下的資料處理方式。

    d:DataContext="{d:DesignInstance local:UserListPage,
                                     IsDesignTimeCreatable=False}"

有時候,我們可能在視窗或者頁面檢視的定義中,還會看到一些其他名稱空間的定義或者屬性的定義,如下程式碼所示。

<Page
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WHC.SugarProject.WpfUI.Views.Pages"

    xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
    xmlns:helpers="clr-namespace:WHC.SugarProject.WpfUI.Helpers"
    xmlns:hc="https://handyorg.github.io/handycontrol"
    xmlns:Controls="clr-namespace:WHC.SugarProject.WpfUI.Controls"

    x:Class="WHC.SugarProject.WpfUI.Views.Pages.UserListPage"
    d:DataContext="{d:DesignInstance local:UserListPage,
                                     IsDesignTimeCreatable=False}"
    d:DesignHeight="450"
    d:DesignWidth="800"
    mc:Ignorable="d">

例如上面的紅色部分,就是一般根據實際情況增加的一些控制元件的名稱空間的定義,便於參照對應的介面控制元件進行使用。

<hc:UniformSpacingPanel
    Margin="0,0,0,20"
    HorizontalAlignment="Right"
    Orientation="Horizontal"
    Spacing="10">
    <Button
        Width="60"
        Height="40"
        Command="{Binding SaveCommand}"
        Content="儲存"
        Style="{StaticResource ButtonPrimary}" />
    <Button
        Width="60"
        Height="40"
        Command="{Binding ViewModel.BackCommand}"
        CommandParameter="test"
        Content="關閉" />
</hc:UniformSpacingPanel>

有時候可能還會看到一些屬性設定,如背景色,以及是否捲動頁面等設定。

    Background="{DynamicResource RegionBrush}"
    ScrollViewer.CanContentScroll="true"

 

2、MVVM應用中包括Model、View、ViewModel三者內容中的處理

 前面我們見到,View檢視中繫結ViewModel模型的時候,使用d:DataConext進行定義設定,確定檢視模型的內容,我們剛才介紹的定義是如下所示。

    d:DataContext="{d:DesignInstance local:UserListPage,
                                     IsDesignTimeCreatable=False}"

這裡你看到的它對映向其本身,而非具體的Viewmodel,這裡沒有錯誤,有些MVVM的設定指向具體的ViewModel定義。

我們來看看這個UserListPage的後端類的定義,如下所示。

首先我們看到一個如下所示。

namespace WHC.SugarProject.WpfUI.Views.Pages;

/// <summary>
/// UserListPage.xaml 互動邏輯
/// </summary>
public partial class UserListPage : INavigableView<UserListViewModel>
{
    /// <summary>
    /// 檢視模型物件
    /// </summary>
    public UserListViewModel ViewModel { get; }

    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="viewModel">檢視模型物件</param>
    public UserListPage(UserListViewModel viewModel)
    {
        ViewModel = viewModel;
        DataContext = this;

        InitializeComponent();
    }
}

這裡通過泛型介面的方式,定義一個具體型別的檢視模型給檢視View使用。

其中INavigableView介面的定義如下。

public interface INavigableView<out T>
{
    /// <summary>
    /// 檢視的 ViewModel
    /// </summary>
    T ViewModel { get; }
}

這樣我們在Xaml檢視裡面,就可以繫結屬性,也可以繫結相應的Command命令了,可以是類的,也可以是ViewModel的內容。

例如我們在繫結查詢條件處理資料查詢的操作的時候,

 其中介面查詢條件定義了一個文字方塊名稱的控制元件,如下所示。

<TextBox
    Margin="5"
    hc:TitleElement.Title="使用者賬號"
    hc:TitleElement.TitlePlacement="Left"
    Style="{StaticResource TextBoxExtend}"
    Text="{Binding ViewModel.PageDto.Name, UpdateSourceTrigger=PropertyChanged}">
    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding ViewModel.SearchCommand}" />
    </TextBox.InputBindings>
</TextBox>

我們通過 {Binding ViewModel.PageDto.Name, UpdateSourceTrigger=PropertyChanged} 的方式繫結文字的介面顯示內容和後端模型屬性的,並且在繫結的控制元件屬性變化的時候觸發更新。這樣只要介面上輸入的內部發生變化,後端繫結的查詢屬性馬上得到更新,我們就可以即時的通過查詢觸發獲得相應條件的記錄了。

另外通過 InputBindings 的方式,接收Enter內容後馬上觸發查詢處理的命令。

查詢的時候,我們在檢視模型上定義一個RelayCommand的特性標註,宣告這個生成的Command為檢視提供處理命令的。

    /// <summary>
    /// 觸發查詢處理命令
    /// </summary>
    /// <returns></returns>
    [RelayCommand]
    private async Task Search()
    {
        //切換第一頁
        this.PagerInfo.CurrentPageIndex = 1;

        //轉換下分頁資訊
        ConvertPagingInfo();
        //查詢更新
        await GetData();
    }

然後我們通過轉換分頁條件的資訊,獲得查詢條件後進行服務介面的呼叫,獲取相應條件的資料即可。

這個介面的後端就是SqlSugar框架的標準請求介面了。

    /// <summary>
    /// 根據分頁和查詢條件查詢,請求資料
    /// </summary>
    /// <returns></returns>
    public virtual async Task GetData()
    {
        var result = await service.GetListAsync(this.PageDto);
        if (result != null)
        {
            this.Items = result.Items?.ToList();
            this.PagerInfo.RecordCount = result.TotalCount;
        }
    }

我們看到,由於框架的通用性抽象處理,因此上面的介面應該是可以實現更高層次的抽象處理的,因此我們設計了幾個檢視基礎類別,用於減少程式碼的處理。

 其中 ObservableObject 基礎類別是CommunityToolkit.Mvvm模組裡面定義的檢視模型基礎類別,我們定義的通用基礎類別也是基於它繼承過來即可。

其中BaseViewModel用來處理一些通用的檢視模型操作方式,如跳轉或者返回等。

而BaseListViewModel則是根據查詢列表處理的操作介面所需要的檢視模型處理封裝。

我們看到類定義和我們SqlSugar很多基礎類別定義類似,都是需要通過泛型傳入一些相關的引數,實現更加通用的控制的。

public abstract partial class BaseListViewModel<TEntity, TKey, TGetListInput> : BaseViewModel, INavigationAware
        where TEntity : class, IEntity<TKey>, new()
        where TGetListInput : IPagedAndSortedResultRequest, new()

由於.net的開發方式,現在基本上都是以介面注入方式來處理,這個也是一樣,我們通過注入一個常規服務介面的類,來實現一些常規的請求處理。檢視模型類的介面注入如下所示。

    /// <summary>
    /// 通用基礎操作介面
    /// </summary>
    protected IMyCrudService<TEntity, TKey, TGetListInput> service { get; set; }

    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="service">操作介面</param>
    /// <param name="navigationService">檢視導航服務介面</param>
    public BaseListViewModel(IMyCrudService<TEntity, TKey, TGetListInput> service)
    {
        this.service = service;
    }

這樣我們在分頁控制元件的頁碼變化的時候,就可以觸發這個基礎類別的命令處理了。

    /// <summary>
    /// 觸發的分頁處理命令
    /// </summary>
    /// <param name="info"></param>
    /// <returns></returns>
    [RelayCommand]
    private async Task PageUpdated(FunctionEventArgs<int> info)
    {
        //根據分頁頁碼展示
        this.PagerInfo.CurrentPageIndex = info.Info;

        //轉換下分頁資訊
        ConvertPagingInfo();
        //查詢更新
        await GetData();
    }

它的分頁控制元件部分的檢視介面的程式碼如下所示。

<hc:Pagination
    Margin="0,10,0,10"
    DataCountPerPage="{Binding ViewModel.PagerInfo.PageSize}"
    IsJumpEnabled="True"
    MaxPageCount="{Binding ViewModel.PagerInfo.MaxPageCount}"
    MaxPageInterval="5"
    PageIndex="{Binding ViewModel.PagerInfo.CurrentPageIndex}">
    <hc:Interaction.Triggers>
        <hc:EventTrigger EventName="PageUpdated">
            <hc:EventToCommand Command="{Binding ViewModel.PageUpdatedCommand}" PassEventArgsToCommand="True" />
        </hc:EventTrigger>
    </hc:Interaction.Triggers>
</hc:Pagination>

而我們查詢命令,也可以通過下面的基礎類別函數來處理了。

    /// <summary>
    /// 觸發查詢處理命令
    /// </summary>
    /// <returns></returns>
    [RelayCommand]
    private async Task Search()
    {
        //切換第一頁
        this.PagerInfo.CurrentPageIndex = 1;

        //轉換下分頁資訊
        ConvertPagingInfo();
        //查詢更新
        await GetData();
    }

這樣我們在前面介紹的文字方塊的TextBox的InputBindings處理就可以直接使用這個RelayCommand宣告的命令函數了。

    <TextBox.InputBindings>
        <KeyBinding Key="Enter" Command="{Binding ViewModel.SearchCommand}" />
    </TextBox.InputBindings>

當然,這個基礎類別裡面還可以定義其他通用用到的一些常規處理,如匯入匯出、刪除和批次刪除等常規的介面。

而我們在這個使用者業務模型裡面所需要做的就是做一下繼承關係的處理即可,如下程式碼所示。

namespace WHC.SugarProject.WpfUI.ViewModels;

/// <summary>
/// 使用者列表-檢視模型物件
/// </summary>
public partial class UserListViewModel : BaseListViewModel<UserInfo, int, UserPagedDto>
{
    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="service">業務服務介面</param>
    public UserListViewModel(IUserService service) : base(service)
    {
    }
}

這樣 UserListViewModel 就具有了一些通用的業務處理物件和命令了,包括常規的查詢、刪除、批次刪除、匯入、匯出的處理,都可以了。

同理,對於編輯具體單個記錄的處理,我們也可以使用通用的抽象業務類封裝來實現,如下程式碼所示。

/// <summary>
/// 使用者新增、編輯-檢視模型
/// </summary>
public partial class UserEditViewModel : BaseEditViewModel<UserInfo, int, UserPagedDto>
{
    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="service"></param>
    public UserEditViewModel(IUserService service) : base(service)
    {
        this.Title = "使用者資訊";
    }
}

至此,整個檢視模型ViewModel的繼承關係如下所示。對於不同的業務類,我們也只需要根據實際情況,生成對應的業務檢視模型類即可。

DataGrid列表展示的處理程式碼,主要就是基礎類別觸發查詢更新後,屬性ViewModel.Items 的記錄獲得更新,這樣DataGrid就可以順利的顯示對應的資料了,如下介面所示。

 

3、介面的統一處理和程式碼生成處理

對於介面上的處理,我們常規的列表和編輯/新增介面,基本上可以滿足大多數的要求,如下是列表介面的效果,包括查詢條件、常規處理按鈕、列表展示、分頁資訊展示等幾個部分,如下所示。

 對於一些編輯介面,也是類似下面的佈局即可。

 當然可以根據資料庫欄位進行設定展示那些輸入資訊最好了。如對於系統引數設定模組的資訊,我們介面如下所示。

 這些內容相對比較標準和統一,可以結合資料庫表資訊使用統一的方式快速生成即可,因此我把它的生成規則結合到我們的程式碼生成工具(Database2Sharp)中進行生成處理,通過選擇那些欄位來實現更加精確的介面處理。

 檢視介面程式碼如下所示,包括Xaml和後端類的程式碼。

 對應有兩個檢視模型ViewModel的繼承類。

 這樣實現,可以極大程度的減少子類的程式碼,以及通過程式碼生成工具快速定義生成的方式,又可以極大的提高開發效率,雙管齊下,可以提高整個專案的開發效率。