引言
如題,如何以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
❞