Search code examples
c#wpfdata-bindingdatatemplateselector

Data template selector only hit once and not on update of Full-Property


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);
     }
}

Solution

  • 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>