欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-04 11:13:14
...

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


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

期望目标

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

包含两列数据(属性名称和属性值),可以手动添加/删除行,同时支持编辑,对外提供一个数据集合(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 自定义依赖项属性

        /// <summary>
        /// 数据源
        /// </summary>
        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

        /// <summary>
        /// 删除行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnDel_Click(object sender, RoutedEventArgs e)
        {
            ((DataRowView)this.dgData.SelectedItem).Row.Delete();
        }

        /// <summary>
        /// 添加行
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        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;

其他

添加样式

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

    <UserControl.Resources>
        <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>
                    <SolidColorBrush Color="#d6c79b"></SolidColorBrush>
                </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>
                    <SolidColorBrush Color="#d6c79b"></SolidColorBrush>
                </Setter.Value>
            </Setter>
            <Setter Property="MinRowHeight" Value="32"></Setter>
            <Setter Property="FontSize" Value="14"></Setter>
        </Style>
    </UserControl.Resources>

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

为控件又添加了一个用于控制编辑状态的自定义依赖项属性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 控件