Search code examples
c#wpfxamlmvvm

wpf xaml MVVM inheritance with multiple ContentPresenter


I am rewriting import masks that have a lot in common, so I want (and must) use inheritance.

I have a basic UserControl with all common controls: (I have left out the grid definitions)

BaseClass.xaml

<UserControl x:Class="BaseImport.BaseClass"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  <UserControl.Template>
    <ControlTemplate TargetType="UserControl">
      <Grid>
        <Border Grid.Row="0" Grid.Column="0">
          <StackPanel>
            <Label Content="Text1:"/>
            <ComboBox Name="cbText1" MinWidth="80"/>
          </StackPanel>
        </Border>
        
        <Border Grid.Row="0" Grid.Column="1">
          <StackPanel>
            <Label Content="Text2:"/>
            <ComboBox Name="cbText2" MinWidth="80"/>
          </StackPanel>
        </Border>
        
        <Border Grid.Row="0" Grid.Column="2">
          <StackPanel>
            <ContentPresenter ContentSource="Content"/> <!-- ContentSource="Content" is the default-->
          </StackPanel>
        </Border>

        <!-- next Row -->
        <Border Grid.Row="1" Grid.Column="0">
          <StackPanel>
            <Label Content="Text3:"/>
            <TextBox Name="tbText3" TextWrapping="Wrap" Text="" MinWidth="80" VerticalAlignment="Center"/>
          </StackPanel>
        </Border>

        <Border Grid.Row="1" Grid.Column="1">
          <StackPanel>
            <ContentPresenter/> 
          </StackPanel>
        </Border>
      </Grid>
    </ControlTemplate>
  </UserControl.Template>
</UserControl>

This is a kind of Template that gets "used" like this:

MainWindow.xaml (just for demonstration a mainwindow)

<Window x:Class="zzz.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:my="clr-namespace:BaseImport;assembly=BaseImport"
        mc:Ignorable="d"
        Title="MainWindow" Height="280" Width="600">
  <my:BaseClass>
    <StackPanel>
      <Label Content="Test:"/>
      <ComboBox ItemsSource="{Binding TestTyps}" MinWidth="80"/>
    </StackPanel>
  </my:BaseClass>
</Window>

MainWindow.xaml.cs

using WpfApp1.ViewModel;

namespace zzz
{
  public partial class MainWindow : Window
  {
    public MainWindow()
    {
      InitializeComponent();

      this.DataContext = new MainViewModel();
    }
  }
}

and to wrap it up MainViewModel.cs:

namespace WpfApp1.ViewModel
{
  public class MainViewModel : INotifyPropertyChanged
  {
    public event PropertyChangedEventHandler? PropertyChanged;

    public string[] TestTyps { get { return new string[] { "abc", "123", "xyz" }; } }

  }
}

If I have one ContentPresenter everything works fine. But in the BaseClass I have two, potentially more. Like this only the "last" Presenter gets populated. And in MainWindow.xaml can only be one declared.

How can I put more Content in MainWindow.xaml?

How can I select the right one?

Thanks

The red rectangle is were the second presenter is located (row 1, column 1) but I want it to be were the arrow points (row 0, column 2).

I want another control in place of the red rectangle also declared in MainWindow.xaml.


Solution

  • The stuff you put directly in the <my:BaseClass> tags is the contentProperty which can be only one.

    Why only the last ContentPresenter shows it? Because each VisualElement can only have one parent, so the last one claiming it wins.

    However you can create as many properties as you want.

    <UserControl x:Class="BaseImport.BaseClass"
                 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
      <UserControl.Template>
        <ControlTemplate TargetType="UserControl">
          <Grid>
            <Border Grid.Row="0" Grid.Column="0">
              <StackPanel>
                <Label Content="Text1:"/>
                <ComboBox Name="cbText1" MinWidth="80"/>
              </StackPanel>
            </Border>
            
            <Border Grid.Row="0" Grid.Column="1">
              <StackPanel>
                <Label Content="Text2:"/>
                <ComboBox Name="cbText2" MinWidth="80"/>
              </StackPanel>
            </Border>
            
            <Border Grid.Row="0" Grid.Column="2">
              <StackPanel>
                <ContentPresenter ContentSource="FirstContent"/>
              </StackPanel>
            </Border>
    
            <!-- next Row -->
            <Border Grid.Row="1" Grid.Column="0">
              <StackPanel>
                <Label Content="Text3:"/>
                <TextBox Name="tbText3" TextWrapping="Wrap" Text="" MinWidth="80" VerticalAlignment="Center"/>
              </StackPanel>
            </Border>
    
            <Border Grid.Row="1" Grid.Column="1">
              <StackPanel>
                <ContentPresenter ContentSource="SecondContent"/> 
              </StackPanel>
            </Border>
          </Grid>
        </ControlTemplate>
      </UserControl.Template>
    </UserControl>
    
    <my:BaseClass>
      <my:BaseClass.FirstContent>
        <StackPanel>
          <Label Content="Test:"/>
          <ComboBox ItemsSource="{Binding TestTyps}" MinWidth="80"/>
        </StackPanel>
      <my:BaseClass.FirstContent>
      <my:BaseClass.SecondContent>
        <StackPanel>
          <Label Content="Test10:"/>
          <TextBox Text="{Binding Whatever}" MinWidth="80"/>
        </StackPanel>
      <my:BaseClass.SecondContent>
    </my:BaseClass>