循序漸進介紹基於CommunityToolkit.Mvvm 和HandyControl的WPF應用端開發(3)--自定義使用者控制元件

2023-09-13 12:01:20

在我們建立介面元素的時候,不管在Vue3+ElementPlus的前端上,還是Winform桌面端上,都是會利用自定義使用者控制元件來快速重用一些自定義的介面內容,對自定義使用者控制元件的封裝處理,也是我們開發WPF應用需要熟悉的一環。本篇隨筆繼續深入介紹介紹基於CommunityToolkit.Mvvm 和HandyControl的WPF應用端開發,主要針對自定義使用者控制元件的封裝和使用做一些介紹。

1、自定義使用者控制元件的應用場景

在我們使用原生的WPF控制元件的時候,有時候發現常規的原生控制元件不夠好看,或者功能達不到要求,就需要進行一定程度上的二次封裝處理,也就是自定義控制元件的開發場景。

例如我們前面介紹到的使用者資訊的查詢介面,我們沒有找到一個輸入數值範圍的控制元件,如對於年齡等類似的屬性,我們需要一個區間的查詢處理,可以保留為空,或者最小、最大值之間進行查詢,如下介面所示。

由於WPF沒有這樣的原生控制元件,我們需要的話,就需要使用常規的數值或者文字控制元件來進行處理,如果多次有這樣的內容,封裝為自定義控制元件,讓她簡單的使用,是最為優雅的方式。

我們看到控制元件的外觀如下所示。

 

2、自定義控制元件的開發程式碼

我們可以用Grid佈局來進行處理,包括兩個TextBlock和兩個文字的控制元件介面,我們建立自定義控制元件後,在Xaml定義好佈局資訊。

<UserControl
    x:Class="WHC.SugarProject.WpfUI.Controls.NumericRange"
    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:hc="https://handyorg.github.io/handycontrol"
    xmlns:local="clr-namespace:WHC.SugarProject.WpfUI.Controls"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Name="NumericRangeControl"
    d:Background="Transparent"
    d:DesignHeight="32"
    d:DesignWidth="150"
    d:Foreground="White"
    mc:Ignorable="d">
    <Grid MinWidth="150">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <TextBlock
            Grid.Column="0"
            Margin="10,0,10,0"
            VerticalAlignment="Center"
            Text="{Binding Path=Text, RelativeSource={RelativeSource AncestorType={x:Type local:NumericRange}}}" />
        <TextBox
            x:Name="txtStart"
            Grid.Column="1"
            Margin="5"
            Text="{Binding Path=StartValue, Converter={StaticResource NumericConverter}, RelativeSource={RelativeSource AncestorType={x:Type local:NumericRange}}}" />
        <TextBlock
            Grid.Column="2"
            Margin="5,0,5,0"
            VerticalAlignment="Center"
            Text="~" />
        <TextBox
            x:Name="txtEnd"
            Grid.Column="3"
            Margin="5"
            Text="{Binding Path=EndValue, Converter={StaticResource NumericConverter}, RelativeSource={RelativeSource AncestorType={x:Type local:NumericRange}}}" />
    </Grid>
</UserControl>

其中繫結動態屬性的地方,我們使用下面程式碼

 Text="{Binding Path=StartValue, Converter={StaticResource NumericConverter}, RelativeSource={RelativeSource AncestorType={x:Type local:NumericRange}}}" 

當然也可以使用Element的標記方式,這種我們需要設定使用者自定義控制元件名稱為Name=「***」,如上面的程式碼設定為。

Name="NumericRangeControl"

這樣我們就可以通過自定義控制元件的ElementName來定位繫結的屬性了,等同於如下程式碼。

<TextBox
            x:Name="txtStart"
            Grid.Column="1"
            Margin="5"
            Text="{Binding Path=StartValue, Converter={StaticResource NumericConverter}, ElementName=NumericRangeControl}" />

前面我們介紹了該控制元件包含了的一些屬性,如StartValue、EndValue、以及文字說明Text等,這些是在使用者控制元件後臺程式碼裡面進行定義的自定義依賴屬性的,我們來看看程式碼。

 如我們增加一個StartValue,那麼同時需要增加一個StartValueProperty的自定義依賴屬性。

        /// <summary>
        /// 開始值
        /// </summary>
        public decimal? StartValue
        {
            get { return (decimal?)GetValue(StartValueProperty); }
            set { SetValue(StartValueProperty, value); }
        }

        public static readonly DependencyProperty StartValueProperty = DependencyProperty.Register(
            nameof(StartValue), typeof(decimal?), typeof(NumericRange),
            new FrameworkPropertyMetadata(default(decimal?), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, new PropertyChangedCallback(OnStartValuePropertyChanged)));

同時,這個屬性的變化,會觸發一個控制元件路由的事件OnStartValuePropertyChanged ,如下所示。

        private static void OnStartValuePropertyChanged(DependencyObject d,DependencyPropertyChangedEventArgs e)
        {
            if (d is not NumericRange control)
                return;

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

                var args = new RoutedPropertyChangedEventArgs<decimal?>(oldValue, newValue);
                args.RoutedEvent = NumericRange.ValueChangedEvent;
                control.RaiseEvent(args);
                control.ValueChangedCommand?.Execute(null);
            }
        }

除了觸發路由事件外,我們可以給該控制元件定義一個Command 命令,類似按鈕的命令處理,繫結後就可以接受到相關的通知了。Command的定義如下程式碼所示。

        /// <summary>
        /// 數量改變命令
        /// </summary>
        public static readonly DependencyProperty ValueChangedCommandProperty =
            DependencyProperty.Register("ValueChangedCommand", typeof(ICommand), typeof(NumericRange), new PropertyMetadata(default(ICommand)));

        /// <summary>
        /// 數量改變命令
        /// </summary>
        public ICommand ValueChangedCommand
        {
            get { return (ICommand)GetValue(ValueChangedCommandProperty); }
            set { SetValue(ValueChangedCommandProperty, value); }
        }

3、自定義控制元件的使用

自定義控制元件開發好後,使用也是很簡單的,需要在頁面或者視窗的定義部分,增加控制元件的名稱空間,便於參照自定義控制元件,如下程式碼所示。

 xmlns:Controls="clr-namespace:WHC.SugarProject.WpfUI.Controls"

這樣我們在使用的時候,就和其他原生控制元件的使用差不多了。如下是在頁面中使用的Xaml程式碼。

 <Controls:NumericRange
      EndValue="{Binding ViewModel.PageDto.AgeEnd, UpdateSourceTrigger=PropertyChanged}"
      StartValue="{Binding ViewModel.PageDto.AgeStart, UpdateSourceTrigger=PropertyChanged}"
      Text="年齡"
      ValueChangedCommand="{Binding ViewModel.SearchCommand}" />

我們可以看到自定義控制元件的屬性的繫結,和其他控制元件的屬性繫結一致的,而且我們這裡定義了一個Command:ValueChangedCommand

我們可以通過這個命令接收控制元件變化的通知。這樣就可以正常的實現我們所需要的處理功能了。

另外,自定義控制元件的輸入框,一般會在失去焦點後觸發命令處理,我們也可以讓文字輸入框在輸入後回車觸發命令處理,我們增加一個KeyDown的事件處理,如下程式碼所示。

        <TextBox
            x:Name="txtStart"
            Grid.Column="1"
            Margin="5"
            KeyDown="txtStartEndValue_KeyDown"
            Text="{Binding Path=StartValue, Converter={StaticResource NumericConverter}, ElementName=NumericRangeControl}" />

        <TextBlock
            Grid.Column="2"
            Margin="5,0,5,0"
            VerticalAlignment="Center"
            Text="~" />

        <TextBox
            x:Name="txtEnd"
            Grid.Column="3"
            Margin="5"
            KeyDown="txtStartEndValue_KeyDown"
            Text="{Binding Path=EndValue, Converter={StaticResource NumericConverter}, RelativeSource={RelativeSource AncestorType={x:Type local:NumericRange}}}" />

讓回車切換到下一個焦點即可。

        private void txtStartEndValue_KeyDown(object sender, KeyEventArgs e)
        {
            if(e.Key == Key.Enter)
            {
                var textBox = sender as System.Windows.Controls.TextBox;
                if(textBox != null)
                {
                    //切換焦點會觸發值更新命令
                    textBox.MoveFocus(new TraversalRequest(FocusNavigationDirection.Next));
                }
            }
        }

至此我們就完成了完美的控制元件處理事件了。

編譯後,我們就可以在工具列中看到使用者自定義控制元件的列表了,可以直接拖動它到頁面進行使用。

 至此,我們就實現了自定義控制元件在頁面上的使用了,非常簡單。

 

 當然,我們也可以組合一些面板,來實現更加複雜的控制元件呈現方式,可以設計一些圖表、文字內容的綜合展示,如下是其中的一個控制元件的多層展示。

根據不同的圖示、內容,背景色、以及一些集合形狀的疊加,就可以設計出非常好看的單個使用者控制元件,然後動態設定,就可以很好的實現不同的內容展示。