为Wpf敏捷开发做准备-Wpf实现Form表单1
AIStudio框架汇总及介绍
前言:Form表单是比较常见的一种布局,Wpf一般使用Gird进行布局,但是代码会显得比较多比较乱,参照vue的Form表单,代码就比较简洁。
比如实现如图的编辑模板,您会想到用什么布局呢?
现在开始,我们来实现一个Wpf的Form表单,实现效果图如下:
第一步:实现Form的子元素FormItem,有个标头和内容项,使用HeaderedContentControl最合适不过了,代码如下:
public class FormItem : HeaderedContentControl
{
static FormItem()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(FormItem), new FrameworkPropertyMetadata(typeof(FormItem), FrameworkPropertyMetadataOptions.Inherits));
}
}
然后再拷贝一个HeaderedContentControl的样式,然后改写成自己的样式风格。
<Style x:Key="AIStudio.Styles.FormItem" TargetType="{x:Type controls:FormItem}">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource MahApps.Brushes.Text}"/>
<Setter Property="MinHeight" Value="{DynamicResource DefaultControlHeight}" />
<Setter Property="Padding" Value="{Binding RelativeSource={RelativeSource Mode=Self},Path=(controls:Form.ItemMargin)}"/>
<Setter Property="HorizontalContentAlignment" Value="Right"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:FormItem}">
<Border Background="{TemplateBinding Background}" Padding="{TemplateBinding Padding}" UseLayoutRounding="False">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=(controls:Form.HeaderWidth)}" />
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=(controls:Form.BodyWidth)}" />
</Grid.ColumnDefinitions>
<ContentPresenter x:Name="PART_Header"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Header"
TextElement.Foreground="{TemplateBinding Foreground}"
Margin="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=(controls:Form.HeaderMargin)}"/>
<ContentPresenter
x:Name="PART_ContentPresenter"
Grid.Column="1"
HorizontalAlignment="Stretch"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentSource="Content"
Margin="{Binding RelativeSource={RelativeSource Mode=TemplatedParent},Path=(controls:Form.BodyMargin)}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
具体使用也和HeaderedContentControl一样
<ac:FormItem Header="标题长度">
<TextBox Margin="2" MinWidth="100" Style="{DynamicResource AIStudio.Styles.TextBox}"/>
</ac:FormItem>
第二步:实现Form,集成ItemsControl即可,但是我们要实现选中项,改成继承Selector。
public class Form : Selector
{
}
另外为了控制子元素的列头宽度和内容宽度,子项之间的间距,添加附加属性,这样只改变Form就能影响FormItem,比挨个设置FormItem方便。
#region AttachedProperty : HeaderWidthProperty
public static readonly DependencyProperty HeaderWidthProperty
= DependencyProperty.RegisterAttached("HeaderWidth", typeof(GridLength), typeof(Form), new FrameworkPropertyMetadata(new GridLength(80d, GridUnitType.Pixel), FrameworkPropertyMetadataOptions.Inherits));
public static GridLength GetHeaderWidth(DependencyObject element) => (GridLength)element.GetValue(HeaderWidthProperty);
public static void SetHeaderWidth(DependencyObject element, GridLength value) => element.SetValue(HeaderWidthProperty, value);
#endregion
#region AttachedProperty : BodyWidthProperty
public static readonly DependencyProperty BodyWidthProperty
= DependencyProperty.RegisterAttached("BodyWidth", typeof(GridLength), typeof(Form), new FrameworkPropertyMetadata(new GridLength(1, GridUnitType.Star), FrameworkPropertyMetadataOptions.Inherits));
public static GridLength GetBodyWidth(DependencyObject element) => (GridLength)element.GetValue(BodyWidthProperty);
public static void SetBodyWidth(DependencyObject element, GridLength value) => element.SetValue(BodyWidthProperty, value);
#endregion
#region AttachedProperty : OrientationProperty
public static readonly DependencyProperty OrientationProperty
= DependencyProperty.RegisterAttached("Orientation", typeof(Orientation), typeof(Form), new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.Inherits));
public static Orientation GetOrientation(DependencyObject element) => (Orientation)element.GetValue(OrientationProperty);
public static void SetOrientation(DependencyObject element, Orientation value) => element.SetValue(OrientationProperty, value);
#endregion
#region AttachedProperty: ItemMarginProperty
public static readonly DependencyProperty ItemMarginProperty
= DependencyProperty.RegisterAttached("ItemMargin", typeof(Thickness), typeof(Form), new FrameworkPropertyMetadata(new Thickness(3), FrameworkPropertyMetadataOptions.Inherits));
public static Thickness GetItemMargin(DependencyObject element) => (Thickness)element.GetValue(ItemMarginProperty);
public static void SetItemMargin(DependencyObject element, Thickness value) => element.SetValue(ItemMarginProperty, value);
#endregion
#region AttachedProperty : HeaderMarginProperty
public static readonly DependencyProperty HeaderMarginProperty
= DependencyProperty.RegisterAttached("HeaderMargin", typeof(Thickness), typeof(Form), new FrameworkPropertyMetadata(new Thickness(0, 0, 3, 0), FrameworkPropertyMetadataOptions.Inherits));
public static Thickness GetHeaderMargin(DependencyObject element) => (Thickness)element.GetValue(HeaderMarginProperty);
public static void SetHeaderMargin(DependencyObject element, Thickness value) => element.SetValue(HeaderMarginProperty, value);
#endregion
#region AttachedProperty : BodyMarginProperty
public static readonly DependencyProperty BodyMarginProperty
= DependencyProperty.RegisterAttached("BodyMargin", typeof(Thickness), typeof(Form), new FrameworkPropertyMetadata(default(Thickness), FrameworkPropertyMetadataOptions.Inherits));
public static Thickness GetBodyMargin(DependencyObject element) => (Thickness)element.GetValue(BodyMarginProperty);
public static void SetBodyMargin(DependencyObject element, Thickness value) => element.SetValue(BodyMarginProperty, value);
#endregion
(在上面的FormItem的样式里面,用到了如上的附加属性) 另外注意:FrameworkPropertyMetadataOptions.Inherits。这样很关键,表示子元素都继承这个属性,设置在Form上,FormItem也生效。
第三步:如何实现Form的快速布局切换呢?大家都知道ItemsControl改变布局的属性是ItemsPanelTemplate,实现是StackPanel还是WrapPanel,或UniformGrid,都可以设置,如:
<ac:Form.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ac:Form.ItemsPanel>
但是这样还是不方便,设置属性才是最方便的,添加依赖性属性PanelType,设置类型,就实现布局切换。
public static readonly DependencyProperty PanelTypeProperty =
DependencyProperty.Register("PanelType", typeof(FormPanelType), typeof(Form), new PropertyMetadata(FormPanelType.StackPanel, OnPanelTypeChanged));
public FormPanelType PanelType
{
get
{
return (FormPanelType)GetValue(PanelTypeProperty);
}
set
{
SetValue(PanelTypeProperty, value);
}
}
private static void OnPanelTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is Form form)
{
ItemsPanelTemplate panel = new ItemsPanelTemplate();
if ((FormPanelType)e.NewValue == FormPanelType.StackPanel)
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(StackPanel));
factory.SetValue(StackPanel.OrientationProperty, Orientation.Vertical);
panel.VisualTree = factory;
}
else if ((FormPanelType)e.NewValue == FormPanelType.WrapPanel)
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(WrapPanel));
panel.VisualTree = factory;
}
else if ((FormPanelType)e.NewValue == FormPanelType.UniformWrapPanel)
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(UniformWrapPanel));
factory.SetValue(UniformWrapPanel.ColumnsProperty, form.PanelColumns);
panel.VisualTree = factory;
}
else if ((FormPanelType)e.NewValue == FormPanelType.UniformGrid)
{
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(UniformGridEx));
factory.SetValue(UniformGridEx.ColumnsProperty, form.PanelColumns);
factory.SetValue(UniformGridEx.VerticalAlignmentProperty, VerticalAlignment.Top);
panel.VisualTree = factory;
}
form.ItemsPanel = panel;
}
}
其中UniformWrapPanel和UniformGridEx是我实现的(具体代码就不贴出了,最后在源码地址大家下载查看),可以设置属性表示占据一个元素位置,还是占据两个,如部门的跨度设置为2时,占两个位置。
第四步:实现拖拽,可以实现用户自定义布局,只贴出了关键代码。
private void Form_Drop(object sender, DragEventArgs e)
{
if (IsReadOnly)
return;
var pos = e.GetPosition(this);
var result = VisualTreeHelper.HitTest(this, pos);
if (result == null)
{
return;
}
//查找元数据
var sourceItem = (e.Data.GetData(typeof(FormItem)) ?? e.Data.GetData(typeof(FormCodeItem))) as FormItem;
if (sourceItem == null)
{
return;
}
//查找目标数据
var targetItem = VisualHelper.FindParent<FormItem>(result.VisualHit);
if (sourceItem == targetItem)
{
return;
}
if (targetItem == null)
{
AddItem(sourceItem);
}
else if (sourceItem.ParentForm != this)
{
InsertItem(sourceItem, targetItem);
}
else
{
ChangedItem(sourceItem, targetItem);
}
this.Items.Refresh();
}
里面还实现了左键按下防抖,按住一定时间才进行拖动,代码太多,请大家下载查看。
如何使用
<ac:Form x:Name="form"
VerticalAlignment="Top"
DataContext="{Binding Base_User}"
Margin="2">
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='UserName'}">
<TextBox Text="{Binding UserName,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.TextBox.Underline}"/>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='Password'}">
<PasswordBox ac:PasswordBoxBindingBehavior.Password="{Binding Password,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.PasswordBox.Underline}"/>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='Sex'}">
<StackPanel Orientation="Horizontal">
<RadioButton Content="男" IsChecked="{Binding Sex,Converter={ac:ConverterValueMapToBool Parameter=0},ConverterParameter=1}" Style="{DynamicResource AIStudio.Styles.RadioButton}"/>
<RadioButton Content="女" IsChecked="{Binding Sex,Converter={ac:ConverterValueMapToBool Parameter=1},ConverterParameter=0}" Style="{DynamicResource AIStudio.Styles.RadioButton}"/>
</StackPanel>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='Birthday'}">
<DatePicker SelectedDate="{Binding Birthday,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.DatePicker.Underline}"/>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='DepartmentId'}" ac:UniformGridEx.Span="2">
<ac:TreeSelect SelectedValue="{Binding DepartmentId,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ItemsSource="{ac:ControlBinding Departments}"
DisplayMemberPath="Text"
SelectedValuePath="Value"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.TreeSelect.Underline}">
<ac:TreeSelect.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Text}" VerticalAlignment="Center" HorizontalAlignment="Left"/>
</StackPanel>
</HierarchicalDataTemplate>
</ac:TreeSelect.ItemTemplate>
</ac:TreeSelect>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='RoleIdList'}">
<ac:MultiComboBox
ac:CustomeSelectionValues.SelectedValues="{Binding RoleIdList,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ItemsSource="{ac:ControlBinding RolesList}"
DisplayMemberPath="Text"
SelectedValuePath="Value"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.MultiComboBox.Underline}"/>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='SelectedDuty'}">
<ComboBox SelectedValue="{Binding SelectedDuty,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ItemsSource="{ac:ControlBinding Duties}"
DisplayMemberPath="Text"
SelectedValuePath="Value"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.ComboBox.Underline}"/>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='Email'}">
<TextBox Text="{Binding Email,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.TextBox.Underline}"/>
</ac:FormItem>
<ac:FormItem Header="{Binding .,Converter={StaticResource DisplayNameConverter},ConverterParameter='PhoneNumber'}">
<TextBox Text="{Binding PhoneNumber,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True, ValidatesOnDataErrors=True, NotifyOnValidationError=True}"
ac:ControlAttach.ClearTextButton="True"
Style="{DynamicResource AIStudio.Styles.TextBox.Underline}"/>
</ac:FormItem>
<ac:FormItem>
<Button Content="提交" Command="{ac:ControlBinding SubmitCommand}" CommandParameter="{Binding .}"
Style="{DynamicResource AIStudio.Styles.Button}"/>
</ac:FormItem>
</ac:Form>
对应的Base_User类如下:
public partial class Base_User : BindableBase
{
private bool _isChecked;
public bool IsChecked
{
get
{
return _isChecked;
}
set
{
SetProperty(ref _isChecked, value);
}
}
public string Id
{
get; set;
}
public bool Deleted
{
get; set;
}
private string _userName;
[Required(ErrorMessage = "用户名不能为空")]
[DisplayName("姓名")]
public string UserName
{
get
{
return _userName;
}
set
{
SetProperty(ref _userName, value);
}
}
private string _realName;
[DisplayName("真实姓名")]
public string RealName
{
get
{
return _realName;
}
set
{
SetProperty(ref _realName, value);
}
}
private string _password;
[Required(ErrorMessage = "密码不能为空")]
[DisplayName("密码")]
public string Password
{
get
{
return _password;
}
set
{
SetProperty(ref _password, value);
}
}
private int _sex;
[Required(ErrorMessage = "请选择性别")]
[DisplayName("性别")]
public int Sex
{
get
{
return _sex;
}
set
{
SetProperty(ref _sex, value);
}
}
private DateTime? _birthday;
[Required(ErrorMessage = "请选择出生日期")]
[DisplayName("生日")]
public DateTime? Birthday
{
get
{
return _birthday;
}
set
{
SetProperty(ref _birthday, value);
}
}
private ObservableCollection<string> _roleIdList = new ObservableCollection<string>();
[NullOrEmptyValidation(ErrorMessage = "请选择角色")]
[DisplayName("角色")]
public ObservableCollection<string> RoleIdList
{
get
{
return _roleIdList;
}
set
{
SetProperty(ref _roleIdList, value);
}
}
private string _departmentId;
[NullOrEmptyValidation(ErrorMessage = "请选择部门")]
[DisplayName("部门")]
public string DepartmentId
{
get
{
return _departmentId;
}
set
{
SetProperty(ref _departmentId, value);
}
}
private string _selectedDuty;
[Required(ErrorMessage = "请选择职能")]
[DisplayName("职能")]
public string SelectedDuty
{
get
{
return _selectedDuty;
}
set
{
SetProperty(ref _selectedDuty, value);
}
}
private string _email;
[EmailValidation(ErrorMessage = "请输入正确的邮箱格式,例:zhangsan@126.com")]
[DisplayName("邮箱")]
public string Email
{
get
{
return _email;
}
set
{
SetProperty(ref _email, value);
}
}
private string _phoneNumber;
[PhoneValidation]
[DisplayName("手机号码")]
public string PhoneNumber
{
get
{
return _phoneNumber;
}
set
{
SetProperty(ref _phoneNumber, value);
}
}
[DisplayName("创建时间")]
public DateTime CreateTime
{
get; set;
}
[DisplayName("修改时间")]
public DateTime? ModifyTime
{
get; set;
}
public string CreatorId
{
get; set;
}
[DisplayName("创建者")]
public string CreatorName
{
get; set;
}
public string ModifyId
{
get; set;
}
[DisplayName("修改者")]
public string ModifyName
{
get; set;
}
public Base_User()
{
RoleIdList.CollectionChanged += RoleIdList_CollectionChanged;
}
private void RoleIdList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
RaisePropertyChanged("RoleIdList");
}
}
public partial class Base_User : IDataErrorInfo
{
public string this[string columnName]
{
get
{
List<ValidationResult> validationResults = new List<ValidationResult>();
bool result = Validator.TryValidateProperty(
GetType().GetProperty(columnName).GetValue(this),
new ValidationContext(this)
{
MemberName = columnName
},
validationResults);
if (result)
return null;
return validationResults.First().ErrorMessage;
}
}
public string Error
{
get
{
ICollection<ValidationResult> results;
this.Validate(out results);
return results.FirstOrDefault()?.ErrorMessage;
}
}
}