Search code examples
c#wpfmvvmdata-bindingdatagrid

Set WPF Binding Source to DataGrid Columns


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?


Solution

  • 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();
        }
    }