Set WPF Binding Source to DataGrid Columns

Viewed 2090

I have a WPF DataGrid with 18 columns and each column has a TextBox above it so I can filter the column.

Each TextBox binds Width to ActualWidth of the column.

<StackPanel Grid.Row="0" Orientation="Horizontal">
    <TextBox Width="{Binding Path=ActualWidth, ElementName=Column1}"
             Text="{Binding FilterFirstName}"/>
    <TextBox Width="{Binding Path=ActualWidth, ElementName=Column2}"
             Text="{Binding FilterLastName}"/>
    <TextBox Width="{Binding Path=ActualWidth, ElementName=Column3}"
             Text="{Binding FilterAge}"/>
    <!-- 15 more -->
</StackPanel>
<DataGrid x:Name="dataGridUsers" Grid.Row="1"
    ItemsSource="{Binding Users}">
    <DataGrid.Columns>
        <DataGridTextColumn x:Name="Column1" Width="*"
            Binding="{Binding FirstName}"/>
        <DataGridTextColumn x:Name="Column2" Width="*"
            Binding="{Binding LastName}"/>
        <DataGridTextColumn x:Name="Column3" Width="*"
            Binding="{Binding Age}"/>
        <!-- 15 more -->
    </DataGrid.Columns>
</DataGrid>

I know that I can bind TextBox Text to a List<string> like this:

<TextBox Width="{Binding Path=ActualWidth, ElementName=Column1}"
         Text="{Binding Filters[0]}"/>
<TextBox Width="{Binding Path=ActualWidth, ElementName=Column2}"
         Text="{Binding Filters[1]}"/>
<TextBox Width="{Binding Path=ActualWidth, ElementName=Column3}"
         Text="{Binding Filters[2]}"/>

I would like to bind Width of the TextBox to ActualWidth of the column with something like this:

<TextBox Width="{Binding Path=ActualWidth, Source=dataGridUsers.Columns[0]}"
         Text="{Binding Filters[0]}"/>
<TextBox Width="{Binding Path=ActualWidth, Source=dataGridUsers.Columns[1]}"
         Text="{Binding Filters[1]}"/>
<TextBox Width="{Binding Path=ActualWidth, Source=dataGridUsers.Columns[2]}"
         Text="{Binding Filters[2]}"/>

Because then I could use ItemsControl instead of StackPanel but it doesn't work this way.

Is there any other way I could achieve this?

3 Answers

You cant bind to DataGrid Columns properties because they are only in the logical tree - not the visual tree. The only way to do something like this is to change the DataGridTextColumn.HeaderTemplate and create a new DataTemplate with the filter TextBox within.

You can indeed bind to the Columns of your DataGrid by doing so

<TextBox Width="{Binding Columns[0].ActualWidth, ElementName=dataGridUsers}" />

But this won't work for what you want to achieve. As soon as you reorder the columns at runtime the order won't match the order of your TextBoxes anymore. So you have to reorder them too.
Note: DataGridColumn.DisplayIndex returns the current index within your DataGrid.


A much better approach and the recommended way would be placing your "FilterTextBoxes" inside the headers of your columns by changing the DataGridColumn.HeaderTemplate

How to use ItemsControl with TextBox to filter DataGrid columns.

It is possible to use ItemsControl and bind to DataGrid.Columns like this:

<ItemsControl Grid.Row="0" ItemsSource="{Binding Path=Columns,
              ElementName=dataGrid}">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <TextBox Width="{Binding ActualWidth}">
                <TextBox.Resources>
                    <local:ListIndexToValueConverter
                        x:Key="listIndexToValueConverter"/>
                </TextBox.Resources>
                <TextBox.Text>
                    <MultiBinding Converter="{StaticResource
                listIndexToValueConverter}" UpdateSourceTrigger="PropertyChanged">
                        <Binding Path="DataContext.Filters"
                                 ElementName="userControl"/>
                        <Binding Path="DisplayIndex"/>
                    </MultiBinding>
                </TextBox.Text>
            </TextBox>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>
<DataGrid x:Name="dataGrid" Grid.Row="1" ItemsSource="{Binding Users}">
    <DataGrid.Columns>
    <DataGridTextColumn DisplayIndex="0" Width="*" Binding="{Binding FirstName}"/>
    <DataGridTextColumn DisplayIndex="1" Width="*" Binding="{Binding LastName}"/>
    <DataGridTextColumn DisplayIndex="2" Width="*" Binding="{Binding Age}"/>
    </DataGrid.Columns>
</DataGrid>

Because you set the ItemsControl.ItemsSource to DataGrid.Columns instead of DataContext.Filters you have to set DataGridColumn.DisplayIndex and use IMultiValueConverter to be able to access DataContext.Filters again:

public class ListIndexToValueConverter : IMultiValueConverter
{
    private IList _list;
    private int _index;
    public object Convert(object[] values, Type targetType,
        object parameter, CultureInfo culture)
    {
        if (values.Length < 2)
            return Binding.DoNothing;
        if (values[0] is IList && values[1] is int)
        {
            _list = (IList)values[0];
            _index = (int)values[1];
            return _list[_index];
        }
        return Binding.DoNothing;
    }
    public object[] ConvertBack(object value, Type[] targetTypes,
        object parameter, CultureInfo culture)
    {
        _list[_index] = value;
        return new object[] { Binding.DoNothing, Binding.DoNothing };
    }
}

The ViewModel:

public class UsersViewModel : BindableBase
{
    public ObservableCollection<User> Users { get; set; }
    private ICollectionView _usersView;
    public ObservableCollection<string> Filters { get; set; }
    public UsersViewModel()
    {
        _usersView = CollectionViewSource.GetDefaultView(Users);
        _usersView.Filter = delegate (object item)
        {
            User user = item as User;
            List<string> columns = new List<string>()
                { user.FirstName, user.LastName, user.Age };
            bool include = true;
            for (int i = 0; i < columns.Count; ++i)
            {
                if (!string.IsNullOrEmpty(Filters[i]) &&
                    columns[i].IndexOf(Filters[i],
                    StringComparison.OrdinalIgnoreCase) == -1)
                {
                    include = false;
                    break;
                }
            }
            return include;
        };
        Filters.CollectionChanged += (object sender,
            NotifyCollectionChangedEventArgs e) => _usersView.Refresh();
    }
}
Related