Reputation: 25929
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
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.
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
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