引言
如题,如何以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 =
)
{
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
转换器,实现了预期功能。
如果大家有更优雅的方案,欢迎留言讨论。
❝参考
stackoverflow - how to hide wpf datagrid columns depending on a propert?: https://stackoverflow.com/questions/6857780/how-to-hide-wpf-datagrid-columns-depending-on-a-property
Freezable Objects Overview:https://learn.microsoft.com/zh-cn/dotnet/desktop/wpf/advanced/freezable-objects-overview?view=netframeworkdesktop-4.8&wt.mc_id=MVP
❞