引言
如题,如何以Binding的方式动态隐藏DataGrid列?
预想方案
像这样:
先在ViewModel创建数据源 People
和控制列隐藏的 IsVisibility
,这里直接以 MainWindow
为 DataContext
public partial class MainWindow : Window, INotifyPropertyChanged
{
public MainWindow()
{
InitializeComponent();
Persons = new ObservableCollection<Person>() { new Person() { Age = 11, Name = "Peter" }, new Person() { Age = 19, Name = "Jack" } };
DataContext = this;
}
public event PropertyChangedEventHandler? PropertyChanged;
public void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private bool isVisibility;
public bool IsVisibility
{
get => isVisibility;
set
{
isVisibility = value;
OnPropertyChanged(nameof(IsVisibility));
}
}
private ObservableCollection<Person> persons;
public ObservableCollection<Person> Persons
{
get { return persons; }
set { persons = value; OnPropertyChanged(); }
}
}
然后创建 VisibilityConverter
,将布尔值转化为 Visibility
。
public class VisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool isVisible && isVisible)
{
return Visibility.Visible;
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
然后再界面绑定 IsVisibility
,且使用转化器转化为Visibility
,最后增加一个 CheckBox
控制是否隐藏列。
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<DataGrid
x:Name="dataGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
ItemsSource="{Binding Persons}"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn
Header="年龄"
Width="*"
Binding="{Binding Age}"
Visibility="{Binding DataContext.IsVisibility, RelativeSource={RelativeSource Mode=FindAncestor, AncestorLevel=1, AncestorType={x:Type Window}}, Converter={StaticResource VisibilityConverter}}" />
<DataGridTextColumn Header="姓名" Width="*" Binding="{Binding Name}" />
</DataGrid.Columns>
</DataGrid>
<CheckBox
Grid.Column="1"
Content="是否显示年龄列"
IsChecked="{Binding IsVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Grid>
这样应该没问题,Visibility
是依赖属性,能直接通过 Binding 的方式赋值。
但实际测试时就会发现,勾选 CheckBox
能够改变 DataContext.IsVisibility
的值,但是无法触发转换器 VisibilityConverter
,即使不用 RelativeSource
方式,更改为指定 ElementName
获取元素的方式,也一样不生效。
这是为什么呢?
我疑惑了很久,直到看到了Visual Studio中的实时可视化树:
从图中可以看出,虽然我在 Xaml 中声明了两列 DataGridTextColumn
,但他根本不在可视化树中。
获取 RelativeSource
和指定 ElementName
的方式,本质上还是在可视化树中寻找元素,所以上述方案无法生效。
那为什么 DataGridTextColumn
不在可视化树中呢?
可视化树(Visula Tree)
在上面那个问题之前,先看看什么是可视化树?
我们先从微软文档来看一下WPF中其他控件的继承树。
比如 Button
比如 DataGrid
:
又比如 ListBox
:
大家可以去看看其他的控件,几乎 WPF 中所有的控件都继承自 Visual
(例如,Panel
、Window
、Button
等都是由 Visual
对象构建而成)。
Visual
是 WPF 中可视化对象模型的基础,而 Visual
对象通过形成可视化树(Visual Tree
)来组织所有可视化模型。所以Visual Tree
是一个层次结构,包含了所有界面元素的视觉表示。所有继承自 Visual
或 UIElement
(UI 元素的更高级别抽象)的对象都存在于可视化树中。
但是,DataGridColumn
是一个特例,它不继承 Visual
,它直接继承 DependencyObject
,如下:
所以,DataGridColumn
的继承树就解答了他为什么不在可视化树中。
解决方案
所以,通过直接找 DataContext
的方式,是不可行的,那就曲线救国。
既然无法找到承载 DataContext.IsVisibility
的对象,那就创建一个能够承载的对象。首先该对象必须是 DependencyObject
类型或其子类,这样才能使用依赖属性在 Xaml
进行绑定,其次必须有属性变化通知功能,这样才能触发 VisibilityConverter
,实现预期功能。
这时候就需要借助一个抽象类 System.Windows.Freezable
。摘取部分官方解释如下:
从文档中可以看出 Freezable
非常符合我们想要的,第一它本身继承 DependencyObject
且 它在子属性值更改时能够提供变化通知。
所以我们可以创建一个自定义 Freezable
类,实现我们的功能,如下:
public class CustomFreezable : Freezable
{
public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(object), typeof(CustomFreezable));
public object Value
{
get => (object)GetValue(ValueProperty);
set => SetValue(ValueProperty, value);
}
protected override void OnChanged()
{
base.OnChanged();
}
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
}
protected override Freezable CreateInstanceCore()
{
return new CustomFreezable();
}
}
然后在 Xaml
添加 customFreezable
资源,给 DataGridTextColumn
的 Visibility
绑定资源
<Window.Resources>
<local:VisibilityConverter x:Key="VisibilityConverter" />
<local:CustomFreezable x:Key="customFreezable" Value="{Binding IsVisibility, Converter={StaticResource VisibilityConverter}}" />
</Window.Resources>
<Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<DataGrid
x:Name="dataGrid"
AutoGenerateColumns="False"
CanUserAddRows="False"
ItemsSource="{Binding Persons}"
SelectionMode="Single">
<DataGrid.Columns>
<DataGridTextColumn
x:Name="personName"
Width="*"
Binding="{Binding Age}"
Header="年龄"
Visibility="{Binding Value, Source={StaticResource customFreezable}}" />
<DataGridTextColumn
Width="*"
Binding="{Binding Name}"
Header="姓名" />
</DataGrid.Columns>
</DataGrid>
<CheckBox
Grid.Column="1"
Content="是否显示年龄列"
IsChecked="{Binding IsVisibility, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Grid>
测试:
勾选后,显示年龄列:
取消勾选后,隐藏年龄列
小结
本篇文章中,首先探索了 DataGridTextColumn
为什么不在可视化树结构内,是因为所有继承自 Visual
或 UIElement
(UI 元素的更高级别抽象)的对象才存在于可视化树中。,DataGridTextColumn
是直接继承DependencyObject
,所以才不在可视化树结构内。
其次探索如何通过曲线救国,实现以 Binding
的方式实现隐藏DataGridTextColumn
,我们借助了一个核心抽象类 System.Windows.Freezable
。该抽象类是 DependencyObject
的子类,能使用依赖属性在 Xaml
进行绑定,且有属性变化通知功能,触发 VisibilityConverter
转换器,实现了预期功能。
如果大家有更优雅的方案,欢迎留言讨论。