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

2023-11-22 06:00:26

引言

如題,如何以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中的實時視覺化樹:

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

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

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

視覺化樹(Visula Tree)

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

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

比如 Button

比如 DataGrid

又比如 ListBox

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

Visual 是 WPF 中視覺化物件模型的基礎,而 Visual 物件通過形成視覺化樹(Visual Tree)來組織所有視覺化模型。所以Visual Tree 是一個層次結構,包含了所有介面元素的視覺表示。所有繼承自 VisualUIElement(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 資源,給 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>

測試:

勾選後,顯示年齡列:

取消勾選後,隱藏年齡列

小結

本篇文章中,首先探索了 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