Click here to Skip to main content
15,881,600 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
I have three buttons. I want to dynamically add a new tab by pressing each button. Each button has its specific content.

What I have tried:

I tried the code proposed by the user Graeme_Grant in How to pass a string from view to the viewmodel in the MVVM pattern?[^] but I'm interested to learn adding these functionalities too.
Posted
Updated 8-May-22 20:37pm
v4

1 solution

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 TabItems 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 DataTemplates 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 TabModels 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:
C#
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:
C#
public abstract class ViewModelBase : ObservableObject, IDisposable
{
    ~ViewModelBase() => Dispose();

    public virtual void Dispose()
    {
        /* to be used if needed */
    }
}

Next we need a TabModel to hold the data for each tab:
C#
public class TabModel : ObservableObject
{
    private string? header;
    private TabViewModelBase? content;

    public string? Header
    {
        get => header;
        set => Set(ref header, value);
    }

    // common base class for each tab
    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.
C#
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) { /* skip */ }
}
public class Tab2ViewModel : TabViewModelBase
{
    public Tab2ViewModel(string name) : base(name) { /* skip */ }
}
public class Tab3ViewModel : TabViewModelBase
{
    public Tab3ViewModel(string name) : base(name) { /* skip */ }
}

Next we need to modify the Host ViewModel, MainViewModel in this example, to use the new TabModel:
C#
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<string> Tabs { get; } = new();
    public ObservableCollection<TabModel> Tabs { get; } = new();

    private void OnClicked(string buttonName)
    {
        //Tabs.Add(buttonName);

        if (Tabs.Any(tab => tab.Header == buttonName))
        {
            // already added, so add decision logic here... eg: warn user
            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 DataTemplates and bind a ContentControl to the ViewModel of the selected TabModel.
XML
<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>
        <!-- this is the header template-->
        <DataTemplate>
            <TextBlock
                Text="{Binding Header}" />
        </DataTemplate>
    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>
        <!-- this is the body of the TabItem template-->
        <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.
C#
public partial class TabView1 : UserControl
{
    public TabView1()
    {
        InitializeComponent();
    }
}

XML
<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!
 
Share this answer
 
v6
Comments
Code4Ever 9-May-22 4:25am    
Thanks.
1- Did you code that generic ObservableObject class? I mean, is it done by you or is it a generic code used by developers?
2- I as a beginner in WPF prefer to use behind code approach. Can I achieve the same result as MVVM?
Graeme_Grant 9-May-22 4:30am    
1. Bit of both
2.Yes. Mind you, it has been years since I have done this - once you get use to it, MVVM simpifies WPF programming. Just set the DataConext to "this". As for the DataTemplateBinding, if you use the View in the Model class, it should work, just remove the DataTemplate code and leave the binding as-is.

FWIW, binding to the code-behind is not MVVM.
Code4Ever 9-May-22 4:40am    
Is there any difference in the performance of a big application coded with an MVVM pattern and the same application coded with a traditional behind-code approach?
Graeme_Grant 9-May-22 4:44am    
It's more about the decoupling to reduce complexity and increase code maintainability & expansion. Also makes it easy to write unit tests for automated testing.

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900