WPF: повторное использование DataTemplateSelector при изменении определенного значения
Итак, вот 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 внутри себя, поэтому именно поэтому я настроен так, как я его настроил. Это единственный рекурсивный хак, который я мог бы собрать вместе. Но это не проблема, которую я испытываю.
Моя проблема в том, что я хочу изменить с ItemTemplate на GroupTemplate при изменении свойства IsLeaf. Это прекрасно работает в первый раз, так как он читает свойство в первый раз. Но как только это свойство изменится, селектор шаблонов не будет повторно применен. Теперь я мог бы использовать триггеры для привязки к значению и правильно установить шаблон элемента, но мне нужно иметь возможность устанавливать другой шаблон для каждого элемента, поскольку они могут находиться в другом состоянии.
Например, скажем, у меня есть список таких групп:
Группа 1: IsLeaf = false, поэтому template = GroupTemplate
Группа 2: IsLeaf = true, поэтому шаблон = ItemTemplate
Группа 3: IsLeaf = false, поэтому template = GroupTemplate
И как только свойство 1 группы IsLeaf изменится на true, шаблон должен автоматически измениться на ItemTemplate.
EDIT:
Вот мое временное решение. Любой лучший способ сделать это?
<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
Что касается вашего EDIT, не будет ли триггера DataTemplate достаточно, а не использовать стиль? То есть:
<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
Я нашел это обходное решение, которое мне кажется более легким. Изнутри 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());
}
}
}
Ответ 3
Возвращаясь к исходному решению, и проблема "селектор шаблонов не может быть повторно применен": вы можете обновить свое представление таким образом
CollectionViewSource.GetDefaultView(YourItemsControl.ItemsSource).Refresh();
где для краткости ваш элемент ItemsControl ссылается на свое имя ( "YourItemsControl" ), добавленное в ваш XAML:
<ItemsControl x:Name="YourItemsControl" ItemsSource="{Binding Path=Groups}"
ItemTemplateSelector="{Binding RelativeSource={RelativeSource AncestorType=Window}, Path=ListTemplateSelector}"/>
Единственной проблемой может быть выбор правильного места в проекте для этой команды обновления. Это может привести к обратному коду представления, или, если ваш IsLeaf является DP, правильным местом будет обратный вызов с измененной зависимостью.
Ответ 4
Я делаю это со связывающим прокси.
Он работает как обычный связующий прокси (но с 2 реквизитами - копирует данные из DataIn в DataOut), но устанавливает 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, но с небольшой разницей: когда триггер изменяется (в этом примере значение bool внутри "Item" ) TemplateSelector будет перезагружен.
Вам не нужно менять свой TemplateSelector для этого.
Также возможно добавить больше триггеров, просто добавьте Trigger2.