为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;
        }
    }
}

最后老规矩,上源码地址,在Controls下的Form文件夹中。

https://gitee.com/akwkevin/AI-wpf-controls

posted @ 2022-06-18 09:21  竹天笑  阅读(1568)  评论(0编辑  收藏  举报