循序漸進介紹基於CommunityToolkit.Mvvm 和HandyControl的WPF應用端開發(11) -- 下拉選單的資料繫結以及自定義系統字典列表控制元件

2023-10-21 18:01:53

在我們開發的前端專案中,往往為了方便,都需對一些控制元件進行自定義的處理,以便實現快速的資料繫結以及便捷的使用,本篇隨筆介紹通過抽取常見字典列表,實現通用的字典型別繫結;以及通過自定義控制元件的屬性處理,實現系統字典內容的快捷繫結的操作。

1、下拉選單的資料繫結

在我們建立下拉選單的時候,我們一般處理方式,是在對應的資料模型中新增對應的下拉選單的集合物件,然後在控制元件繫結對應的ItemSource,如下所示是檢視模型,我們增加一個性別的列表參考。

/// <summary>
/// 使用者列表-檢視模型物件
/// </summary>
public partial class UserListViewModel : BaseListViewModel<UserInfo, int, UserPagedDto>
{
    /// <summary>
    /// 性別
    /// </summary>
    [ObservableProperty]
    private List<CListItem> genderItems;

    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="service">業務服務介面</param>
    public UserListViewModel(IUserService service) : base(service)
    {
        //初始化性別的列表
        this.GenderItems = new List<CListItem>()
        {
            new CListItem(""),
            new CListItem("")
        };
    }

然後初始化後,就可以在介面上進行資料的繫結了,如下是對應控制元件的介面程式碼。

<hc:ComboBox
    Margin="5"
    hc:TitleElement.Title="性別"
    hc:TitleElement.TitlePlacement="Left"
    DisplayMemberPath="Text"
    ItemsSource="{Binding ViewModel.GenderItems}"
    SelectedValue="{Binding ViewModel.PageDto.Gender, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    SelectedValuePath="Value"
    ShowClearButton="True" />

 這種方式可能是經常用到的方式,隨著不同介面程式碼的編寫,我們發現很多這樣下拉選單,如機構可能有一些類別(來自列舉物件)需要處理,其他頁面也有類似的需求。

/// <summary>
/// 機構(部門)資訊 列表-檢視模型物件
/// </summary>
public partial class OuListViewModel : BaseListViewModel<OuInfo, int, OuPagedDto>
{
    /// <summary>
    /// 機構分類
    /// </summary>
    [ObservableProperty]
    private List<CListItem> categoryItems = new();

    /// <summary>
    /// 建構函式
    /// </summary>
    /// <param name="service">業務服務介面</param>
    public OuListViewModel(IOuService service) : base(service)
    {  
        //機構分類
        string[] enumNames = EnumHelper.GetMemberNames<OUCategoryEnum>();
        this.CategoryItems.Clear();
        this.CategoryItems.AddRange(enumNames.Select(s => new CListItem(s)));
    }

如果每次都需要在對應的檢視模型上建立這些列表,則顯得累贅、臃腫。因為這些下拉選單的內容,是介面中常用到的列表,我們是否可以把它作為一個公用的物件模型來使用呢。

為了方便,我們來建立一個物件DictItemsModel ,用來初始化系統用到的所有參考列表物件,如下程式碼所示。

/// <summary>
/// 定義一些系統常用的字典專案,供頁面參考參照
/// </summary>
public partial class DictItemsModel : ObservableObject
{
    /// <summary>
    /// 性別
    /// </summary>
    [ObservableProperty]
    private  List<CListItem> genderItems = new();

    /// <summary>
    /// 機構分類
    /// </summary>
    [ObservableProperty]
    private List<CListItem> ouCategoryItems = new();

    //******更多列表處理**********

    /// <summary>
    /// 建構函式
    /// </summary>
    public DictItemsModel()
    {
        InitDictItem(); // 初始化字典
    }

    /// <summary>
    /// 初始化字典
    /// </summary>
    /// <returns></returns>
    public async Task InitDictItem()
    {
        //初始化性別的列表
        this.GenderItems = new List<CListItem>()
        {
            new(""),
            new(""),
            new("")
        };

        //機構分類
        this.OuCategoryItems = EnumHelper.GetMemberNames<OUCategoryEnum>().Select(s => new CListItem(s)).ToList();
        this.OuCategoryItems.Insert(0, new CListItem(""));

        //*********************
    }
}

然後,我們在應用程式的XAML程式碼中,引入對應的靜態資源,相當於每次使用這些的時候,是使用該物件的範例。

<Application
    x:Class="WHC.SugarProject.WpfUI.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:helpers="clr-namespace:WHC.SugarProject.WpfUI.Helpers"
    xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
    DispatcherUnhandledException="OnDispatcherUnhandledException"
    Exit="OnExit"
    Startup="OnStartup">
    <Application.Resources>
        <ResourceDictionary>
            <!--  整合所有用到的跳脫輔助類,減少頁面中新增的處理程式碼  -->
            <helpers:IntToBooleanConverter x:Key="IntToBooleanConverter" />
            
            <helpers:DictItemsModel x:Key="DictItemsModel" />
            
        </ResourceDictionary>
    </Application.Resources>
</Application>
            

有了這些定義,我們的下拉選單的資料來源ItemSource的屬性改動一下就可以實現一致的效果了,相當於抽取了字典列表到獨立的類中處理了。

<!--  ItemsSource="{Binding ViewModel.GenderItems}"  -->
<hc:ComboBox
    Margin="5"
    hc:TitleElement.Title="性別"
    hc:TitleElement.TitlePlacement="Left"
    DisplayMemberPath="Text"
    ItemsSource="{Binding Path=GenderItems, Source={StaticResource DictItemsModel}}"
    SelectedValue="{Binding ViewModel.PageDto.Gender, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    SelectedValuePath="Value"
    ShowClearButton="True" />
<!--  ItemsSource="{Binding ViewModel.CategoryItems}"  -->
<hc:ComboBox
    Margin="5"
    hc:TitleElement.Title="機構分類"
    hc:TitleElement.TitlePlacement="Left"
    DisplayMemberPath="Text"
    ItemsSource="{Binding Path=OuCategoryItems, Source={StaticResource DictItemsModel}}"
    SelectedValue="{Binding ViewModel.PageDto.Category, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    SelectedValuePath="Value"
    ShowClearButton="True" />

上面程式碼中,我們通過Source={StaticResource DictItemsModel}來指定資料來源的位置來自靜態資源即可。如機構列表,通過列舉進行解析到的集合如下所示,當然其他資料也可以通過相應的處理實現。

  

通過這種把常見的字典類別集中到一個類中進行維護,除了統一處理列表的初始化外,也方便我們在介面程式碼中的統一使用。

2、自定義系統字典列表控制元件

我們框架一般都維護一個通用的字典型別和字典專案的資訊,通過維護這些常見的系統字典資訊,可以為我們的介面的一些下拉類列表提供資料支援,是指實現通用、統一的字典處理。

以前在Winform中繫結字典列表的時候,一般通過擴充套件函數BindDictItems就可以實現型別繫結了,有興趣可以瞭解下隨筆《在Winform開發框架中下拉選單系結字典以及使用快取提高介面顯示速度》、《使用擴充套件函數方式,在Winform介面中快捷的繫結樹形列表TreeList控制元件和TreeListLookUpEdit控制元件》、《在Winform開發中,我們使用的幾種下拉選單展示字典資料的方式》、《在各種開發專案中使用公用類庫的擴充套件方法,通過上下文方式快速呼叫處理常式》。

對WPF來說,我們需要改變下思路,和Vue3的BS的控制元件的處理方式類似,我們通過給他指定一個字典型別的名稱,讓它自己取得對應列表,進行繫結處理即可,因此我們自定義字典列表控制元件即可。

    /// <summary>
    /// 自定義下拉選單,方便繫結字典型別
    /// </summary>
    public class ComboBox : HandyControl.Controls.ComboBox

建立一個繼承自所需下拉選單控制元件,可以使用原生控制元件繼承,不過我這裡偏向於UI更好的HandyControl的ComboBox。

然後給它指定對應的字典型別屬性,對應我們系統的字典大類名稱。

    /// <summary>
    /// 自定義下拉選單,方便繫結字典型別
    /// </summary>
    public class ComboBox : HandyControl.Controls.ComboBox
    {
        /// <summary>
        /// 字典型別名稱
        /// </summary>
        public string? DictTypeName
        {
            get { return (string?)GetValue(DictTypeNameProperty); }
            set { SetValue(DictTypeNameProperty, value); }
        }

        public static readonly DependencyProperty DictTypeNameProperty = DependencyProperty.Register(
            nameof(DictTypeName), typeof(string), typeof(ComboBox),
            new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnDictTypeNamePropertyChanged)));

封裝過自定義的WPF控制元件的話,我們知道,增加一個自定義屬性,就需要同時增加一個自定義屬性+Property的 DependencyProperty 屬性物件,如上程式碼所示。

通過PropertyChangedCallback的回撥處理,實現設定字典型別值後觸發控制元件內部資料的處理邏輯,也就是需要從字典服務中獲取下拉類別資料,變為控制元件的ItemSource集合即可。

一般情況下,我們實現下面的程式碼邏輯,獲得資料來源就差不過可以了。

private static async void OnDictTypeNamePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    if (d is not ComboBox control)
        return;

    if (control != null)
    {
        var oldValue = (string?)e.OldValue;  // 舊的值
        var newValue = (string?)e.NewValue; // 更新的新的值

        //更新下拉選單的資料來源
        if(!newValue.IsNullOrEmpty() && !control.IsInDesignMode())
        {
            var itemList = await BLLFactory<IDictDataService>.Instance.GetListItemByDictType(newValue);
            if (itemList != null)
            {
                itemList.Insert(0, new CListItem(""));//CListItem具有Text、Value兩個屬性
                control.ItemsSource = itemList;
                control.ShowClearButton = true;
                control.SelectedValuePath = control.SelectedValuePath.IsNullOrEmpty() ? "Value" : control.SelectedValuePath;
            }
        }
    }
}

同理,我們按照同樣的處理方式,做一個核取方塊的下拉選單CheckComboBox。

/// <summary>
/// 自定義下拉選單,方便繫結字典型別
/// </summary>
public class CheckComboBox : HandyControl.Controls.CheckComboBox
{
    /// <summary>
    /// 字典型別名稱
    /// </summary>
    public string? DictTypeName
    {
        get { return (string?)GetValue(DictTypeNameProperty); }
        set { SetValue(DictTypeNameProperty, value); }
    }

    public static readonly DependencyProperty DictTypeNameProperty = DependencyProperty.Register(
        nameof(DictTypeName), typeof(string), typeof(CheckComboBox),
        new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnDictTypeNamePropertyChanged)));

完成上面的自定義控制元件編寫,我們需要在UI上放置控制元件,指定它的指定型別就可以了。

<control:ComboBox
    Width="250"
    Height="32"
    VerticalAlignment="Center"
    hc:InfoElement.Title="客戶型別"
    hc:InfoElement.TitlePlacement="Left"
    DictTypeName="客戶型別"
    SelectedValue="{Binding ViewModel.PageDto.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    ShowClearButton="True"
    Style="{StaticResource ComboBoxPlusBaseStyle}" />
<control:CheckComboBox
    Width="500"
    Height="32"
    VerticalAlignment="Center"
    hc:InfoElement.Title="客戶型別"
    hc:InfoElement.TitlePlacement="Left"
    DictTypeName="客戶型別"
    SelectedValue="{Binding ViewModel.PageDto.Name, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
    SelectedValuePath="Value"
    ShowClearButton="True"
    ShowSelectAllButton="True"
    Style="{StaticResource CheckComboBoxPlus}" />

完成後,我們測試下自定義控制元件的處理效果。

    

效果符合實際的期望。而且程式碼和普通WPF控制元件的使用類似,只需要增加一個 DictTypeName="客戶型別" 的類似寫法即可。可以極大的減輕我們繫結常見系統字典的下拉選單的複雜度。

介面效果如下所示。