In
Part 1[
^] of your questions, we addressed how to:
1. Pass text to a
ViewModel
when a
Button
is pressed
2. Dynamically add new
TabItem
s with specfic
TabItem
Header names when the
Button
is passed.
Now you require:
A. How do I display content for the TabItem
?
Dynamic content in the
TabItem
's Cotent area. We will need to modify the first answer with a
Model
to hold the
TabItem
Header text and the
ViewModel
(View data context) for the TabItem Content area.
Why the
ViewModel
and not the View? MVVM is using data binding, ie: Data first.
How Does passing the
ViewModel
and not the View in the binding work? We use
DataTemplate
s to link the
View
to the
ViewModel
. The context of the binding of the
ContentControl
will look for a
DataTemplate
for the
ViewModel
(DataContext).
B. How do I Select the Tab
and prevent dupelicates?
We use the
SelectedIndex
to select the active tab.
To prevent duplicates we simply check the collection holding the
TabModel
s to see if it already exists.
Now down to the code.
First we need to Notify the data binding of property changes using the
INotifyPropertyChanged
interface. I have a base
ObservableObject
class that I like to use:
public abstract class ObservableObject : INotifyPropertyChanged
{
protected bool Set<T>(Expression<Func<T>> propertyExpression,
ref T field, T newValue)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
return false;
field = newValue;
OnPropertyChanged(propertyExpression);
return true;
}
protected bool Set<T>(string? propertyName, ref T field, T newValue)
{
if (EqualityComparer<T>.Default.Equals(field, newValue))
return false;
field = newValue;
OnPropertyChanged(propertyName);
return true;
}
protected bool Set<T>(ref T field, T newValue,
[CallerMemberName] string? propertyName = null)
=> Set(propertyName, ref field, newValue);
public event PropertyChangedEventHandler? PropertyChanged;
protected static string GetPropertyName<T>
(Expression<Func<T>> propertyExpression)
{
if (propertyExpression == null)
throw new ArgumentNullException(nameof(propertyExpression));
if (propertyExpression.Body is not MemberExpression body)
throw new ArgumentException("Invalid argument",
nameof(propertyExpression));
if (body.Member is not PropertyInfo member)
throw new ArgumentException("Argument is not a property",
nameof(propertyExpression));
return member.Name;
}
public virtual void OnPropertyChanged
([CallerMemberName] string? propertyName = null)
=> PropertyChanged?.Invoke(this,
new PropertyChangedEventArgs(propertyName));
public virtual void OnPropertyChanged<T>
(Expression<Func<T>> propertyExpression)
{
if (PropertyChanged == null) return;
string propertyName = GetPropertyName(propertyExpression);
if (string.IsNullOrEmpty(propertyName)) return;
OnPropertyChanged(propertyName);
}
}
We will also need to raise
PropertyChanged
events from the
ViewModel
. This is a simplified version of a
ViewModelBase
class:
public abstract class ViewModelBase : ObservableObject, IDisposable
{
~ViewModelBase() => Dispose();
public virtual void Dispose()
{
}
}
Next we need a
TabModel
to hold the data for each tab:
public class TabModel : ObservableObject
{
private string? header;
private TabViewModelBase? content;
public string? Header
{
get => header;
set => Set(ref header, value);
}
public TabViewModelBase? Content
{
get => content;
set =>Set(ref content, value);
}
}
Next We require a
ViewModel
for the Tabs. Here I use a generic
ViewModel
. You would Use what you need. The key thing is that you will require a base class or interface for the
TabModel
.
public abstract class TabViewModelBase
{
public TabViewModelBase(string name) => Name = name;
public string? Name { get; init; }
}
public class Tab1ViewModel : TabViewModelBase
{
public Tab1ViewModel(string name) : base(name) { }
}
public class Tab2ViewModel : TabViewModelBase
{
public Tab2ViewModel(string name) : base(name) { }
}
public class Tab3ViewModel : TabViewModelBase
{
public Tab3ViewModel(string name) : base(name) { }
}
Next we need to modify the Host
ViewModel
,
MainViewModel
in this example, to use the new
TabModel
:
public class MainViewModel : ViewModelBase
{
private int selectedIndex = -1;
public MainViewModel()
=> ButtonClickCommand = new ButtonClickCommand(OnClicked);
public int SelectedIndex
{
get => selectedIndex;
set => Set(ref selectedIndex, value);
}
public ICommand? ButtonClickCommand { get; }
public ObservableCollection<TabModel> Tabs { get; } = new();
private void OnClicked(string buttonName)
{
if (Tabs.Any(tab => tab.Header == buttonName))
{
return;
}
string text = $"This is {buttonName}'s tab...";
Tabs.Add(new TabModel
{
Header = buttonName,
Content = buttonName switch
{
"Button 1" => new Tab1ViewModel(text),
"Button 2" => new Tab2ViewModel(text),
_ => new Tab3ViewModel(text),
}
});
SelectedIndex = Tabs.Count - 1;
}
}
The
OnClicked
method answers two key questions - How to Select the
Tab
and how to stop duplicates.
Now we need to answer the final question: How do we show the correct
Content
for the
TabItem
. To do this, we need to use
DataTemplate
s and bind a
ContentControl
to the
ViewModel
of the selected
TabModel
.
<StackPanel Orientation="Horizontal">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Padding" Value="20 10" />
<Setter Property="Margin" Value="5 10"/>
</Style>
</StackPanel.Resources>
<Button Content="Button 1"
Command="{Binding ButtonClickCommand}"
CommandParameter="Button 1" />
<Button Content="Button 2"
Command="{Binding ButtonClickCommand}"
CommandParameter="Button 2" />
<Button Content="Button 3"
Command="{Binding ButtonClickCommand}"
CommandParameter="Button 3" />
</StackPanel>
<TabControl Grid.Row="1"
ItemsSource="{Binding Tabs}"
SelectedIndex="{Binding SelectedIndex}">
<TabControl.Resources>
<DataTemplate DataType="{x:Type viewmodels:Tab1ViewModel}">
<views:TabView1 />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:Tab2ViewModel}">
<views:TabView2 />
</DataTemplate>
<DataTemplate DataType="{x:Type viewmodels:Tab3ViewModel}">
<views:TabView3 />
</DataTemplate>
</TabControl.Resources>
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock
Text="{Binding Header}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding Content}" />
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
This is where the magic happens. The
Data Binding
will initialize the
View
and set the
ViewModel
to the
DataContext
of the
View
as the
ViewModel
is already initialized. There is no need to set the
DataContext
of the
View
manually.
Lastly, Here is a sample of one of the
Views
. Note, the
DataContext
is not set as it happens in the
Data Binding
.
public partial class TabView1 : UserControl
{
public TabView1()
{
InitializeComponent();
}
}
<UserControl x:Class="CommandBinding.Views.TabView1"
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:CommandBinding.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid Background="Green">
<TextBlock Text="{Binding Name}" Foreground="White" />
</Grid>
</UserControl>
It's a bit long, article length actually, but I wanted to ensure that you understood how it worked.
Enjoy!