Bind datagrid column visibility MVVM

jrb picture jrb · Oct 10, 2011 · Viewed 33.1k times · Source

.Net 3.5

I know that the columns doesn't inherit the datacontext and by reading other posts i thought this would work:

Visibility="{Binding RelativeSource={x:Static RelativeSource.Self},
                     Path=(FrameworkElement.DataContext).IsColumnNameVisible,
                     Converter={StaticResource boolToVisConverter}}"

However of course it doesn't.. The output window does not complain, it seems that the resource i found but the viewmodel property is newer called.

This is the entire DG :

<tk:DataGrid                                        
            VirtualizingStackPanel.IsVirtualizing="False"                                        
            Grid.Column="0"
            AlternationCount="2"
            AreRowDetailsFrozen="True"
            AutoGenerateColumns="False"
            Background="Transparent"
            BorderThickness="0"
            CanUserAddRows="False"
            CanUserReorderColumns="True"
            CanUserResizeRows="False"
            GridLinesVisibility="None"
            ItemsSource="{Binding Employees}"
            SelectionMode="Single"
            ColumnHeaderStyle="{StaticResource columnHeaderStyle}"
            RowHeaderStyle="{StaticResource rowHeaderStyle}"
            CellStyle="{StaticResource cellStyle}"
            RowStyle="{StaticResource rowStyle}" 
            ContextMenu="{StaticResource columnHeaderContextMenu}">
    <tk:DataGrid.Resources>
        <ContextMenu x:Key="columnHeaderContextMenu" ItemsSource="{Binding ColumnHeaderContextMenuItems}" />
        <Style TargetType="{x:Type ScrollBar}">
            <Setter Property="Background" Value="Transparent"/>
        </Style>                                    
        <Style TargetType="{x:Type tk:DataGridColumnHeader}">
            <Setter Property="Background" Value="Transparent"/>
        </Style>
    </tk:DataGrid.Resources>
    <tk:DataGrid.Triggers>
        <EventTrigger RoutedEvent="tk:DataGridRow.MouseDoubleClick">
            <EventTrigger.Actions>
                <BeginStoryboard Storyboard="{StaticResource showDetailGrid}"/>
            </EventTrigger.Actions>
        </EventTrigger>
    </tk:DataGrid.Triggers>
    <tk:DataGrid.Columns>
        <tk:DataGridTextColumn IsReadOnly="True" Header="test" Binding="{Binding Name, Mode=OneWay}" Visibility="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(FrameworkElement.DataContext).IsColumnNameVisible, Converter={StaticResource boolToVisConverter}}"  />
    </tk:DataGrid.Columns>
</tk:DataGrid>

I have read pretty much every single solution to this problem and nothing works..

Answer

WPF-it picture WPF-it · Oct 10, 2011

DataGridColumns are not part of visual tree so they are not connected to the data context of the DataGrid.

For them to connect together use proxy element approach like this...

  1. Add a proxy FrameworkElement in your ancestor panel's Resources.
  2. Host it into an invisible ContentControl bound to its Content.
  3. Use this ProxyElement as StaticResource for data context source in your visibility binding.

    <StackPanel>
        <StackPanel.Resources>
           <local:BooleanToVisibilityConverter
                  x:Key="BooleanToVisibilityConverter" />
    
           <FrameworkElement x:Key="ProxyElement"
                             DataContext="{Binding}"/>
        </StackPanel.Resources>
        <ContentControl Visibility="Collapsed"
                    Content="{StaticResource ProxyElement}"/>
        <DataGrid AutoGenerateColumns="False">
            <DataGrid.Columns>
                <DataGridTextColumn
                       Visibility="{Binding DataContext.IsTextColumnVisibile,
                                            Source={StaticResource ProxyElement},
                                            Converter={StaticResource
                                                BooleanToVisibilityConverter}}"
                       Binding="{Binding Text}"/>
            </DataGrid.Columns>
        </DataGrid>
    </StackPanel> 
    

Apart from DataGridColumn, the above approach also works great to connect DataContext to Popups and ContextMenus (i.e. any element that is not connected to the visual tree).

Silverlight Users

Sadly setting contents of content controls with any framework elements is not allowed in silverlight. So the workaround would be (this is just a guidance code for silverlight) ...

  1. Change the framework element resource to something lightweight like a Textblock. (Silverlight does not allow specifying static resource of FrameworkElement type.)

    <StackPanel.Resources>
        <TextBlock x:Key="MyTextBlock" />
    
  2. Write an attached property to hold text block against the content control.

    <ContentControl Visibility="Collapsed" 
                    local:MyAttachedBehavior.ProxyElement="{StaticResource MyTextBlock}" />
    
  3. In the attached dependency property changed event handler, set the bind the data context of the content control to the text block's.

     private static void OnProxyElementPropertyChanged(
         DependencyObject depObj, DependencyPropertyChangedEventArgs e)
     {
           if (depObj is ContentControl && e.NewValue is TextBlock)
           {
               var binding = new Binding("DataContext");
               binding.Source = depObj;
               binding.Mode = OneWay;
               BindingOperations.SetBinding(
                   (TextBlock)e.NewValue, TextBlock.DataContextProperty, binding);
           }
     }
    

So this way the textblock may not be connected to the visual tree but will probably be aware of the data context changes.

Hope this helps.