In a C# WPF project, I have a ContentControl
in the MainWindow
. I bind to the whole MainViewModel
which is my DataContext
. Only bind on my StreamDeckViewModel
StreamDeck
didn't work.
My DataTemplateSelector
is just hit once, but not when I change the value of the InputMode
in my KeyViewModel
. The KeyViewModel
is in the StreamDeckViewModel
which is in the MainViewModel
. I use the NuGet CommunityToolkit.mvvm, all my view models inherit from ObservableObject
and I use Full-Properties with their backing fields.
Markup in my MainWindow.xaml
:
<ContentControl Content="{Binding}"
ContentTemplateSelector="{StaticResource FromModeTemplateSelector}" />
<Window.Resources>
<DataTemplate x:Key="OpeningModeTemplate">
<ScrollViewer>
<DockPanel VerticalAlignment="Top">
<ComboBox
DockPanel.Dock="Top"
ItemsSource="{Binding StreamDeck.SelectedKey.ActionChoices}"
SelectedIndex="{Binding StreamDeck.SelectedKey.SelectedActionIndex}" />
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="{x:Static resx:Resources.ChooseFilePath}" />
<Button
Width="30"
Background="Transparent"
BorderBrush="Transparent"
Command="{Binding SelectFileToOpenCommand}"
DockPanel.Dock="Right">
<Image Source="/EmbeddedResourcesIn/file.png" />
</Button>
</StackPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="{x:Static resx:Resources.ChooseFolderPath}" />
<Button
Width="30"
Background="Transparent"
BorderBrush="Transparent"
Command="{Binding SelectFolderToOpenCommand}"
DockPanel.Dock="Right">
<Image Source="/EmbeddedResourcesIn/folder.png" />
</Button>
</StackPanel>
<ScrollViewer
DockPanel.Dock="Top"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Disabled">
<TextBlock Text="{Binding StreamDeck.SelectedKey.Path}" />
</ScrollViewer>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="{x:Static resx:Resources.ActualizeImage}" />
<CheckBox IsChecked="{Binding Path=StreamDeck.SelectedKey.ActualizeImage, Mode=TwoWay}" />
</StackPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="{x:Static resx:Resources.ChooseImagePath}" />
<Button
Width="30"
Background="Transparent"
BorderBrush="Transparent"
Command="{Binding SelectImagePathCommand}"
DockPanel.Dock="Right">
<Image Source="/EmbeddedResourcesIn/image.png" />
</Button>
</StackPanel>
<ScrollViewer
DockPanel.Dock="Top"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Disabled">
<TextBlock Text="{Binding StreamDeck.SelectedKey.ImagePath}" />
</ScrollViewer>
<Label Content="{x:Static resx:Resources.TypeText}" DockPanel.Dock="Top" />
<TextBox DockPanel.Dock="Top" Text="{Binding StreamDeck.SelectedKey.Text.Content}" />
<Label Content="{x:Static resx:Resources.ChooseTextColor}" DockPanel.Dock="Top" />
<ComboBox
DockPanel.Dock="Top"
ItemsSource="{Binding StreamDeck.SelectedKey.Text.ColorChoices}"
SelectedIndex="{Binding StreamDeck.SelectedKey.Text.SelectedColorIndex}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Rectangle
Width="10"
Height="10"
Fill="{Binding Code}" />
<TextBlock
Margin="5"
VerticalAlignment="Center"
Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DockPanel>
</ScrollViewer>
</DataTemplate>
<DataTemplate x:Key="ShortcutModeTemplate">
<ScrollViewer>
<DockPanel VerticalAlignment="Top">
<ComboBox
DockPanel.Dock="Top"
ItemsSource="{Binding StreamDeck.SelectedKey.ActionChoices}"
SelectedIndex="{Binding StreamDeck.SelectedKey.SelectedActionIndex}" />
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="{x:Static resx:Resources.EnterShortcut}" />
<Button
Width="30"
Background="Transparent"
BorderBrush="Transparent"
Command="{Binding EnterShortcutCommand}"
DockPanel.Dock="Right">
<Image Source="/EmbeddedResourcesIn/shortcutMiniBlack.png" />
</Button>
</StackPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="{x:Static resx:Resources.ActualizeImage}" />
<CheckBox IsChecked="{Binding Path=StreamDeck.SelectedKey.ActualizeImage, Mode=TwoWay}" />
</StackPanel>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
<Label Content="{x:Static resx:Resources.ChooseImagePath}" />
<Button
Width="30"
Background="Transparent"
BorderBrush="Transparent"
Command="{Binding SelectImagePathCommand}"
DockPanel.Dock="Right">
<Image Source="/EmbeddedResourcesIn/image.png" />
</Button>
</StackPanel>
<ScrollViewer
DockPanel.Dock="Top"
HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Disabled">
<TextBlock Text="{Binding StreamDeck.SelectedKey.ImagePath}" />
</ScrollViewer>
<Label Content="{x:Static resx:Resources.TypeText}" DockPanel.Dock="Top" />
<TextBox DockPanel.Dock="Top" Text="{Binding StreamDeck.SelectedKey.Text.Content}" />
<Label Content="{x:Static resx:Resources.ChooseTextColor}" DockPanel.Dock="Top" />
<ComboBox
DockPanel.Dock="Top"
ItemsSource="{Binding StreamDeck.SelectedKey.Text.ColorChoices}"
SelectedIndex="{Binding StreamDeck.SelectedKey.Text.SelectedColorIndex}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<Rectangle
Width="10"
Height="10"
Fill="{Binding Code}" />
<TextBlock
Margin="5"
VerticalAlignment="Center"
Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</DockPanel>
</ScrollViewer>
</DataTemplate>
<dts:FromModeTemplateSelector
x:Key="FromModeTemplateSelector"
OpeningModeTemplate="{StaticResource OpeningModeTemplate}"
ShortcutModeTemplate="{StaticResource ShortcutModeTemplate}" />
</Window.Resources>
Code in my KeyViewModel
:
public InputModeBase InputMode { get => inputMode; set => SetProperty(ref inputMode, value); }
private InputModeBase inputMode = new OpeningMode();
public int SelectedActionIndex
{
get => selectedActionIndex;
set
{
SetProperty(ref selectedActionIndex, value);
if (selectedActionIndex == Constants.OPEN_INDEX)
{
InputMode = new OpeningMode();
}
else if (selectedActionIndex == Constants.SHORTCUT_INDEX)
{
InputMode = new ShortcutMode();
}
}
}
private int selectedActionIndex = 0;
My DataTemplateSelector
:
public class FromModeTemplateSelector : DataTemplateSelector
{
public System.Windows.DataTemplate OpeningModeTemplate { get; set; } = new();
public System.Windows.DataTemplate ShortcutModeTemplate { get; set; } = new();
public override System.Windows.DataTemplate SelectTemplate(object item, System.Windows.DependencyObject container)
{
if (container is System.Windows.FrameworkElement && item != null && item is MainViewModel mainViewModel)
{
if (mainViewModel.StreamDeck.SelectedKey.InputMode is OpeningMode)
{
return OpeningModeTemplate;
}
else if (mainViewModel.StreamDeck.SelectedKey.InputMode is ShortcutMode)
{
return ShortcutModeTemplate;
}
}
return base.SelectTemplate(item, container);
}
}
The DataTemplateSelector
is only invoked when the layout has been invalidated and templates must be reapplied. Of course, changing property values in some data model does not invalidate the layout of a control (except the dependency property that binds to the data model's property is explicitly configured to do so, which is not the default configuration).
If the DataTemplate
depends on the value of a data model property then you can bind the ContentControl.ContentTemplate
property to that property and use a IValueConverter
to return the appropriate DataTemplate
based on the current value.
You can also use a DataTrigger
to select the DataTemplate
:
<ContentControl Content="{Binding}">
<ContentControl.Style>
<Style targetType="ContentControl">
<Style.Triggers>
<DataTrigger Binding="{Binding SelectedActionIndex}"
Value="{x:Static Constants.OPEN_INDEX}">
<Setter Property="ContentTemplate"
Value="{StaticResource OpeningModeTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding SelectedActionIndex}"
Value="{x:Static Constants.SHORTCUT_INDEX}">
<Setter Property="ContentTemplate"
Value="{StaticResource ShortcutModeTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
In general, we can say that a DataTemplateSelector
is an inadequate choice when we have to select a DataTemplate
based on property values (instead of data type and additional conditions).
If you can select based on the data type, then an implicit DataTemplate
will do it. If you have to select based on property values, then a DataTrigger
or IValueConverter
must be used. DataTemplateSelector
is rarely needed.
But my recommended solution would be to introduce a data model for each layout and bind the ContentControl
directly to these types. When using implicit DataTemplate
the ContentControl
will automatically use the correct template. This solution eliminates the DataTemplateSelector
and improves the template bindings (as it will correct the template's DataContext
which results in shortening the depth of binding path nesting).
In your case the main data context of each DataTemplate
appears to be the KeyViewModel
. Therefore, you should redesign this type, and the owning MainViewModel
, in order to be able to provide the differently typed data contexts. You can use inheritance to avoid duplicate code in the new KeyViewModel
hierarchy:
A good class design solves a lot of problems and simplifies the interaction between classes. As i don't know any details I can only make a suggestion based on what you have posted. However, the example should show the idea.
ModeData.cs
abstract class KeyViewModel : INotifyPropertyChanged
{
// Common implementation
public InputModeBase InputMode { get; }
protected KeyViewModel(InputModeBase inputMode)
=> this.InputMode = inputMode;
}
ShortcutModeKeyViewModel.cs
class ShortcutModeKeyViewModel : KeyViewModel
{
public ShortcutModeKeyViewModel() : base(new ShortcutMode())
{}
}
OpeningModeKeyViewModel.cs
class OpeningModeKeyViewModel : KeyViewModel
{
public OpeningModeKeyViewModel() : base(new OpeningMode())
{}
}
MainViewModel.cs
class MainViewModel : INotifyPropertyChanged
{
public MainViewModel()
=> this.SelectedActionIndex = Constants.OPEN_INDEX;
public KeyViewModel CurrentKeyViewModel { get; set; }
private int selectedActionIndex = -1;
public int SelectedActionIndex
{
get => selectedActionIndex;
set
{
SetProperty(ref this.selectedActionIndex, value);
if (this.SelectedActionIndex == Constants.OPEN_INDEX)
{
this.CurrentKeyViewModel = new OpeningModeKeyViewModel();
}
else if (this.SelectedActionIndex == Constants.SHORTCUT_INDEX)
{
this.CurrentKeyViewModel = new ShortcutModeKeyViewModel();
}
}
}
}
MainWindow.xaml
<Window>
<Window.Resources>
<!-- Implicit DataTemplate (without x:Key) -->
<DataTemplate DataType="{x:Type OpeningModeKeyViewModel}">
<StackPanel>
<!-- Element that binds to the current KeyViewModel (the DataContext of this DataTemplate) -->
<ComboBox ItemsSource="{Binding ActionChoices}"
SelectedIndex="{Binding SelectedActionIndex}" />
<!-- Element that binds to the MainViewModel -->
<Button Command="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.SelectFileToOpenCommand}" />
</StackPanel>
</DataTemplate>
<!-- Implicit DataTemplate (without x:Key) -->
<DataTemplate DataType="{x:Type ShortcutModeKeyViewModel}">
<StackPanel>
<!-- Element that binds to the current KeyViewModel (the DataContext of this DataTemplate) -->
<ComboBox ItemsSource="{Binding ActionChoices}"
SelectedIndex="{Binding SelectedActionIndex}" />
<!-- Element that binds to the MainViewModel -->
<Button Command="{Binding RelativeSource={RelativeSource AncestorType=ContentControl}, Path=DataContext.SelectFileToOpenCommand}" />
</StackPanel>
</DataTemplate>
</Window.Resources>
<ContentControl Content="{Binding CurrentKeyViewModel}" />
</Window>