Bind datagrid column visibility MVVM
Asked Answered
A

1

51

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

Abecedarium answered 10/10, 2011 at 10:12 Comment(4)
If find your question a little unclear. Are you just trying to make a data column visible or invisible depending on a bound ViewModel property?Buhler
You change your ContextMenu in <tk:DataGrid.Resources> - no wonder that your window DataContext isn't reachable.Embroil
@ ChrisBD : Yes that is the idea. The VM prop is set via the datacontext.Abecedarium
@ Felix: What do you mean? Its a contextmenu.. why would that effect the datacontext availability?Abecedarium
U
110

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.

Uprear answered 10/10, 2011 at 10:40 Comment(14)
Got an exeption when deselect and reselect : Specified element is already the logical child of another element. Disconnect it first. Do u know why?Abecedarium
Could be your ContextMenu ... it can only be attached to one parent.Uprear
Changed the ContextMenu to DataGrid CM not DGColumnHeader CM but it didn't help. I use this on multiple columns if that matters..Abecedarium
Does this work for silverlight as well? FrameworkElement is in System.Windows, and I can't seem to import that as a namespace to the xaml file ...Lucilla
Instead of FrameworkElement use any derived type like TextBlock. :)Uprear
The TextBlock worked no problem, but I'm getting an error at runtime, "Failed to assign to property 'System.Windows.Controls.ContentControl.Content'." Here's the relevant code: <Grid>... <Grid.Resources> <TextBlock x:Key="ProxyElement" DataContext="{Binding}"/> </Grid.Resources> <ContentControl Visibility="Collapsed" Content="{StaticResource ProxyElement}"/> <f:ScrollDataGrid> ... <sdk:DataGridTemplateColumn Header="VIN" SortMemberPath="VIN" Visibility="{Binding DataContext.BasicsVisibility, Source= {StaticResource ProxyElement}}"> Any ideas?Lucilla
@John, please see my edited reply (I havent tested it though)Uprear
@AngelWPF thanks for looking into that for me! I used this method as total workaround for now (mockable.blogspot.com/2010/10/…) but I'd prefer not having to extend base classes for simple functionality so I'll go back and try out yours once I can.Lucilla
For some reason this only works for the Visibility column, although I don't understand why. For example, binding "Width" has no effect at all. Binding "DisplayIndex" throws an exception, telling me -1 is out of range - even though its correct in the binding.Duvalier
@WPF-it can you explain plz why ContentControl is required here if we use ProxyElement anyway? This solution uses the similiar approach, but no ContentControl is required.Heterogamete
@monstr, Freezable classes hack the DataContext ... in our case 'proxyelement' is not Freezable, so the only way it can acquire a DataContext if it is hosted within the VisualTree which is possible if it is a Content of a ContentControl. Also for the second solution in your link, the NameScoping is not resolved in Template like DataGridColumnHeaderTemplate so the ElementName wouldn't work. The solution I have provided takes care of both situations.Uprear
Hi WPF-it, your approach works perfectly fine and I am able to hide/show column MVVM way. However when I hide column, an extra blank column is shown at the end. I have AutoGenerateColumns set to False & HeaderVisibility set to Column. All column widths are set to Auto (* doesn't help). Do you have any solution/inputs for the extra column? ThanksArquebus
I made the mistake to think that DataBindings of Visibility (at DataGridColumn) be a subset of items which bound as ItemsSource in the DataGrid. <DataGrid ItemsSource="{Binding... has nothing to do with the binding of Visibility in DataGridColumn. The binding of Visibility in DataGridColumn, is bound to the DataContext (indicated f.e. by a ProxyBinding)Puppet
Thank you a thousand times, this works! I spent good part of the day on this. I hate anyone who created WPF and introduced this kind of bugs practically unsolvable without Google and SO. Once again I have to state that WPF as a technology is only good for writing tutorials, I can't imagine that anyone use it for production apps voluntarily.Schlicher

© 2022 - 2024 — McMap. All rights reserved.