WPF DataTemplate binding parameter in Window.Resou

2019-07-12 03:24发布

问题:

I'm creating a datagrid, with filters in the column headers. It works, but I don't think it's a good approach. Let me show you the code, very simple example:

The View

<Window x:Class="TestDataGridApp.Views.MainWindow"
        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:viewModels="clr-namespace:TestDataGridApp.ViewModels"
        mc:Ignorable="d"
        Title="MainWindow" Height="300" Width="300">
    <Window.DataContext>
        <viewModels:MainWindowViewModel />
    </Window.DataContext>
    <Window.Resources>
        <DataTemplate x:Key="DataGridHeader">
            <DockPanel>
                <TextBlock DockPanel.Dock="Top" TextAlignment="Left" Text="{Binding Content, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
                <TextBox DockPanel.Dock="Top" Text="{Binding DataContext.FilterName, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=LostFocus}"/>
            </DockPanel>
        </DataTemplate>
    </Window.Resources>
    <Grid>
        <DataGrid ItemsSource="{Binding ItemCollection}" AutoGenerateColumns="False">
            <DataGrid.ColumnHeaderStyle>
                <Style TargetType="{x:Type DataGridColumnHeader}">
                    <Setter Property="HorizontalContentAlignment" Value="Stretch" />
                </Style>
            </DataGrid.ColumnHeaderStyle>

            <DataGrid.Columns>
                <DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" Width="60" MinWidth="60" MaxWidth="60" HeaderTemplate="{StaticResource DataGridHeader}"/>
                <DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" Width="60" MinWidth="60" MaxWidth="60" HeaderTemplate="{StaticResource DataGridHeader}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

ViewModel

namespace TestDataGridApp.ViewModels
{
    using System;
    using System.Collections.ObjectModel;
    using System.ComponentModel;
    using System.Windows.Data;
    using TestDataGridApp.Entities;
    using Prism.Mvvm;
    public class MainWindowViewModel : BindableBase
    {
        private string _filterId;
        private string _filterName;
        private ObservableCollection<Item> _items = new ObservableCollection<Item>();

        public MainWindowViewModel()
        {
            for (int i = 1; i <= 100; ++i)
            {
                Items.Add(new Item() {Id = i, Name = $"Item{i}"});
            }
        }
        public string FilterId
        {
            get { return _filterId; }
            set
            {
                SetProperty(ref _filterId, value);
                TriggerFilters();
            }
        }
        public string FilterName
        {
            get { return _filterName; }
            set
            {
                SetProperty(ref _filterName, value);
                TriggerFilters();
            }
        }
        public ObservableCollection<Item> Items
        {
            get { return _items; }
            set { SetProperty(ref _items, value); }
        }
        public ICollectionView ItemCollection => CollectionViewSource.GetDefaultView(Items);

        private void TriggerFilters()
        {
            ItemCollection.Filter = o => FilterItem((Item)o);
        }
        private bool FilterItem(Item item)
        {
            try
            {
                bool checkId = false;
                bool checkName = false;

                int itemId = 0;
                if (!string.IsNullOrEmpty(FilterId) && int.TryParse(FilterId, out itemId)) checkId = true;
                if (!string.IsNullOrEmpty(FilterName)) checkName = true;

                if (!checkId && !checkName) return true;
                if (item == null) return false;

                bool checkIdIsOk = (checkId && item.Id == int.Parse(FilterId) || !checkId);
                bool checkNameIsOk = (checkName && item.Name.ToUpper().Contains(FilterName.ToUpper()) || !checkName);
                if (checkIdIsOk && checkNameIsOk) return true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            return false;
        }
    }
}

The Item

public class Item
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Basically simple datagrid, 2 columns. In each column there is a TextBox with binded filter. Each filter has its own field, so after the focus is lost, I can filter the grid by all filters.

My issue is.. I have a lot of columns. This is customized datagrid, so you can add and remove columns on the fly and there's a lot of duplicated code. Basically this is duplicated:

                <DataGridTextColumn.HeaderTemplate>
                    <DataTemplate>
                        <DockPanel>
                            <TextBlock DockPanel.Dock="Top" TextAlignment="Left" Text="{Binding Content, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
                            <TextBox DockPanel.Dock="Top"
                                     Text="{Binding DataContext.FilterId, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=LostFocus}"/>
                        </DockPanel>
                    </DataTemplate>
                </DataGridTextColumn.HeaderTemplate>

... only this <TextBox DockPanel.Dock="Top" Text="{Binding DataContext.FilterId, ... is changing for different columns.

So, I thought, I can easily replace it with this solution, but now.. I lost binding to my filter fields in the ViewModel:

<Window.Resources>
    <DataTemplate x:Key="DataGridHeader">
        <DockPanel>
            <TextBlock DockPanel.Dock="Top" TextAlignment="Left" Text="{Binding Content, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
            <TextBox DockPanel.Dock="Top" Text="{Binding DataContext.FilterName, RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=LostFocus}"/>
        </DockPanel>
    </DataTemplate>
</Window.Resources>
<Grid>
    <DataGrid ItemsSource="{Binding ItemCollection}" AutoGenerateColumns="False">
        <DataGrid.ColumnHeaderStyle>
            <Style TargetType="{x:Type DataGridColumnHeader}">
                <Setter Property="HorizontalContentAlignment" Value="Stretch" />
            </Style>
        </DataGrid.ColumnHeaderStyle>
        <DataGrid.Columns>
            <DataGridTextColumn Header="Id" Binding="{Binding Path=Id}" Width="60" MinWidth="60" MaxWidth="60" HeaderTemplate="{StaticResource DataGridHeader}"/>
            <DataGridTextColumn Header="Name" Binding="{Binding Path=Name}" Width="60" MinWidth="60" MaxWidth="60" HeaderTemplate="{StaticResource DataGridHeader}"/>
        </DataGrid.Columns>
    </DataGrid>
</Grid>

SOO.. I was thinking, to create a Dictionary for filters, where key would be the name of the column and in value I will store current filter (or null, if there's no filter at the moment for this column). Something like..

<TextBox x:Name="Foo" DockPanel.Dock="Top" Text="{Binding DataContext.FiltersDictionary[Foo], RelativeSource={RelativeSource AncestorType=Window}, UpdateSourceTrigger=LostFocus}"/>

But then I have to Biding contexts.. for one TextBox. I'm really not sure about this solution..

My question will be, how to create a parameter for DataTemplate in the above scenario?

Thanks for help!

PS. It's not a duplicate. This question is about "how to create a parameter for DataTemplate". The "duplicated" question is about dictionary as a binding - a potential solution for this question.. although probably NOT. As another user suggested there might be totally different, better solution to solve this problem. Two different things. I'm shocked that I have to explain this

回答1:

Easiest way is to not rely only on xaml and add some code to help. For example use Loaded event of your TextBox like this:

<DataTemplate x:Key="DataGridHeader">
    <DockPanel>
        <TextBlock DockPanel.Dock="Top" TextAlignment="Left" Text="{Binding Content, RelativeSource={RelativeSource Mode=TemplatedParent}}" />
        <TextBox DockPanel.Dock="Top" Loaded="OnFilterBoxLoaded" />
    </DockPanel>
</DataTemplate>

And setup binding when it is loaded:

private void OnFilterBoxLoaded(object sender, RoutedEventArgs e) {
    var tb = (TextBox)sender;
    // find column
    DataGridColumnHeader parent = null;
    DependencyObject current = tb;
    do {
        current = VisualTreeHelper.GetParent(current);
        parent = current as DataGridColumnHeader;
    }
    while (parent == null);
    // setup binding
    var binding = new Binding();
    // use parent column header as name of the filter property
    binding.Path = new PropertyPath("DataContext.Filter" + parent.Column.Header);
    binding.Source = this;
    binding.UpdateSourceTrigger = UpdateSourceTrigger.LostFocus;
    tb.SetBinding(TextBox.TextProperty, binding);
}

You can use attached property to achieve the same, but I don't think it's needed in this case.



回答2:

I used Evk solution with DependencyProperty instead of using Header

<controls:FilterDataGridTextColumn FilterName="Name" Header="Name" Binding="{Binding Path=Name}" Width="200" HeaderTemplate="{StaticResource HeaderTemplate}" />

FilterDataGridTextColumn :

public class FilterDataGridTextColumn : DataGridTextColumn
{
    public static readonly DependencyProperty FilterNameProperty =
        DependencyProperty.Register("FilterName", typeof(string), typeof(FilterDataGridTextColumn));

    public string FilterName
    {
        get { return (string) GetValue(FilterNameProperty); }
        set { SetValue(FilterNameProperty, value); }
    }
}