前言
我們平時在開發軟體的過程中,有這樣一類比較常見的功能,它沒什麼技術含量,開發起來也沒有什麼成就感,但是你又不得不花大量的時間來處理它,它就是對資料的增刪改查。當我們每增加一個需求就需要對應若干個頁面來處理資料的新增、修改、刪除、查詢,每個頁面因為資料欄位的差異需要單獨處理佈局及排列,在處理資料錄入或修改的過程中還需要校驗資料的準確性。那麼有沒有什麼方法可以簡化這個過程,提升我們的工作效率?今天這篇文章我們來討論這個問題。
一、業務分析
我們分析一下傳統的開發流程,看看有哪些地方可以最佳化。
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 }
以上為資料新增及修改部分的最佳化實現,下一節講解如何在查詢列表中最佳化。