Spook
Spook

Reputation: 25929

How to prevent WPF's ContentControl from reusing DataTemplates?

I stumbled upon the famous TabControl virtuality problem. I thought of replacing the TabControl with styled list (to present tabs) and ContentControl (to present content of a tab). However, it seems like ContentControl has the same behavior of reusing DataTemplates if two contents share their type.

Is there a way to force ContentControl to instantiate separate DataTemplates for all displayed items?


Edit: Example

Say, that we have the following UserControl acting as DataTemplate for document view model:

DocumentControl.xaml:

<UserControl x:Class="ControlTemplateProblem.DocumentControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:ControlTemplateProblem"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800"
             Loaded="UserControl_Loaded"
             Unloaded="UserControl_Unloaded"
             x:Name="Main">
    <StackPanel Orientation="Vertical">
        <TextBlock Text="{Binding Index}" />
        <TextBlock Text="{Binding ElementName=Main, Path=StoredIndex}" />
    </StackPanel>
</UserControl>

DocumentControl.xaml.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ControlTemplateProblem
{
    /// <summary>
    /// Interaction logic for DocumentControl.xaml
    /// </summary>
    public partial class DocumentControl : UserControl, INotifyPropertyChanged
    {
        private bool initialized = false;
        private string storedIndex;

        public DocumentControl()
        {
            InitializeComponent();
        }

        private void UserControl_Loaded(object sender, RoutedEventArgs e)
        {
            StoredIndex = ((ItemViewModel)DataContext).Index;
        }

        private void UserControl_Unloaded(object sender, RoutedEventArgs e)
        {
            StoredIndex = "(detached)";
        }

        public string StoredIndex
        {
            get => storedIndex;
            set
            {
                storedIndex = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StoredIndex)));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Main window's view model looks like this:

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ControlTemplateProblem
{
    public class MainViewModel : INotifyPropertyChanged
    {
        private ItemViewModel selectedItem;

        public MainViewModel()
        {
            selectedItem = Items.First();
        }

        public ObservableCollection<ItemViewModel> Items { get; } = new ObservableCollection<ItemViewModel> {
            new ItemViewModel(),
            new ItemViewModel(),
            new ItemViewModel()
        };

        public ItemViewModel SelectedItem
        {
            get => selectedItem;
            set
            {
                selectedItem = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem)));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

MainWindow.xaml

<Window x:Class="ControlTemplateProblem.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:ControlTemplateProblem"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    <StackPanel Orientation="Vertical">
        <ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
        <ContentControl Content="{Binding SelectedItem}">
            <ContentControl.ContentTemplate>
                <DataTemplate DataType="{x:Type local:ItemViewModel}">
                    <local:DocumentControl />
                </DataTemplate>
            </ContentControl.ContentTemplate>
        </ContentControl>
    </StackPanel>
</Window>

I expect the document to display two equal indices - one coming directly from document's viewmodel and second extracted from the document's viewmodel by the control (this is simulation of performing some initializations on the control upon attaching viewmodel). After starting the application and choosing different items will yield always ( 0) and the 0 is because control was initialized only once and ContentControl reuses the DataTemplate.

Alternative question to the one I asked would be: how to reliably call code just after DataContext was set and just before DataContext is about to change? Then I'd be able to (again) reliaby initialize and deinitialize visuals for the specific viewmodel.

Upvotes: 5

Views: 1266

Answers (2)

AlphaModder
AlphaModder

Reputation: 3386

If @TomM's answer didn't work for you, add the x:Shared="False" attribute to your DataTemplate and then also add a dummy DataTemplateSelector whose SelectTemplate method always returns null to the ContentControl's ContentTemplateSelector property. This will prevent it from reusing DataTemplate instances in the fashion described without affecting any other functionality.

Why does this work?

Whether a ContentControl reuses DataTemplate instances is based on the value of mismatch computed in ContentPresenter.OnContentChanged. If mismatch is false the current template will be reused, otherwise it will be recreated. In the case of template selection based on DataType, the method reaches this line which sets mismatch to true iff the old and new Content values have different types, matching the behavior you observed. However, if a ContentTemplateSelector is present, mismatch is unconditionally set to true on this line.

As for the dummy DataTemplateSelector, it doesn't break anything because ContentPresenter.ChooseTemplate falls back to a (private) DefaultTemplateSelector if ContentTemplateSelector is null or if it returns null - so you'll get the same template selection algorithm as with no DataTemplateSelector at all.

If x:Shared is not false, the above is all still true, but it does not achieve the desired effect because the DataTemplate is cached at a lower level of the system.

Upvotes: 1

TomM
TomM

Reputation: 21

I had a similar problem and found that by setting the x:Shared attribute of the DataTemplate to false, a new instance of my view would be created for each viewmodel. According to the documentation for x:Shared, it can only be applied in a ResourceDictionary, however, so you might have to change your view code to something like:

<Window x:Class="ControlTemplateProblem.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
            xmlns:local="clr-namespace:ControlTemplateProblem"
            mc:Ignorable="d"
            Title="MainWindow" Height="450" Width="800">
    <Window.Resources>
        <DataTemplate x:Shared="False" DataType="{x:Type local:ItemViewModel}">
            <local:DocumentControl />
        </DataTemplate>
    </Window.Resources>
    <StackPanel Orientation="Vertical">
        <ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
        <ContentControl Content="{Binding SelectedItem}"/>
    </StackPanel>
</Window>

Upvotes: 2

Related Questions