引言
如題,如何以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
轉換器,實現了預期功能。
如果大家有更優雅的方案,歡迎留言討論。
參考
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