Search code examples
c#wpfbindingdatagridfiltering

WPF: how to bind to a dictionary item from ContentTemplate of DatagridHeader


I'm trying to implement a filtering popup with combobox on a datagrid header. I'm trying to bind the itemsource of the combobox inside the popup to the content of the related column.

To do this I'm populating a dictionary of observable collections with the property name of the data collected in the grid as key, and up to here everything works properly.

Then I could write the property name that I want to use as key in a textblock inside the popup with the binding and I can write the combobox content if I explicitly use the property name as index of the dictionary in the binding (see first <LisBox> element commented declaration).

But when I'm trying to bind the index to the property name I got binding errors.

I hope the code makes clearer what I'm trying to do:

WPF:

<Window x:Class="Prove_varie.GridWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Prove_varie"
        mc:Ignorable="d"
        Title="GridWindow"
        Height="450"
        Width="800"
        x:Name="myWindow">
    <Grid>
        <DataGrid IsReadOnly="True"
                  ItemsSource="{Binding Path=Persone}">
            <DataGrid.Resources>
                <Style TargetType="{x:Type DataGridColumnHeader}">
                    <Setter Property="ContentTemplate">
                        <Setter.Value>
                            <DataTemplate>
                                <StackPanel Orientation="Horizontal">
                                    <TextBlock Text="{Binding}"
                                               TextWrapping="Wrap"
                                               HorizontalAlignment="Center"
                                               VerticalAlignment="Center" />
                                    <ToggleButton x:Name="togglePopUp"
                                                  Content="v"
                                                  Width="15"
                                                  HorizontalAlignment="Right"
                                                  VerticalAlignment="Center"
                                                  BorderThickness="0"
                                                  Background="Transparent"
                                                  Foreground="DarkGray"
                                                  Margin="5,0,0,0" />
                                    <Popup Name="myPopup"
                                           DataContext="{Binding DataContext, ElementName=myWindow}"
                                           IsOpen="{Binding IsChecked, ElementName=togglePopUp}">
                                        <Border BorderThickness="1">
                                            <StackPanel>
                                                <!-- in this textblock i get the property name that I want to use as index -->
                                                <TextBlock Name="myPopupText"
                                                           Background="LightBlue"
                                                           Foreground="Blue"
                                                           Padding="30"
                                                           Text="{Binding Column.Header,
                                                                RelativeSource={RelativeSource
                                                                Mode=FindAncestor, 
                                                                AncestorType=  {x:Type DataGridColumnHeader}},
                                                                StringFormat='{}{0}'}">
                                                </TextBlock>

                                                <ListBox ItemsSource="{Binding Path=PropertiesDictionary[{Binding Column.Header, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=  {x:Type DataGridColumnHeader}},StringFormat='{}{0}'}]}">

                                                <!-- this declaration works but only with static dictionary key name -->
                                                <!--<ListBox ItemsSource="{Binding Source={x:Reference myWindow}, Path=PropertiesDictionary[Name]}">-->
                                                <!--<ListBox ItemsSource="{Binding Source={x:Reference myWindow}, Path=PropertiesDictionary[{Binding Column.Header, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridColumnHeader}}}]}">-->
                                                <!--<ListBox ItemsSource="{Binding Path=PropertiesDictionary[{Binding Column.Header, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type DataGridColumnHeader}}}]}">-->
                                                </ListBox>

                                            </StackPanel>
                                        </Border>
                                    </Popup>
                                </StackPanel>
                            </DataTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </DataGrid.Resources>
        </DataGrid>
    </Grid>
</Window>

Code behind:

using Prove_varie.Mocks;
using System.Collections.ObjectModel;
using System.Reflection;
using System.Windows;

namespace Prove_varie
{
    public partial class GridWindow : Window
    {
        public ObservableCollection<Persona> Persone { get; }

        public Dictionary<string, ObservableCollection<object>> PropertiesDictionary { get; private set; }

        public GridWindow()
        {
            InitializeComponent();
            Persone = new ObservableCollection<Persona>(new FakeDataProvider().GetData());
            DataContext = this;
            PropertiesDictionary = new Dictionary<string, ObservableCollection<object>>();

            PropertyInfo[] propArray = typeof(Persona).GetProperties();

            foreach (var prop in propArray)
            {
                PropertiesDictionary.Add(prop.Name,
                    new ObservableCollection<object>(Persone.Select(p => prop.GetValue(p)!).Distinct()));

            }
        }
    }
}

All suggestions are welcome, even about using completely different approach.


Solution

  • The Popup is not a child of the visual tree that the DatGrid is a child of. The DataGridColumn and its elements like DataGridColumnHeader are not a child of the visual tree. This is why you can't reference elements that are children of the Window (or tree root in general).

    But you can reference static resources.
    For example, you could define a CollectionViewSource that binds to your Dictionary. Then bind to it using a MultiBinding. You have to use a MultiBinding because nested Binding expressions are not supported:

    DictionaryToValueConverter.cs

    public class DictionaryToValueConverter : IMultiValueConverter
    {
      public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
      {
        return values.OfType<string>().FirstOrDefault() is string key
          && values.OfType<Dictionary<string, IEnumerable<object>>>().FirstOrDefault() is Dictionary<string, IEnumerable<object>> propertyMap
          && propertyMap.TryGetValue(key, out IEnumerable<object> propertyValues)
            ? propertyValues
            : Binding.DoNothing;
      }
    
      public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) 
        => throw new NotSupportedException();
    }
    

    GridWindow.cs

    <DataGrid>
      <DataGrid.Resources>
        <local:DictionaryToValueConverter x:Key="DictionaryToValueConverter" />
        <CollectionViewSource x:Key="SourceDictionary"
                              Source="{Binding PropertiesDictionary}" />
    
        <Style TargetType="{x:Type DataGridColumnHeader}">
          <Setter Property="ContentTemplate">
            <Setter.Value>
              <DataTemplate>
                <StackPanel Orientation="Horizontal">
                  <TextBlock Text="{Binding}"
                             TextWrapping="Wrap"
                             HorizontalAlignment="Center"
                             VerticalAlignment="Center" />
                  <ToggleButton x:Name="togglePopUp"
                                Content="v"
                                Width="15"
                                HorizontalAlignment="Right"
                                VerticalAlignment="Center"
                                BorderThickness="0"
                                Background="Transparent"
                                Foreground="DarkGray"
                                Margin="5,0,0,0" />
                  <Popup Name="myPopup"
                         IsOpen="{Binding IsChecked, ElementName=togglePopUp}">
                    <Border BorderThickness="1">
                      <StackPanel>
                        <!-- in this textblock i get the property name that I want to use as index -->
                        <TextBlock Name="myPopupText"
                                   Background="LightBlue"
                                   Foreground="Blue"
                                   Padding="30"
                                   Text="{Binding Column.Header, RelativeSource={RelativeSource AncestorType=DataGridColumnHeader}, StringFormat='{}{0}'}" />
    
                        <ListBox>
                          <ListBox.ItemsSource>
                            <MultiBinding Converter="{StaticResource DictionaryToValueConverter}">
                              <Binding Source="{StaticResource SourceDictionary}"
                                       Path="(CollectionView.SourceCollection)" />
                              <Binding ElementName="myPopupText"
                                       Path="Text" />
                            </MultiBinding>
                          </ListBox.ItemsSource>
                        </ListBox>
                      </StackPanel>
                    </Border>
                  </Popup>
                </StackPanel>
              </DataTemplate>
            </Setter.Value>
          </Setter>
        </Style>
      </DataGrid.Resources>
    </DataGrid>