一個可以讓你有更多時間摸魚的WPF控制元件(一)

趋时软件發表於2024-03-29

前言

我們平時在開發軟體的過程中,有這樣一類比較常見的功能,它沒什麼技術含量,開發起來也沒有什麼成就感,但是你又不得不花大量的時間來處理它,它就是對資料的增刪改查。當我們每增加一個需求就需要對應若干個頁面來處理資料的新增、修改、刪除、查詢,每個頁面因為資料欄位的差異需要單獨處理佈局及排列,在處理資料錄入或修改的過程中還需要校驗資料的準確性。那麼有沒有什麼方法可以簡化這個過程,提升我們的工作效率?今天這篇文章我們來討論這個問題。

一、務分析

我們分析一下傳統的開發流程,看看有哪些地方可以最佳化。

1.1 新增資料

資料錄入的時候我們需要先確定要錄入哪些資料,每條資料都是什麼型別,然後在新增資料介面上設計佈局,確認引數的排列方式,還需要做必要的資料校驗工作,比如判斷輸入是否為空,判斷電子郵箱格式是否正確,判斷電話號碼格式是否正確等,有時還需要在輸入前提示一些必要的資訊,比如告知使用者正確的輸入格式,限定輸入的內容必須為某些字元等。

最佳化方案:

1)使用反射讀取實體類屬性,根據實體類的屬性型別生成不同的資料錄入控制元件。

2)實體類實現IDataErrorInfo介面,實現資料校驗功能。

1.2 修改資料

基本與新增資料一致。

1.3 刪除資料

選中資料後執行刪除操作,基本無最佳化空間。

1.4 查詢資料

查詢資料一般使用DataGrid或ListView作為資料列表使用,DataGrid對很多表格功能做了封裝,它可以對每行的資料進行編輯,可以自動生成列,如果不是特別複雜的需求使用DataGrid的自動列,確實可以節省很多工作,只要簡單的繫結一下就可以使用這些功能。但是真實的業務場景需求千變萬化,我們來看看會碰到哪些問題。

1) 設定自動列的時候,DataGrid列顯示的是屬性名,而屬性名往往都是英文的,中文環境中基本都是使用中文列名。

2) 設定自動列的時候無法對資料進行格式化操作,無法使用轉換器。

3) 設定自動列時無法對列的順序做自定義排列。

4) 設定自動列時無法控制自定義列的排列,比如在第一列設定一個CheckBox核取方塊,在列尾設定編輯、刪除按鈕等。

5)設定自動列時無法單獨指定某列的寬度。

6)設定自動列時無法單獨隱藏某些列。

雖然以上問題也有解決方案,但是實現起來略顯繁瑣。

最佳化方案:

1)為實體類開發一個特性類(ColumnAttribute),新增列名、排序、寬度、是否可見、轉換器、格式化字串等屬性。

2)新增一個ListView控制元件附加屬性,讀取以上特性,在ListView控制元件附加屬性上實現以上功能。

二、例程式碼

透過對業務需求的分析,我們總結出了幾點最佳化方案,下面展示根據最佳化方案開發出來的Form控制元件,該控制元件可以大大節省開發期間枯燥的重複工作,提升工作效率。

View

<Window
    x:Class="QuShi.Controls.Samples.Views.EntityEditorView1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
    xmlns:prism="http://prismlibrary.com/"
    Width="450"
    prism:ViewModelLocator.AutoWireViewModel="True"
    SizeToContent="Height"
    WindowStartupLocation="CenterScreen">

    <StackPanel Margin="20" Orientation="Vertical">
        <Form Source="{Binding Person}" />
        <Button Margin="10" Padding="20,5" HorizontalAlignment="Center" Command="{Binding ConfirmCommand}" Content="提交" />
    </StackPanel>
</Window>

ViewModel

public class EntityEditorView1ViewModel : BindableBase
{
    public event Action<Person> Completed;

    private Person _person;
    public Person Person
    {
        get => _person;
        set => this.SetProperty(ref _person, value);
    }

    public DelegateCommand<object> ConfirmCommand
    {
        get
        {
            return new DelegateCommand<object>(parameter =>
            {
                if (!string.IsNullOrEmpty(Person.Error))
                {
                    MessageBox.Show(Person.Error);
                }
                else
                {
                    Completed?.Invoke(Person);
                }
            });
        }
    }
}

Model

public class Person : ValidationObject
{
    private string _name;
    private DateTime _dateOfBirth = DateTime.Now;
    private int _age;
    private int _gender = 1;
    private string _phoneNumber;
    private string _email;
    private string _address;
    private string _idCardNumber;
    private EducationInfo _education;
    private MaritalStatus _maritalStatus;

    /// <summary>
    /// 姓名
    /// </summary>
    [DisplayOrder(1)]
    [DisplayHint("請填寫姓名")]
    [DisplayName("姓名")]
    [StringLength(15, MinimumLength = 2, ErrorMessage = "姓名的有效長度為2-15個字元.")]
    [Required(ErrorMessage = "姓名為必填項.")]
    [Column(Name = "姓名", Order = 1)]
    public string Name
    {
        get => _name;
        set => this.SetProperty(ref _name, value);
    }

    /// <summary>
    /// 出生日期
    /// </summary>
    [DisplayOrder(2)]
    [DisplayHint("請選擇出生日期")]
    [DisplayName("出生日期")]
    [Required(ErrorMessage = "出生日期為必填項.")]
    [Column(Name = "出生日期", StringFormat = "{0:yyyy年MM月dd日}", Order = 2)]
    public DateTime DateOfBirth
    {
        get => _dateOfBirth;
        set => this.SetProperty(ref _dateOfBirth, value);
    }

    /// <summary>
    /// 年齡
    /// </summary>
    [DisplayOrder(3)]
    [DisplayHint("請填寫年齡")]
    [DisplayName("年齡")]
    [Range(1, 120, ErrorMessage = "年齡的有效值為1-120.")]
    [Required(ErrorMessage = "年齡為必填項.")]
    [Column(Name = "年齡", Order = 3)]
    public int Age
    {
        get => _age;
        set => this.SetProperty(ref _age, value);
    }

    /// <summary>
    /// // 性別
    /// </summary>
    [DisplayOrder(4)]
    [DisplayHint("請選擇性別")]
    [DisplayName("性別")]
    [OverwriteType(HandleMethod.RadioButton, "1=男", "2=女", "3=其他")]
    [Column(Name = "性別", Order = 4)]
    public int Gender
    {
        get => _gender;
        set => this.SetProperty(ref _gender, value);
    }

    /// <summary>
    /// 手機號碼
    /// </summary>
    [DisplayOrder(5)]
    [DisplayHint("請填寫電話號碼")]
    [DisplayName("電話號碼")]
    [Phone(ErrorMessage = "電話號碼格式不正確.")]
    [Column(Name = "電話號碼", Order = 5)]
    public string PhoneNumber
    {
        get => _phoneNumber;
        set => this.SetProperty(ref _phoneNumber, value);
    }

    /// <summary>
    /// 電子郵箱
    /// </summary>
    [DisplayOrder(6)]
    [DisplayHint("請填寫電子郵箱")]
    [DisplayName("電子郵箱")]
    [EmailAddress(ErrorMessage = "電子郵箱格式不正確.")]
    [Column(Name = "電子郵箱", Order = 6)]
    public string Email
    {
        get => _email;
        set => this.SetProperty(ref _email, value);
    }

    /// <summary>
    /// 地址資訊
    /// </summary>
    [DisplayOrder(7)]
    [DisplayHint("請填寫地址")]
    [DisplayName("地址")]
    [StringLength(50, ErrorMessage = "地址的最大長度為50個字元.")]
    [Column(Name = "地址", Order = 7)]
    public string Address
    {
        get => _address;
        set => this.SetProperty(ref _address, value);
    }

    /// <summary>
    /// 身份證號碼
    /// </summary>
    [DisplayOrder(8)]
    [DisplayName("身份證號碼")]
    [DisplayHint("請填寫身份證號碼")]
    [RegularExpression(RegexHelper.IdCardNumber, ErrorMessage = "身份證號碼格式不正確.")]
    [Column(Name = "身份證號碼", Order = 8)]
    public string IdCardNumber
    {
        get => _idCardNumber;
        set => this.SetProperty(ref _idCardNumber, value);
    }

    /// <summary>
    /// 教育資訊
    /// </summary>
    [DisplayOrder(9)]
    [DisplayHint("請填寫教育資訊")]
    [DisplayName("教育資訊")]
    [Browsable(false)]
    public EducationInfo Education
    {
        get => _education;
        set => this.SetProperty(ref _education, value);
    }

    /// <summary>
    /// 婚姻狀況
    /// </summary>
    [DisplayOrder(10)]
    [DisplayHint("請選擇婚姻狀況")]
    [DisplayName("婚姻狀況")]
    [Column(Name = "婚姻狀況", Order = 10)]
    public MaritalStatus MaritalStatus
    {
        get => _maritalStatus;
        set => this.SetProperty(ref _maritalStatus, value);
    }
}

public class EducationInfo
{
    public string Degree { get; set; } // 學位
    public string Major { get; set; } // 專業
    public string School { get; set; } // 學校
    public DateTime GraduationDate { get; set; } // 畢業日期
}

public enum MaritalStatus
{
    /// <summary>
    /// 單身
    /// </summary>
    [Description("單身")]
    Single,
    /// <summary>
    /// 已婚
    /// </summary>
    [Description("已婚")]
    Married,
    /// <summary>
    /// 離異
    /// </summary>
    [Description("離異")]
    Divorced,
    /// <summary>
    /// 喪偶
    /// </summary>
    [Description("喪偶")]

執行效果

三、答疑解惑

3.1 如何實現每個屬性的自定義佈局?

答:控制元件提供了預設外觀,如果無法滿足要求,也可以編輯控制元件模板,在相應的資料型別模板中修改佈局程式碼,也可以只編輯某種型別的控制元件模板,下面展示修改String型別模板,其它型別基本相似。

<Form Source="{Binding Person}">
    <Form.StringTemplate>
        <DataTemplate>
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition SharedSizeGroup="RowHeight" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto" SharedSizeGroup="Title" />
                    <ColumnDefinition Width="10" />
                    <ColumnDefinition Width="*" />
                </Grid.ColumnDefinitions>
                <StackPanel
                            HorizontalAlignment="Right"
                            VerticalAlignment="Center"
                            Orientation="Horizontal">
                    <TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
                    <TextBlock
                                VerticalAlignment="Center"
                                Foreground="Red"
                                Text="*"
                                Visibility="{Binding IsRequired, Converter={StaticResource BooleanToVisibilityConverter}}" />
                </StackPanel>
                <TextBox
                            Grid.Column="2"
                            MinWidth="150"
                            VerticalAlignment="Center"
                            extensions:BindingExtensions.BindingProperty="{x:Static TextBox.TextProperty}"
                            extensions:BindingExtensions.BindingSource="{Binding}" />
            </Grid>
        </DataTemplate>
    </Form.StringTemplate>
</Form>

3.2 如果控制元件模板中提供的基礎資料型別沒有我需要的屬性型別,如何擴充套件新的屬性型別?

答:控制元件提供了一個名為“CustomTypeTemplates”的屬性來處理這個問題,以下為示例程式碼。

<Form Source="{Binding Person}">
    <Form.CustomTypeTemplates>
        <CustomTypeDataTemplateCollection>
            <CustomTypeDataTemplate CustomType="{x:Type model:EducationInfo}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition SharedSizeGroup="RowHeight" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" SharedSizeGroup="Title" />
                        <ColumnDefinition Width="10" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <StackPanel
                                HorizontalAlignment="Right"
                                VerticalAlignment="Center"
                                Orientation="Horizontal">
                        <TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
                        <TextBlock
                                    VerticalAlignment="Center"
                                    Foreground="Red"
                                    Text="*"
                                    Visibility="{Binding IsRequired, Converter={StaticResource BooleanToVisibilityConverter}}" />
                    </StackPanel>
                    <TextBox
                                Grid.Column="2"
                                MinWidth="150"
                                VerticalAlignment="Center"
                                extensions:BindingExtensions.BindingProperty="{x:Static TextBox.TextProperty}"
                                extensions:BindingExtensions.BindingSource="{Binding}" />
                </Grid>
            </CustomTypeDataTemplate>
        </CustomTypeDataTemplateCollection>
    </Form.CustomTypeTemplates>
</Form>

3.3 如果我想自定義某個屬性名的模板,如何實現?

答:控制元件提供了一個名為“CustomNameTemplates”的屬性來處理這個問題,以下為示例程式碼。

<Form Source="{Binding Person}">
    <Form.CustomNameTemplates>
        <CustomNameDataTemplateCollection>
            <CustomNameDataTemplate CustomName="Name">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition SharedSizeGroup="RowHeight" />
                    </Grid.RowDefinitions>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" SharedSizeGroup="Title" />
                        <ColumnDefinition Width="10" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <StackPanel
                                HorizontalAlignment="Right"
                                VerticalAlignment="Center"
                                Orientation="Horizontal">
                        <TextBlock VerticalAlignment="Center" Text="{Binding Name}" />
                        <TextBlock
                                    VerticalAlignment="Center"
                                    Foreground="Red"
                                    Text="*"
                                    Visibility="{Binding IsRequired, Converter={StaticResource BooleanToVisibilityConverter}}" />
                    </StackPanel>
                    <TextBox
                                Grid.Column="2"
                                MinWidth="150"
                                VerticalAlignment="Center"
                                extensions:BindingExtensions.BindingProperty="{x:Static TextBox.TextProperty}"
                                extensions:BindingExtensions.BindingSource="{Binding}" />
                </Grid>
            </CustomNameDataTemplate>
        </CustomNameDataTemplateCollection>
    </Form.CustomNameTemplates>
</Form>

3.4 如果我想隱藏某些屬性應該怎麼做?

答:對屬性設定“Browsable”特性,以下為示例程式碼。

[Browsable(false)]
public EducationInfo Education
{
    get => _education;
    set => this.SetProperty(ref _education, value);
}

3.5 如何繫結列舉型別?

答:列舉在Form控制元件中預設顯示為下拉框(ComboBox),只需要在列舉中設定"Description“特性就可以正常顯示中文選項,如果不設定該屬性則直接顯示列舉名稱。

public enum MaritalStatus
{
    /// <summary>
    /// 單身
    /// </summary>
    [Description("單身")]
    Single,
    /// <summary>
    /// 已婚
    /// </summary>
    [Description("已婚")]
    Married,
    /// <summary>
    /// 離異
    /// </summary>
    [Description("離異")]
    Divorced,
    /// <summary>
    /// 喪偶
    /// </summary>
    [Description("喪偶")]
    Widowed
}

3.6 如何繫結複雜屬性,諸如單選框(RadioButton)、多選框(CheckBox)、下拉框(ComboBox)等?

答:控制元件讀取一個名為”OverwriteType“的特性,特性中有一個名為“HandleMethod”的屬性,該屬性指明瞭覆蓋當前型別的方式,並需要指定對映引數。

[OverwriteType(HandleMethod.RadioButton, "1=男", "2=女", "3=其他")]
public int Gender
{
    get => _gender;
    set => this.SetProperty(ref _gender, value);
}
public enum HandleMethod
{
    ComboBox,
    CheckBox,
    RadioButton
}

以上為資料新增及修改部分的最佳化實現,下一節講解如何在查詢列表中最佳化。

技術交流群
聯絡方式

相關文章