0

I have an ItemsControl bound to an ItemsSource. Each item can have one of several DataTemplates assigned depending on the value of various properties on the item. These properties can change at runtime, and the DataTemplates need to be swapped out individually. In WPF I was able to do so with the following (partial simplified xaml):

<ItemsControl 
    ItemsSource="{Binding Items}">
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="{x:Type ContentPresenter}">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <MultiBinding Converter="{StaticResource RowTemplateConverter}">
                        <Binding Path="(local:Row.Sum)" />
                        <Binding Path="(local:Row.Avg)" />
                        <Binding Path="(local:Row.ShowFlagA)" />
                        <Binding Path="(local:Row.ShowFlagB)" />    
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style>
    </ItemsControl.ItemContainerStyle>

I've run into several issues trying to move this to UWP:

  1. MultiBinding is not supported
  2. To compensate for the above, I tried consolidating the converter logic into a single string property of the Row but the DataTemplate doesn't appear to be assigned. Also the binding syntax I used gives runtime XAML errors, not sure why.
<ItemsControl 
    ItemsSource="{Binding Items}" >
    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="ContentTemplate">
                <Setter.Value>
                    <Binding Path="RowTemplate" Converter="{StaticResource RowTemplateConverter}"/>
                </Setter.Value>
            </Setter>  
        </Style>
    </ItemsControl.ItemContainerStyle>
  1. DataTemplateSelector and ItemContainerStyleSelectors won't work because they're only evaluated once, and I need the templates updated on various property changes
  2. I've seen some answers here that say to null the above Selectors and re-assign them. This is the closest I've been able to get to my desired behavior, but the performance is poor with dozens of items, and fast changing properties, so I'm unable to use this.
Sean Beanland
  • 1,098
  • 1
  • 10
  • 25
  • 2
    Bindings won't work in UWP Style Setters. You may try an approach like this: https://stackoverflow.com/a/33582406/1136211 – Clemens Jul 28 '21 at 06:23

2 Answers2

1

You can write an attached behavior to accomplish this. Alternatively extend e.g. ItemsControl (or a derived type).

The key is to reassign the item container's content in order to invoke the DataTemplateSelector again.
The attacehed property will reset the content to trigger the DataTemplateSelector. Your view model will track the changes of the data items that require to re-evaluate the actual DataTemplate and finally trigger the attached property. This is done by simply assigning the changed item to a view model property that binds to the attached behavior.

First create a template selector by extending DataTemplateSelector:

public class DataItemTemplateSelector : DataTemplateSelector
{
  public DataTemplate ActivatedTemplate { get; set; }
  public DataTemplate DeactivatedTemplate { get; set; }

  protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
  {
    switch (item)
    {
      case DataItem dataItem when dataItem.IsActivated: return this.ActivatedTemplate; 
      default: return this.DeactivatedTemplate; 
    }
  }
}

Implement the attached behavior that modifies the container of the changed item:

public class TemplateSelector : DependencyObject
{
  public static object GetChangedItem(DependencyObject obj)
  {
    return (object)obj.GetValue(ChangedItemProperty);
  }

  public static void SetChangedItem(DependencyObject obj, object value)
  {
    obj.SetValue(ChangedItemProperty, value);
  }

  public static readonly DependencyProperty ChangedItemProperty =
      DependencyProperty.RegisterAttached("ChangedItem", typeof(object), typeof(TemplateSelector), new PropertyMetadata(default(object), OnChangedItemChanged));

  private static void OnChangedItemChanged(DependencyObject attachingElement, DependencyPropertyChangedEventArgs e)
  {
    if (!(attachingElement is ItemsControl itemsControl))
    {
      throw new ArgumentException($"Attaching element must be of type '{nameof(ItemsControl)}'");
    }

    var container = (itemsControl.ItemContainerGenerator.ContainerFromItem(e.NewValue) as ContentControl);
    var containerContent = container.Content;
    container.Content = null;
    container.Content = containerContent; // Trigger the DataTemplateSelector
  }
}

Apply the attached property and bind it to your view model. Also assign the template selector:

<Page>    
  <Page.Resources>
    <local:DataItemTemplateSelector x:Key="TemplateSelector">
      <local:DataItemTemplateSelector.ActivatedTemplate>
        <DataTemplate x:DataType="local:DataItem">
          <TextBlock Text="{Binding Text}" Foreground="Red" />
        </DataTemplate>
      </local:DataItemTemplateSelector.ActivatedTemplate>
      <local:DataItemTemplateSelector.DeactivatedTemplate>
        <DataTemplate x:DataType="local:DataItem">
          <TextBlock Text="{Binding Text}" Foreground="Black" />
        </DataTemplate>
      </local:DataItemTemplateSelector.DeactivatedTemplate>
    </local:DataItemTemplateSelector>
  </Page.Resources>

  <Grid>
    <ListBox ItemsSource="{x:Bind MainViewModel.DataItems}"
             local:TemplateSelector.ChangedItem="{x:Bind MainViewModel.UpdatedItem, Mode=OneWay}"
             ItemTemplateSelector="{StaticResource TemplateSelector}" />

  </Grid>
</Page>

Finally let the view model track the relevant property changes and set the changed property e.g. to a UpdatedItem property which binds to the attached behavior (see above):

public class MainViewModel : ViewModel, INotifyPropertyChanged
{
  public MainViewModel()
  {
    DataItems = new ObservableCollection<DataItem>();

    for (int index = 0; index < 10; index++)
    {
      DataItem newItem = new DataItem();

      // Listen to property changes that are relevant 
      // for the selection of the DataTemplate
      newItem.Activated += OnItemActivated;

      this.DataItems.Add(newItem);
    }
  }

  // Trigger the attached property by setting the property that binds to the behavior
  private void OnItemActivated(object sender, EventArgs e) => this.UpdatedItem = sender as DataItem

  public ObservableCollection<DataItem> DataItems { get; }

  private DataItem updatedItem;
  public DataItem UpdatedItem
  {
    get => this.updatedItem;
    set 
    {
      this.updatedItem = value;
      OnPropertyChanged();
    }
  }
}
BionicCode
  • 1
  • 4
  • 28
  • 44
  • This would update the DataTemplates for all items in the control though, yes? As I said in my post I've tried this already and the performance impact is too great to go that route. – Sean Beanland Jul 28 '21 at 17:07
  • No, this only updates the container of the item that you select in the view model. – BionicCode Jul 28 '21 at 18:17
0

this only updates the container of the item that you select in the view model.

Yep, The DataItemTemplateSelector works when preparing items. it will not response the item's property change even if it has implement INotifyPropertyChanged interface, the better way is use IValueConverter to update the uielement base on the specific property.

For example

public class ImageConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        object img = null;

        switch (value.ToString())
        {
            case "horizontal":
                img = new BitmapImage(new Uri("ms-appx:///Assets/holder1.png"));
                break;
            case "vertical":
                img = new BitmapImage(new Uri("ms-appx:///Assets/holder2.png"));
                break;
            default:
                break;
        }

        return img;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

For please refer IValueConverter document.

Nico Zhu
  • 32,367
  • 2
  • 15
  • 36