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

NiueryDiary發表於2023-11-21

引言

如題,如何以Binding的方式動態隱藏DataGrid列?

預想方案

像這樣:

先在ViewModel建立資料來源 People 和控制列隱藏的 IsVisibility,這裡直接以 MainWindowDataContext

 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中的實時視覺化樹:

image.png

從圖中可以看出,雖然我在 Xaml 中宣告瞭兩列 DataGridTextColumn,但他根本不在視覺化樹中。

獲取 RelativeSource 和指定 ElementName 的方式,本質上還是在視覺化樹中尋找元素,所以上述方案無法生效。

那為什麼 DataGridTextColumn 不在視覺化樹中呢?

視覺化樹(Visula Tree)

在上面那個問題之前,先看看什麼是視覺化樹?

我們先從微軟文件來看一下WPF中其他控制元件的繼承樹。

比如 Button

image.png

比如 DataGrid

image.png

又比如 ListBox

image.png

大家可以去看看其他的控制元件,幾乎 WPF 中所有的控制元件都繼承自 Visual(例如,PanelWindowButton 等都是由 Visual 物件構建而成)。

Visual 是 WPF 中視覺化物件模型的基礎,而 Visual 物件透過形成視覺化樹(Visual Tree)來組織所有視覺化模型。所以Visual Tree 是一個層次結構,包含了所有介面元素的視覺表示。所有繼承自 VisualUIElement(UI 元素的更高階別抽象)的物件都存在於視覺化樹中。

但是,DataGridColumn 是一個特例,它不繼承 Visual,它直接繼承 DependencyObject,如下:

image.png

所以,DataGridColumn的繼承樹就解答了他為什麼不在視覺化樹中。

解決方案

所以,透過直接找 DataContext 的方式,是不可行的,那就曲線救國。

既然無法找到承載 DataContext.IsVisibility 的物件,那就建立一個能夠承載的物件。首先該物件必須是 DependencyObject 型別或其子類,這樣才能使用依賴屬性在 Xaml 進行繫結,其次必須有屬性變化通知功能,這樣才能觸發 VisibilityConverter,實現預期功能。

這時候就需要藉助一個抽象類 System.Windows.Freezable。摘取部分官方解釋如下:

image.png
image.png

從文件中可以看出 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 資源,給 DataGridTextColumnVisibility 繫結資源

<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>

測試:

勾選後,顯示年齡列:
image.png

取消勾選後,隱藏年齡列
image.png

小結

本篇文章中,首先探索了 DataGridTextColumn 為什麼不在視覺化樹結構內,是因為所有繼承自 VisualUIElement(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

相關文章