WPF:当某个值更改时重新应用 DataTemplateSelector

Posted

技术标签:

【中文标题】WPF:当某个值更改时重新应用 DataTemplateSelector【英文标题】:WPF: Reapply DataTemplateSelector when a certain value changes 【发布时间】:2011-04-02 09:25:34 【问题描述】:

这是我拥有的 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,所以这就是我设置的原因。这是我可以组合的唯一递归黑客。但这不是我遇到的问题。

我的问题是,当 IsLeaf 属性更改时,我想从 ItemTemplate 更改为 GroupTemplate。自从它第一次读取属性以来,这是第一次很好地工作。但是一旦这个属性改变,模板选择器就不会被重新应用。现在,我可以使用触发器绑定到值并适当地设置项目模板,但我需要能够为每个项目设置不同的模板,因为它们可能处于不同的状态。

例如,假设我有一个这样的组列表:

第 1 组:IsLeaf = false,因此模板 = GroupTemplate

第 2 组:IsLeaf = true,因此模板 = ItemTemplate

第 3 组:IsLeaf = false,因此模板 = 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>

【问题讨论】:

为了清楚起见,您是否放弃了 DataTemplateSelector 方法以支持触发器,或者您是否也使用 DataTemplateSelector 将触发器加入到解决方案中? @alastairs 我不能代表 OP,但触发器似乎使 DataTemplateSelector 变得不必要。 【参考方案1】:

关于您的 EDIT,DataTemplate 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 触发器。我会用这个作为我的解决方案,所以谢谢! DataTemplateSelectors 需要改进,以便它们允许这种情况,因为这种解决方案虽然是必要的,但却是一种更丑陋的语法。希望 WPF 团队能够解决这个问题【参考方案2】:

我发现这种解决方法对我来说似乎更容易。从 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());
        
    

【讨论】:

这很完美! WPF 中这个缺失功能​​的最佳解决方法! 小心使用此代码 - 在将其作为我自己的模板切换情况的解决方案并注意到性能下降后进行调查,发现由于切换时涉及的 DataTemplate 的大小而导致大量内存泄漏 - 好多了使用似乎根本不会泄漏的 DataTriggers 方法的想法。 已经很久了,但我不得不在通用应用程序中实现这个解决方法,因为 WinRT 没有 Style.Triggers... @toadflakz Mem 泄漏可能是由于(或增加)这个 lambda 被分配给在 ItemsSource 中的每个项目更改一次的属性,因此将重复运行 - 每次强制刷新.也许只应用一次会更好的性能?【参考方案3】:

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

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

为简洁起见,您的 ItemsControl 由添加到 XAML 的名称(“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(); 【参考方案4】:

我用绑定代理来做。

它像普通绑定代理一样工作(但有 2 个 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' 内的布尔值), TemplateSelector 被重新触发。

您不必为此更改 TemplateSelector。

也可以添加更多的Triggers,只需添加一个Trigger2即可。

【讨论】:

【参考方案5】:

我对我将发布的解决方案并不满意,我设法让选择器检查更改:

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。

干杯, 马可

【讨论】:

以上是关于WPF:当某个值更改时重新应用 DataTemplateSelector的主要内容,如果未能解决你的问题,请参考以下文章

WPF:在运行时更改配置文件用户设置?

WPF datagrid 更新数据库

有界数据更改后重新排序 WPF DataGrid

Mobx 仅在计算值更改时重新渲染项目

Flutter StreamBuilder ListView在流数据更改时不会重新加载

WPF MVVM Command CanExecute,仅在焦点更改时重新评估