WPF: Reapply DataTemplateSelector when a certain value changes

安稳与你 提交于 2019-11-28 11:53:06

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>

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


}

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.

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.

Marco

I wasn't really satisfied with the solutions I will post the way I've managed to get the selector check for changes:

public class DynamicSelectorContentControl : ContentControl
{
    // Using a DependencyProperty as the backing store for ListenToProperties.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ListenToPropertiesProperty =
        DependencyProperty.Register("ListenToProperties", typeof(string),
            typeof(DynamicSelectorContentControl),
            new FrameworkPropertyMetadata(string.Empty));

    public DynamicSelectorContentControl()
    {
        this.DataContextChanged += DynamicSelectorContentControl_DataContextChanged;
    }

    public string ListenToProperties
    {
        get { return (string)GetValue(ListenToPropertiesProperty); }
        set { SetValue(ListenToPropertiesProperty, value); }
    }
    private void CheckForProperty(object sender, PropertyChangedEventArgs e)
    {
        if (ListenToProperties.Contains(e.PropertyName))
        {
            ClearSelector();
        }
    }

    private void ClearSelector()
    {
        var oldSelector = this.ContentTemplateSelector;
        if (oldSelector != null)
        {
            this.ContentTemplateSelector = null;
            this.ContentTemplateSelector = oldSelector;
        }
    }

    private void DynamicSelectorContentControl_DataContextChanged(object sender, System.Windows.DependencyPropertyChangedEventArgs e)
    {
        var listOfProperties = ListenToProperties.Split(',').Select(s => s.Trim());

        var oldObservable = e.OldValue as INotifyPropertyChanged;

        if (oldObservable != null && listOfProperties.Any())
        {
            PropertyChangedEventManager.RemoveHandler(oldObservable, CheckForProperty, string.Empty);
        }

        var newObservable = e.NewValue as INotifyPropertyChanged;

        if (newObservable != null && listOfProperties.Any())
        {
            PropertyChangedEventManager.AddHandler(newObservable, CheckForProperty, string.Empty);
        }

        if (e.OldValue != null)
        {
            ClearSelector();
        }
    }
}

Usage in XAML:

                                <controls:DynamicSelectorContentControl DockPanel.Dock="Top"
                                            ContentTemplateSelector="{StaticResource AgeGenderSelector}"
                                            ListenToProperties="Gender, Age"                        
                                            Content="{Binding .}"/>

This could be changed to have the dependency be a list, but a string was better for my case. It works well and has no memory leak. Besides, you can have you DataTemplates in an extra file which does not garbage your main xaml.

Cheers, Marco

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!