-->

WPF: Reapply DataTemplateSelector when a certain v

2019-01-19 10:21发布

问题:

So here is the XAML that I have:

<ItemsControl ItemsSource="{Binding Path=Groups}" ItemTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ListTemplateSelector}"/>

Here is my ListTemplateSelector class:

public class ListTemplateSelector : DataTemplateSelector {
public DataTemplate GroupTemplate { get; set; }
public DataTemplate ItemTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
    GroupList<Person> list = item as GroupList<Person>;
    if (list != null && !list.IsLeaf)
        return GroupTemplate;
    return ItemTemplate;
}
}

The GroupTemplate data template references the ListTemplateSelector inside itself, so this is why I have set up like I have it set up. It's the only recursive hack I could put together. But that's not the problem I'm having.

My problem is, I want to change from ItemTemplate to GroupTemplate when the IsLeaf property changes. This works beautifully the very first time since it reads the property the first time. But once this property changes, the template selector doesn't get reapplied. Now, I could use triggers to bind to the value and set the item template appropriately, but I need to be able to set a different template for each item, as they could be in a different state.

For instance, say I have a list of groups like this:

Group 1: IsLeaf = false, so template = GroupTemplate

Group 2: IsLeaf = true, so template = ItemTemplate

Group 3: IsLeaf = false, so template = GroupTemplate

And once group 1's IsLeaf property changes to true, the template needs to automatically change to ItemTemplate.

EDIT:

Here is my temporary solution. Any better way to do it?

<ItemsControl ItemsSource="{Binding Path=Groups}">
<ItemsControl.ItemTemplate>
    <DataTemplate>
        <ContentControl Content="{Binding}">
            <ContentControl.Style>
                <Style TargetType="{x:Type ContentControl}">
                    <Setter Property="ContentTemplate" Value="{DynamicResource ItemTemplate}"/>
                    <Style.Triggers>
                        <DataTrigger Binding="{Binding Path=IsLeaf}" Value="False">
                            <Setter Property="ContentTemplate" Value="{DynamicResource GroupTemplate}"/>
                        </DataTrigger>
                    </Style.Triggers>
                </Style>
            </ContentControl.Style>
        </ContentControl>
    </DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>

回答1:

Regarding your EDIT, wouldn't a DataTemplate Trigger be enough instead of using a Style? That is:

<ItemsControl ItemsSource="{Binding Path=Groups}">
    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <ContentControl x:Name="cc" Content="{Binding}" ContentTemplate="{DynamicResource ItemTemplate}"/>

            <DataTemplate.Triggers>
                <DataTrigger Binding="{Binding Path=IsLeaf}" Value="False">
                    <Setter TargetName="cc" Property="ContentTemplate" Value="{DynamicResource GroupTemplate}"/>
                </DataTrigger>
            </DataTemplate.Triggers>

        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>


回答2:

I found this workaround that seems easier to me. From within the TemplateSelector listen to the property that your care about and then reapply the template selector to force a refresh.

public class DataSourceTemplateSelector : DataTemplateSelector
{

    public DataTemplate IA { get; set; }
    public DataTemplate Dispatcher { get; set; }
    public DataTemplate Sql { get; set; }

    public override DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
    {
        var ds = item as DataLocationViewModel;
        if (ds == null)
        {
            return base.SelectTemplate(item, container);
        }
      PropertyChangedEventHandler lambda = null;
        lambda = (o, args) =>
            {
                if (args.PropertyName == "SelectedDataSourceType")
                {
                    ds.PropertyChanged -= lambda;
                    var cp = (ContentPresenter)container;
                    cp.ContentTemplateSelector = null;
                    cp.ContentTemplateSelector = this;                        
                }
            };
        ds.PropertyChanged += lambda;

        switch (ds.SelectedDataSourceType.Value)
        {
            case DataSourceType.Dispatcher:
                return Dispatcher;
            case DataSourceType.IA:
                return IA;
            case DataSourceType.Sql:
                return Sql;
            default:
                throw new NotImplementedException(ds.SelectedDataSourceType.Value.ToString());
        }
    }


}


回答3:

Returning back to your original solution and the problem of "the template selector doesn't get reapplied": you can refresh your view like that

CollectionViewSource.GetDefaultView(YourItemsControl.ItemsSource).Refresh();

where for brevity sake your ItemsControl is referenced by its name ("YourItemsControl") added to your XAML:

<ItemsControl x:Name="YourItemsControl" ItemsSource="{Binding Path=Groups}" 
ItemTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ListTemplateSelector}"/>

The only problem may be how to choose right place in your project for this refresh instruction. It could go into a view code-behind, or, if your IsLeaf is a DP, the right place would be a dependency-property-changed callback.



回答4:

I do it with a binding proxy.

It works like a normal binding proxy (but with 2 Props - copies data from DataIn to DataOut), but sets the DataOut to NULL and back to the DataIn value whenever the Trigger value changes:

public class BindingProxyForTemplateSelector : Freezable
{
    #region Overrides of Freezable

    protected override Freezable CreateInstanceCore()
    {
        return new BindingProxyForTemplateSelector();
    }

    #endregion

    public object DataIn
    {
        get { return (object)GetValue(DataInProperty); }
        set { SetValue(DataInProperty, value); }
    }

    public object DataOut
    {
        get { return (object) GetValue(DataOutProperty); }
        set { SetValue(DataOutProperty, value); }
    }

    public object Trigger
    {
        get { return (object) GetValue(TriggerProperty); }
        set { SetValue(TriggerProperty, value); }
    }


    public static readonly DependencyProperty TriggerProperty = DependencyProperty.Register(nameof(Trigger), typeof(object), typeof(BindingProxyForTemplateSelector), new PropertyMetadata(default(object), OnTriggerValueChanged));

    public static readonly DependencyProperty DataInProperty = DependencyProperty.Register(nameof(DataIn), typeof(object), typeof(BindingProxyForTemplateSelector), new UIPropertyMetadata(null, OnDataChanged));

    public static readonly DependencyProperty DataOutProperty = DependencyProperty.Register(nameof(DataOut), typeof(object), typeof(BindingProxyForTemplateSelector), new PropertyMetadata(default(object)));



    private static void OnTriggerValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        // this does the whole trick

        var sender = d as BindingProxyForTemplateSelector;
        if (sender == null)
            return;

        sender.DataOut = null; // set to null and then back triggers the TemplateSelector to search for a new template
        sender.DataOut = sender.DataIn;
    }



    private static void OnDataChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var sender = d as BindingProxyForTemplateSelector;
        if (sender == null)
            return;

        sender.DataOut = e.NewValue;
    }

}

Use it like this:

<Grid>
    <Grid.Resources>
        <local:BindingProxyForTemplateSelector DataIn="{Binding}" Trigger="{Binding Item.SomeBool}" x:Key="BindingProxy"/>
    </Grid.Resources>
    <ContentControl Content="{Binding Source={StaticResource BindingProxy}, Path=DataOut.Item}" ContentTemplateSelector="{StaticResource TemplateSelector}"/>
</Grid>

So you don't bind to your DataContext directly, but to the BindingProxy's DataOut, which mirrors the original DataContext, but with a small difference: When the trigger changes (in this example a bool value inside the 'Item'), the TemplateSelector gets retriggered.

You don't have to change your TemplateSelector for this.

It is also possible to add more Triggers, just add a Trigger2.