WPF自定义表格控件(动态添加/删除行)
最近在项目开发中遇到一个小问题,我们的设备管理模块中有一项叫做“技术参数”,具体来说就是不同的设备具有不同的属性,而且属性的数量也不同。举个例子,桌子有长、宽、高、材质四个属性,日光灯有安装高度、额定功率两个属性。我们希望根据设备类型能够自主添加/修改/删除属性,另一方面其他模块也会用到此功能,所以考虑做一个自定义控件,将增、删、改操作封装在控件内部,数据对外开放。
环境 | 版本 |
---|---|
操作系统 | Windows 10 |
编译器 | Visual Studio 2015 update3 |
期望目标
期望达到的效果如下图所示:
包含两列数据(属性名称和属性值),可以手动添加/删除行,同时支持编辑,对外提供一个数据集合(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;
}
}
总体上实现了预期的效果,但还有一些地方需要继续改进,欢迎广大朋友批评指导!