當前位置: 華文頭條 > 推薦

WPF --- 如何以Binding方式隱藏DataGrid列

2024-01-17推薦

引言

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