一个可以让你有更多时间摸鱼的WPF控件(一)
前言
我们平时在开发软件的过程中,有这样一类比较常见的功能,它没什么技术含量,开发起来也没有什么成就感,但是你又不得不花大量的时间来处理它,它就是对数据的增删改查。当我们每增加一个需求就需要对应若干个页面来处理数据的添加、修改、删除、查询,每个页面因为数据字段的差异需要单独处理布局及排列,在处理数据录入或修改的过程中还需要校验数据的准确性。那么有没有什么方法可以简化这个过程,提升我们的工作效率?今天这篇文章我们来讨论这个问题。
一、业务分析
我们分析一下传统的开发流程,看看有哪些地方可以优化。
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 }
以上为数据添加及修改部分的优化实现,下一节讲解如何在查询列表中优化。