WPF:当特定值更改时重新应用DataTemplateSelector

32

以下是我的 XAML 代码:

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

这是我的ListTemplateSelector类:

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;
}
}
GroupTemplate数据模板内部引用了ListTemplateSelector,所以我设置的方式就是这样。这是我能想到的唯一递归Hack。但这不是我的问题所在。
我的问题是,当IsLeaf属性更改时,我想从ItemTemplate更改为GroupTemplate。这在第一次读取属性时非常有效。但是一旦此属性更改,模板选择器就不会重新应用。现在,我可以使用触发器来绑定该值并适当设置项模板,但我需要能够为每个项设置不同的模板,因为它们可能处于不同的状态。
例如,假设我有一个如下所示的组列表:
Group 1: IsLeaf = false,则template = GroupTemplate Group 2: IsLeaf = true,则template = ItemTemplate Group 3: IsLeaf = false,则template = GroupTemplate 并且一旦组1的IsLeaf属性更改为true,模板就需要自动更改为ItemTemplate。
编辑:
这是我的临时解决方案。有更好的方法吗?
<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>

2
为了清晰起见,您是放弃了DataTemplateSelector方法而选择了触发器,还是将触发器与DataTemplateSelector方法结合在一起解决问题的? - alastairs
@alastairs 我不能代表OP说话,但触发器似乎使DataTemplateSelector变得不必要。 - piedar
5个回答

27

我找到了一个对我来说似乎更容易的解决办法。在TemplateSelector中监听你关心的属性,然后重新应用模板选择器以强制刷新。

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

1
这个完美地运行了!在 WPF 中解决这个缺失功能的所有解决方法中最好的一个! - Vaccano
3
使用这段代码要谨慎——在我将其作为解决自身模板切换问题的方案实施后,注意到性能下降,进一步调查发现涉及的DataTemplate过大导致了严重的内存泄漏。更好的方法是使用DataTriggers方法,这种方法似乎完全没有泄漏问题。 - toadflakz
已经很久了,但我不得不在通用应用程序中实现此解决方法,因为WinRT没有Style.Triggers... - Loul G.
3
Mem泄漏可能是由于(或者因为)这个lambda表达式被分配给了PropertyChanged,在ItemsSource中的每个项目中都会运行,因此会重复运行-每次都强制刷新。也许只应用一次会更好的性能? - MemeDeveloper

25

关于你的修改,使用数据模板触发器 Trigger 而不是使用 Style 是否足够?如下:

<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>

是的,那样会更好。我忘记了DataTemplate触发器。我将使用它作为我的解决方案,所以谢谢! - Nick
10
希望WPF团队能改进DataTemplateSelectors,以便允许这种情况,因为尽管必要,但这种解决方案语法更加复杂。 - Xcalibur

4

回到您的原始解决方案和“模板选择器没有重新应用”的问题:您可以像这样刷新视图

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

为了简洁起见,在XAML中,您的ItemsControl将通过其名称("YourItemsControl")进行引用:

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

唯一的问题可能是如何在您的项目中选择适当的位置来执行此刷新指令。它可以放在视图代码后台,或者如果您的IsLeaf是一个DP,则正确的位置应该是依赖属性更改回调。

这个方法非常有效!在我的情况下,我有一个自定义控件,其中包含一个 ListBox 模板部分,其项具有 3 种可能的数据模板(第一个、中间和最后一个项),当 ItemsSource 更改时,ItemTemplateSelector 应该更新。我的列表更改代码如下: if (Template?.FindName("PART_HeaderList", this) is ListBox listBox) { CollectionViewSource.GetDefaultView(listBox.ItemsSource).Refresh(); } - LoRdPMN

1

我对现有的解决方案并不满意,我将发布我已经找到的用于检查选择器更改的方法:

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

XAML中的用法:

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

这可以更改为将依赖项设置为列表,但在我的情况下,字符串更好。它运行良好且没有内存泄漏。此外,您可以将DataTemplates放入一个额外的文件中,这不会垃圾回收您的主xaml。
祝好, 马可

1
我会使用绑定代理来完成它。
它的工作原理类似于普通的绑定代理(但有两个Props——从DataIn复制数据到DataOut),但是每当Trigger值更改时,它会将DataOut设置为NULL,然后再将其设置回DataIn的值:
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;
    }

}

像这样使用它:
<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>

所以您不直接绑定到您的DataContext,而是绑定到BindingProxy的DataOut,它反映了原始的DataContext,但有一个小区别:当触发器改变(在这个例子中是“Item”内的bool值)时,TemplateSelector会重新触发。您不必更改TemplateSelector来实现这一点。还可以添加更多的Triggers,只需添加Trigger2即可。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接