搭建Wpf框架(15) ——敏捷开发crud界面的设计
AIStudio框架汇总及介绍
前言:即便有了代码生成器可以使用,我们还是需要很大一部分工作要做,然后我们就在想,能不能使用一个通用的View和一个通用的ViewModel,让新增一个crud的代码降到最低呢?答案是可以的。开发要做的就是定义类,剩下的东西完全围绕着这个类进行,一遍代码,通用执行。
第一步:界面设计如下:
1.查询条件,根据一个查询类反射生成一个集合,显示到界面上。
public ObservableCollection<QueryConditionItem> QueryConditionItems
{
get; private set;
} = new ObservableCollection<QueryConditionItem>();
2.查询结果,根据结果类反射生成DataGridColumns
public ObservableCollection<DataGridColumnCustom> DataGridColumns
{
get; private set;
} = new ObservableCollection<DataGridColumnCustom>();
3.编辑表单,根据结果类反射生成一个集合
public ObservableCollection<EditFormItem> EditFormItems
{
get; private set;
} = new ObservableCollection<EditFormItem>();
这三个部分都是动态生成的,因此只要类定义(查询类和结果类)好了,代码就可以自动生成。
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="auto"/>
<RowDefinition Height="auto"/>
</Grid.RowDefinitions>
<Grid Background="Transparent">
<ac:Form HeaderWidth="Auto" ItemMargin="3" HeaderMargin="0,0,3,0" ItemsSource="{Binding QueryConditionItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Control Style="{StaticResource QueryControlStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ac:Form>
</Grid>
<agc:FilterDataGrid x:Name="table"
Grid.Row="1"
ItemsSource="{Binding Data}"
SelectedItem="{Binding SelectedItem}"
LayoutName="{Binding Title}"
attribute:DataGridColumnsAttach.BindableColumns="{Binding DataGridColumns}"
AutoGenerateColumns="False">
</agc:FilterDataGrid>
<ac:Form Grid.Row="3" HeaderWidth="Auto" ItemMargin="3" HeaderMargin="0,0,3,0" ItemsSource="{Binding EditFormItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Control Style="{StaticResource EditControlStyle}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ac:Form>
</Grid>
这个一个View就可以对应任何的ViewModel.
第二步:类的设计,一般类的属性如下 public string UserName{get;set;},为了注入我们更多的信息,我们新建一个Attribute(标签属性)-ColumnHeaderAttribute
public class ColumnHeaderAttribute : DisplayNameAttribute
{
public ColumnHeaderAttribute()
{
}
public ColumnHeaderAttribute(string displayName) : base(displayName) { }
protected static IUserData _userData { get; }
static ColumnHeaderAttribute()
{
_userData = ContainerLocator.Current.Resolve<IUserData>();
}
public Visibility Visibility
{
get; set;
} = Visibility.Visible;
public int DisplayIndex
{
get; set;
} = int.MaxValue;
public string StringFormat
{
get; set;
}
public bool Ignore
{
get; set;
}
public Type Converter
{
get; set;
}
public string ItemSource
{
get; set;
}
public object ConverterParameter
{
get; set;
}
public string ForegroundExpression
{
get; set;
}
public string BackgroundExpression
{
get; set;
}
public bool IsPin
{
get; set;
}
public bool IsRequired
{
get; set;
}
public bool IsReadOnly
{
get; set;
}
public ControlType ControlType
{
get; set;
} = ControlType.None;
public HorizontalAlignment HorizontalAlignment
{
get; set;
}
public string SortMemberPath
{
get; set;
}
public bool CanUserSort
{
get; set;
} = true;
}
使用的时候,把我们个性化的属性加在属性上,如下:
public partial class Base_UserDTO : Prism.Mvvm.BindableBase, IIsChecked
{
private bool _isChecked;
[ColumnHeader(Ignore = true)]
public bool IsChecked
{
get { return _isChecked; }
set
{
SetProperty(ref _isChecked, value);
}
}
[ColumnHeader("Id", IsReadOnly = true)]
public string Id { get; set; }
[ColumnHeader(Ignore = true)]
public bool Deleted { get; set; }
private string _userName;
[Required(ErrorMessage = "用户名不能为空")]
[ColumnHeader("姓名")]
public string UserName
{
get
{
return _userName;
}
set
{
SetProperty(ref _userName, value);
}
}
private string _realName;
[ColumnHeader("真实姓名")]
public string RealName
{
get
{
return _realName;
}
set
{
SetProperty(ref _realName, value);
}
}
private string _password;
[ColumnHeader("密码", Visibility = System.Windows.Visibility.Collapsed)]
public string Password
{
get
{
return _password;
}
set
{
SetProperty(ref _password, value);
}
}
private int _sex;
[Required(ErrorMessage = "请选择性别")]
[ColumnHeader("性别", Converter = typeof(ObjectToStringConverter), ControlType = ControlType.ComboBox)]
public int Sex
{
get
{
return _sex;
}
set
{
SetProperty(ref _sex, value);
}
}
private DateTime? _birthday;
[Required(ErrorMessage = "请选择出生日期")]
[ColumnHeader("生日")]
public DateTime? Birthday
{
get
{
return _birthday;
}
set
{
SetProperty(ref _birthday, value);
}
}
private ObservableCollection<string> _roleIdList = new ObservableCollection<string>();
[NullOrEmptyValidation(ErrorMessage = "请选择角色")]
[ColumnHeader("角色", ItemSource = "Base_Role", ControlType = ControlType.MultiComboBox, Converter = typeof(ObjectToStringConverter))]
public ObservableCollection<string> RoleIdList
{
get
{
return _roleIdList;
}
set
{
SetProperty(ref _roleIdList, value);
}
}
private string _departmentId;
[NullOrEmptyValidation(ErrorMessage = "请选择部门")]
[ColumnHeader("部门", ItemSource = "Base_Department", ControlType = ControlType.TreeSelect, Converter = typeof(ObjectToStringConverter))]
public string DepartmentId
{
get
{
return _departmentId;
}
set
{
SetProperty(ref _departmentId, value);
}
}
private string _phoneNumber;
[PhoneValidation]
[ColumnHeader("手机号码")]
public string PhoneNumber
{
get
{
return _phoneNumber;
}
set
{
SetProperty(ref _phoneNumber, value);
}
}
[ColumnHeader("创建时间", IsReadOnly = true)]
public DateTime CreateTime { get; set; }
[ColumnHeader("修改时间", IsReadOnly = true)]
public DateTime? ModifyTime { get; set; }
[ColumnHeader(Ignore = true)]
public string CreatorId { get; set; }
[ColumnHeader("创建者", IsReadOnly = true)]
public string CreatorName { get; set; }
[ColumnHeader(Ignore = true)]
public string ModifyId { get; set; }
[ColumnHeader("修改者", IsReadOnly = true)]
public string ModifyName { get; set; }
public Base_UserDTO()
{
RoleIdList.CollectionChanged += RoleIdList_CollectionChanged;
}
private void RoleIdList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("RoleIdList");
}
}
第三步:QueryConditionItem和EditFormItem的设计,提取类的属性上所需要的信息,放到查询类和编辑类上,在点击查询或提交的时候,将List<>相关信息送给后台即可。
public abstract class BaseControlItem : BindableBase
{
public int DisplayIndex
{
get; set;
}
public object Header
{
get; set;
}
public string PropertyName
{
get; set;
}
private object _value;
public object Value
{
get
{
return _value;
}
set
{
SetProperty(ref _value, value);
}
}
private Visibility _visibility;
public Visibility Visibility
{
get
{
return _visibility;
}
set
{
SetProperty(ref _visibility, value);
}
}
public ControlType ControlType
{
get; set;
}
public object ItemSource
{
get; set;
}
public bool IsRequired
{
get; set;
}
public string StringFormat
{
get; set;
}
public bool IsReadOnly
{
get; set;
}
/// <summary>
/// 正则校验表达式
/// </summary>
public string Regex
{
get; set;
}
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage
{
get; set;
}
}
public class EditFormItem : BaseControlItem
{
}
public class QueryConditionItem : BaseControlItem
{
}
截个例子 其中,Header是姓名,ControlType=TextBox,生成了一个TextBox输入框,Value为输入的数据,PropertyName为对应类的属性名,点查询或提交的时候,把PropertyName和Value做为键值对送给后台即可。
另外还有ItemSource(如果ControlType=ComboBox时的ItemSource数据源),StringFormat格式化显示,IsRequired送给后台数据的必填项,Regex为开启输入数据的校验,ErrorMessage为对应的错误提示。
类的字段生成QueryConditionItem和EditFormItem的方法如下:
public static bool GetControlItem(PropertyInfo property, BaseControlItem baseControlItem)
{
string itemSource = property.Name;
var attribute = ColumnHeaderAttribute.GetPropertyAttribute(property);
if (attribute != null)
{
if (attribute.Ignore)
{
return false;
}
baseControlItem.Header = attribute.DisplayName ?? property.Name;
baseControlItem.ControlType = attribute.ControlType;
baseControlItem.IsRequired = attribute.IsRequired;
baseControlItem.StringFormat = attribute.StringFormat;
baseControlItem.DisplayIndex = attribute.DisplayIndex;
if (!string.IsNullOrEmpty(attribute.ItemSource))
{
itemSource = attribute.ItemSource;
}
}
else if (_userData.Base_Dictionary.ContainsKey(property.Name))
{
var dic = _userData.Base_Dictionary[property.Name];
baseControlItem.Header = dic.Text;
baseControlItem.ControlType = dic.ControlType;
baseControlItem.DisplayIndex = int.MaxValue;
if (!string.IsNullOrEmpty(dic.Code))
{
itemSource = dic.Code;
}
}
else
{
baseControlItem.Header = property.Name;
baseControlItem.DisplayIndex = int.MaxValue;
}
if (_userData.ItemSource.ContainsKey(itemSource))
{
//树形控件使用树形数据集
if (baseControlItem.ControlType == ControlType.TreeSelect || baseControlItem.ControlType == ControlType.MultiTreeSelect)
{
baseControlItem.ItemSource = _userData.ItemSource[$"{itemSource}Tree"];
}
else
{
baseControlItem.ItemSource = _userData.ItemSource[itemSource];
}
}
if (property.PropertyType == typeof(int) || property.PropertyType == typeof(int?))
{
if (string.IsNullOrEmpty(baseControlItem.StringFormat))
{
baseControlItem.StringFormat = "n0";
}
if (baseControlItem.ControlType == ControlType.None)
{
baseControlItem.ControlType = ControlType.IntegerUpDown;
}
}
else if (property.PropertyType == typeof(long) || property.PropertyType == typeof(long?))
{
if (string.IsNullOrEmpty(baseControlItem.StringFormat))
{
baseControlItem.StringFormat = "n0";
}
if (baseControlItem.ControlType == ControlType.None)
{
baseControlItem.ControlType = ControlType.LongUpDown;
}
}
else if (property.PropertyType == typeof(double) || property.PropertyType == typeof(double?))
{
if (string.IsNullOrEmpty(baseControlItem.StringFormat))
{
baseControlItem.StringFormat = "f3";
}
if (baseControlItem.ControlType == ControlType.None)
{
baseControlItem.ControlType = ControlType.DoubleUpDown;
}
}
else if (property.PropertyType == typeof(decimal) || property.PropertyType == typeof(decimal?))
{
if (string.IsNullOrEmpty(baseControlItem.StringFormat))
{
baseControlItem.StringFormat = "f3";
}
if (baseControlItem.ControlType == ControlType.None)
{
baseControlItem.ControlType = ControlType.DecimalUpDown;
}
}
else if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?))
{
if (string.IsNullOrEmpty(baseControlItem.StringFormat))
{
baseControlItem.StringFormat = "yyyy-MM-dd HH:mm:ss";
}
if (baseControlItem.ControlType == ControlType.None)
{
baseControlItem.ControlType = ControlType.DateTimeUpDown;
}
}
else
{
if (baseControlItem.ControlType == ControlType.None)
{
baseControlItem.ControlType = ControlType.TextBox;
}
}
baseControlItem.PropertyName = property.Name;
return true;
}
第四步:DataGrid的DataGridColumns的自动生成。因为DataGridColumns是不支持MVVM,因此我们自定义一个DataGridColumnCustom与DataGridColum对应起来,并新建一个附加属性,绑定DataGridColumnCustom集合,生成DataGridColum集合。
public class DataGridColumnsAttach
{
public static readonly DependencyProperty BindableColumnsProperty =
DependencyProperty.RegisterAttached(
"BindableColumns",
typeof(ObservableCollection<DataGridColumnCustom>),
typeof(DataGridColumnsAttach),
new UIPropertyMetadata(null, OnBindableColumnsChanged));
public static void SetBindableColumns(DependencyObject element, ObservableCollection<DataGridColumnCustom> value)
{
element.SetValue(BindableColumnsProperty, value);
}
public static ObservableCollection<DataGridColumnCustom> GetBindableColumns(DependencyObject element)
{
return (ObservableCollection<DataGridColumnCustom>)element.GetValue(BindableColumnsProperty);
}
private static void OnBindableColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is DataGrid dataGrid)
{
if (columns == null)
{
return;
}
var colums1 = columns.Where(p => p.DisplayIndex == int.MaxValue).ToList();
var colums2 = columns.Except(colums1).OrderBy(p => p.DisplayIndex).ToList();
foreach (DataGridColumnCustom column in colums1)
{
dataGrid.Columns.Add(GetDataColumn(column));
}
foreach (DataGridColumnCustom column in colums2)
{
dataGrid.Columns.Add(GetDataColumn(column));
}
columns.CollectionChanged += (sender, e2) => {
NotifyCollectionChangedEventArgs ne = e2;
if (ne.Action == NotifyCollectionChangedAction.Reset)
{
dataGrid.Columns.Clear();
foreach (DataGridColumnCustom column in ne.NewItems)
{
dataGrid.Columns.Add(GetDataColumn(column));
}
}
else if (ne.Action == NotifyCollectionChangedAction.Add)
{
foreach (DataGridColumnCustom column in ne.NewItems)
{
dataGrid.Columns.Add(GetDataColumn(column));
}
}
else if (ne.Action == NotifyCollectionChangedAction.Move)
{
dataGrid.Columns.Move(ne.OldStartingIndex, ne.NewStartingIndex);
}
else if (ne.Action == NotifyCollectionChangedAction.Remove)
{
foreach (DataGridColumnCustom column in ne.OldItems)
{
dataGrid.Columns.Remove(GetDataColumn(column));
}
}
else if (ne.Action == NotifyCollectionChangedAction.Replace)
{
dataGrid.Columns[ne.NewStartingIndex] = GetDataColumn(ne.NewItems[0] as DataGridColumnCustom);
}
};
}
}
private static DataGridColumn GetDataColumn(DataGridColumnCustom columnCustom)
{
var column = new DataGridTextColumn();
column.IsReadOnly = true;
column.Header = columnCustom.Header;
column.Binding = new Binding(columnCustom.Binding);
return column;
}
}
此处代码有简略,实际使用的DataGridTemplateColumn,实现了StringFormat,Converter,ConverterParameter,还有背景色和前景色触发。
根据类的属性生成DataGridColumnCustom的代码如下:
public static DataGridColumnCustom GetDataGridColumnCustom(PropertyInfo property)
{
DataGridColumnCustom dataGridColumnCustom = new DataGridColumnCustom();
dataGridColumnCustom.PropertyName = property.Name;
var attribute = ColumnHeaderAttribute.GetPropertyAttribute(property);
if (attribute != null)
{
if (attribute.Ignore)
{
return null;
}
dataGridColumnCustom.Header = attribute.DisplayName ?? property.Name;
dataGridColumnCustom.StringFormat = attribute.StringFormat;
dataGridColumnCustom.Visibility = attribute.Visibility;
if (attribute.Converter != null)
{
dataGridColumnCustom.Converter = attribute.Converter.FullName;
dataGridColumnCustom.ConverterParameter = attribute.ConverterParameter ?? attribute.ItemSource ?? property.Name;
}
dataGridColumnCustom.ForegroundExpression = attribute.ForegroundExpression;
dataGridColumnCustom.BackgroundExpression = attribute.BackgroundExpression;
dataGridColumnCustom.HorizontalAlignment = attribute.HorizontalAlignment;
dataGridColumnCustom.DisplayIndex = attribute.DisplayIndex;
dataGridColumnCustom.SortMemberPath = attribute.SortMemberPath ?? property.Name;
dataGridColumnCustom.CanUserSort = attribute.CanUserSort;
}
else if (_userData.Base_Dictionary.ContainsKey(property.Name))
{
var dic = _userData.Base_Dictionary[property.Name];
dataGridColumnCustom.Header = dic.Text;
dataGridColumnCustom.DisplayIndex = int.MaxValue;
dataGridColumnCustom.SortMemberPath = property.Name;
dataGridColumnCustom.CanUserSort = true;
dataGridColumnCustom.Converter = typeof(ObjectToStringConverter).FullName;
if (!string.IsNullOrEmpty(dic.Code))
{
dataGridColumnCustom.ConverterParameter = dic.Code;
}
else
{
dataGridColumnCustom.ConverterParameter = dic.Value;
}
}
else
{
dataGridColumnCustom.Header = property.Name;
dataGridColumnCustom.DisplayIndex = int.MaxValue;
dataGridColumnCustom.SortMemberPath = property.Name;
dataGridColumnCustom.CanUserSort = true;
}
if (property.PropertyType == typeof(int) || property.PropertyType == typeof(long) || property.PropertyType == typeof(int?) || property.PropertyType == typeof(long?))
{
if (string.IsNullOrEmpty(dataGridColumnCustom.StringFormat))
{
dataGridColumnCustom.StringFormat = "n0";
}
}
else if (property.PropertyType == typeof(double) || property.PropertyType == typeof(decimal) || property.PropertyType == typeof(double?) || property.PropertyType == typeof(decimal?))
{
if (string.IsNullOrEmpty(dataGridColumnCustom.StringFormat))
{
dataGridColumnCustom.StringFormat = "f3";
}
}
else if (property.PropertyType == typeof(DateTime) || property.PropertyType == typeof(DateTime?))
{
if (string.IsNullOrEmpty(dataGridColumnCustom.StringFormat))
{
dataGridColumnCustom.StringFormat = "yyyy-MM-dd HH:mm:ss";
}
}
return dataGridColumnCustom;
}
最后使用:(本文贴出来的代码很少,还有大量代码请下载后查看)
实现的地方,代码量很少:(换一个对象,只需要加三个如下修改以及定义好的类,即可。)
[View]
<UserControl x:Class="AIStudio.Wpf.Agile_Development.Views.Base_UserQueryView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:AIStudio.Wpf.Agile_Development.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<local:Common_QueryView/>
</Grid>
</UserControl>
[ViewModel]
namespace AIStudio.Wpf.Agile_Development.ViewModels
{
class Base_UserQueryViewModel: Common_QueryViewModel<Base_UserDTO, Base_UserDTO_Query>
{
public Base_UserQueryViewModel() : base("Base_Manage", typeof(Base_UserQueryEditViewModel))
{
}
}
}
[EditViewModel]
namespace AIStudio.Wpf.Agile_Development.ViewModels
{
class Base_UserQueryEditViewModel : Common_QueryEditViewModel<Base_UserDTO>
{
public Base_UserQueryEditViewModel(IEnumerable<EditFormItem> editFormItems, Base_UserDTO data, string area, string identifier, string title = "编辑表单") : base(editFormItems, data, area, identifier, title)
{
}
}
}
最后老规矩,上源码地址
https://gitee.com/akwkevin/aistudio.-wpf.-aclient 的AIStudio.Wpf.Agile_Development的ViewModels的Base_UserQueryViewModel.cs下