WPF自定义表格控件(动态添加/删除行)

最近在项目开发中遇到一个小问题,我们的设备管理模块中有一项叫做“技术参数”,具体来说就是不同的设备具有不同的属性,而且属性的数量也不同。举个例子,桌子有长、宽、高、材质四个属性,日光灯有安装高度、额定功率两个属性。我们希望根据设备类型能够自主添加/修改/删除属性,另一方面其他模块也会用到此功能,所以考虑做一个自定义控件,将增、删、改操作封装在控件内部,数据对外开放。


环境 版本
操作系统 Windows 10
编译器 Visual Studio 2015 update3

期望目标

期望达到的效果如下图所示:
WPF自定义表格控件(动态添加/删除行)_第1张图片

包含两列数据(属性名称和属性值),可以手动添加/删除行,同时支持编辑,对外提供一个数据集合(DataTable,Dictionary或List)。

创建控件并添加依赖项属性

在WPF项目里添加一个UserControl,命名为TableControl。我们希望这个控件的某个属性具有这样的特性:属性值发生变化时,控件的数据呈现立刻跟随变化;任意时刻访问控件的这个属性,都能保证属性值与呈现的数据保持一致。由此我们就需要一个自定义的依赖项属性,另外考虑到通用性,我们决定使用DataTable作为这个依赖项属性的数据类型。(创建自定义依赖项属性快捷键:输入propdp,按两次Tab键)。

    public partial class TableControl : UserControl
    {
        public TableControl()
        {
            InitializeComponent();
        }


        #region 自定义依赖项属性

        public DataTable DataSource
        {
            get { return (DataTable)GetValue(DataSourceProperty); }
            set { SetValue(DataSourceProperty, value); }
        }

        // Using a DependencyProperty as the backing store for DataSource.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty DataSourceProperty =
            DependencyProperty.Register("DataSource", typeof(DataTable), typeof(TableControl), new PropertyMetadata(new DataTable(), DataSourceChanged));

        private static void DataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TableControl control = d as TableControl;
            if (e.NewValue != e.OldValue)
            {
                DataTable dt = e.NewValue as DataTable;
            }
        }
        #endregion

    }

创建自定义依赖项属性DataSource后,又为其添加了一个属性改变事件DataSourceChanged,以确保属性值发生变化后能够进行相应的操作。

修改控件布局

WPF自带的DataGrid表格控件本身就支持增删改数据行,所以控件主体仍为DataGrid。要让DataGrid增加行,只需要将其CanUserAddRows属性设为true即可,但使用起来不那么方便,所以另外添加了一个按钮用来添加行。删除和编辑行均可以在DataGrid的模板列里实现,代码如下:

    <Grid>
        <StackPanel>
            <DataGrid HeadersVisibility="None" AutoGenerateColumns="False" CanUserAddRows="False" x:Name="dgData" GridLinesVisibility="Horizontal">

                <DataGrid.Columns>
                    <DataGridTemplateColumn Width="3*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock  Text="{Binding Path=ParamKey,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}">TextBlock>
                            DataTemplate>
                        DataGridTemplateColumn.CellTemplate>
                        <DataGridTemplateColumn.CellEditingTemplate>
                            <DataTemplate>
                                <TextBox  Text="{Binding Path=ParamKey,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,TargetNullValue=请输入}">TextBox>
                            DataTemplate>
                        DataGridTemplateColumn.CellEditingTemplate>
                    DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="10">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock Text=":">TextBlock>
                            DataTemplate>
                        DataGridTemplateColumn.CellTemplate>
                    DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="3*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <TextBlock  Text="{Binding Path=ParamValue,Mode=OneWay,UpdateSourceTrigger=PropertyChanged}">TextBlock>
                            DataTemplate>
                        DataGridTemplateColumn.CellTemplate>
                        <DataGridTemplateColumn.CellEditingTemplate>
                            <DataTemplate>
                                <TextBox  Text="{Binding Path=ParamValue,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,TargetNullValue=请输入}">TextBox>
                            DataTemplate>
                        DataGridTemplateColumn.CellEditingTemplate>
                    DataGridTemplateColumn>
                    <DataGridTemplateColumn Width="*">
                        <DataGridTemplateColumn.CellTemplate>
                            <DataTemplate>
                                <Button Click="btnDel_Click">
                                    <Button.Content>
                                        <Border Width="32" Height="32" CornerRadius="16" Background="CornflowerBlue" VerticalAlignment="Center" HorizontalAlignment="Center">
                                            <Path Data="M0 0L22 0" Stroke="WhiteSmoke" StrokeThickness="4" VerticalAlignment="Center" HorizontalAlignment="Center">Path>
                                        Border>
                                    Button.Content>
                                Button>
                            DataTemplate>
                        DataGridTemplateColumn.CellTemplate>
                    DataGridTemplateColumn>
                DataGrid.Columns>
            DataGrid>
            <Grid Width="35">
                <Button x:Name="btnAdd" Click="btnAdd_Click">
                    <Button.Content>
                        <Border Width="32" Height="32" CornerRadius="16" Background="CornflowerBlue" VerticalAlignment="Center" HorizontalAlignment="Center">
                            <Path Data="M0 11L22 11M11 0L11 22" Stroke="WhiteSmoke" StrokeThickness="4" VerticalAlignment="Center" HorizontalAlignment="Center">Path>
                        Border>
                    Button.Content>
                Button>
            Grid>
        StackPanel>    
    Grid>

数据处理

在数据处理上主要应用了WPF的双向绑定模式。控件初始化时就为DataGrid进行数据绑定。添加行时,创建一个与当前数据源具有相同结构的DataRow,将其追加到数据源上,重新进行数据绑定。由于采用了双向绑定,在页面上进行修改操作时,数据源也会随之发生变化,这样后台数据源和前台页面展示能始终保持一致,修改后的代码如下:

    public partial class TableControl : UserControl
    {
        private DataTable _dt = new DataTable();
        public TableControl()
        {
            InitializeComponent();
            _dt.Columns.Add(new DataColumn("ParamKey", typeof(string)));
            _dt.Columns.Add(new DataColumn("ParamValue", typeof(string)));
            this.dgData.ItemsSource = null;
            this.dgData.ItemsSource = _dt.DefaultView;
        }


        #region 自定义依赖项属性

        /// 
        /// 数据源
        /// 
        public DataTable DataSource
        {
            get { return ((DataView)this.dgData.ItemsSource).Table; }
            set { SetValue(DataSourceProperty, value); }
        }

        public static readonly DependencyProperty DataSourceProperty =
            DependencyProperty.Register("DataSource", typeof(DataTable), typeof(TableControl), new PropertyMetadata(new DataTable(), DataSourceChanged));


        private static void DataSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            TableControl control = d as TableControl;
            if (e.NewValue != e.OldValue)
            {
                DataTable dt = e.NewValue as DataTable;
                control._dt = dt;
                control.dgData.ItemsSource = null;
                control.dgData.ItemsSource = control._dt.DefaultView;
            }
        }

       #endregion

        /// 
        /// 删除行
        /// 
        /// 
        /// 
        private void btnDel_Click(object sender, RoutedEventArgs e)
        {
            ((DataRowView)this.dgData.SelectedItem).Row.Delete();
        }

        /// 
        /// 添加行
        /// 
        /// 
        /// 
        private void btnAdd_Click(object sender, RoutedEventArgs e)
        {
            _dt = ((DataView)this.dgData.ItemsSource).Table;
            DataRow dr = _dt.NewRow();
            _dt.Rows.Add(dr);
            this.dgData.ItemsSource = _dt.DefaultView;
        }

    }

改动主要有四处:
1.中间变量_dt
创建了一个DataTable类型的局部变量_dt作为中间变量,用于数据初始化和数据绑定:

private DataTable _dt = new DataTable();

_dt.Columns.Add(new DataColumn("ParamKey", typeof(string)));
_dt.Columns.Add(new DataColumn("ParamValue", typeof(string)));
this.dgData.ItemsSource = null;
this.dgData.ItemsSource = _dt.DefaultView;

2.DataSource的返回值
由于使用了双向绑定,我们想要的数据就在DataGrid的数据源里,将其返回即可:

get { return ((DataView)this.dgData.ItemsSource).Table; }

3.删除行

((DataRowView)this.dgData.SelectedItem).Row.Delete();

进行删除行操作时,这段代码获取选择的DataRowView对象,查找到相应的DataRow对象,并使用Delete()方法将其标识为即将删除。这时可以看到删除的DataRow对象从列表中消失了,但实际上它仍位于DataTable.Rows集合中。原因是DataView中的默认过滤设置隐藏了所有已删除的记录(只是将其标识为删除,但并未真正删除)。这也是官方推荐使用的方法。
另外一种方式就会导致选择的DataRowView对象被真正删除,代码如下,仅作参考,不建议使用:

_dt.Rows.Remove(((DataRowView)this.dgData.SelectedItem).Row);

4.新增行
获取DataGrid当前的数据源,创建一个具有相同结构的空行,追加到中间变量_dt,然后重新绑定。这样就保证了原有数据不丢失,同时又增加一个空行。

            _dt = ((DataView)this.dgData.ItemsSource).Table;
            DataRow dr = _dt.NewRow();
            _dt.Rows.Add(dr);
            this.dgData.ItemsSource = _dt.DefaultView;

其他

添加样式

简单的添加了样式,主要是确保行列对齐,看起来不那么丑

    
        <Style x:Key="ElementStyle" TargetType="FrameworkElement">
            <Setter Property="VerticalAlignment" Value="Center">Setter>
            <Setter Property="HorizontalAlignment" Value="Center">Setter>
        Style>
        <Style x:Key="TextBoxStyle" TargetType="TextBox" BasedOn="{StaticResource ElementStyle}">
            <Setter Property="BorderThickness" Value="1">Setter>
            <Setter Property="BorderBrush">
                <Setter.Value>
                    "#d6c79b">
                Setter.Value>
            Setter>
            <Setter Property="MinHeight" Value="24">Setter>
            <Setter Property="MinWidth" Value="100">Setter>
            <Setter Property="Margin" Value="5,2,5,2">Setter>
            <Setter Property="TextAlignment" Value="Center">Setter>
        Style>
        <Style x:Key="TextBlockStyle" TargetType="TextBlock" BasedOn="{StaticResource ElementStyle}">
            <Setter Property="MinHeight" Value="24">Setter>
            <Setter Property="MinWidth" Value="100">Setter>
            <Setter Property="TextAlignment" Value="Center">Setter>
        Style>
        <Style x:Key="ButtonStyle" TargetType="Button">
            <Setter Property="BorderThickness" Value="0">Setter>
            <Setter Property="Background" Value="Transparent">Setter>
        Style>
        <Style TargetType="DataGrid">
            <Setter Property="Background" Value="{x:Null}">Setter>
            <Setter Property="BorderThickness" Value="0">Setter>
            <Setter Property="HorizontalGridLinesBrush">
                <Setter.Value>
                    "#d6c79b">
                Setter.Value>
            Setter>
            <Setter Property="MinRowHeight" Value="32">Setter>
            <Setter Property="FontSize" Value="14">Setter>
        Style>
    

添加是否编辑状态的依赖项属性

为控件又添加了一个用于控制编辑状态的自定义依赖项属性IsEdit。非编辑状态下(IsEdit=false),隐藏新增和删除按钮,表格只读。编辑状态下(IsEdit=true),显示新增和删除按钮,表格可编辑。默认值为可编辑。

    public bool IsEdit
    {
        get { return (bool)GetValue(IsEditProperty); }
        set { SetValue(IsEditProperty, value); }
    }

    // Using a DependencyProperty as the backing store for IsEdit.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsEditProperty =
        DependencyProperty.Register("IsEdit", typeof(bool), typeof(TableControl), new PropertyMetadata(true, IsEditChanged));

    private static void IsEditChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        TableControl control = d as TableControl;
        bool isEdit = Convert.ToBoolean(e.NewValue);
        if (!isEdit)
        {
            int len = control.dgData.Columns.Count;
            control.dgData.Columns[len - 1].Visibility = Visibility.Collapsed;
            control.dgData.IsReadOnly = true;
            control.btnAdd.Visibility = Visibility.Collapsed;
        }
    }

完整代码下载点这里

总体上实现了预期的效果,但还有一些地方需要继续改进,欢迎广大朋友批评指导!

你可能感兴趣的:(WPF)