How to have one view instance per viewmodel in tabcontrol?
12:44 22 Jul 2025

I have a TabControl showing views from ObservableCollection Projects. ProjectViewModel and ProjectVarioViewModel do implement IProjectViewModel. The first of each get one instance of its view, but the next ones of the same type will get the same view instance as the first, which is problematic since the first view then won't match the subsequent viewmodels.

How to get one view instance for each view model ?

Each project will customize the View extensively, and it does not make sense to destroy/rebuild the view every time I switch from project to another.

I remarked that without ItemContainerStyle, tabitem header and body will be set with a view, the header will get its unique instance, but the body will not, ie., the same view is used, which is problematic.

So, more precisely: why do I get only one instance of view for the viewmodels of the same type in the body, and one instance of view per viewmodel in the header ? And how to get one instance of view per viewmodel in the body ?

Repro: in the following implementation, each View instance will have its own UniqueID, to verify that each view has its own instance or not. To test this visually, create at least 2 projects of the same type, and verify that UniqueID of the view is the same.

Here I have VarioViewModels with different configurations (W,H), and because the View is of the same instance, I do not have the proper Grid configuration.

The core of the problem:


    
        
            
        
        
            
        
    
    
        
    

When I remove completely TabControl.ItemContainerStyle, I get 1 view instance per tabitem header, but only 1 view instance in in the content of the tabitem, as show in the picture below:

cropped no ItemContainerStyle

When I select a tabitem by clicking on the header, I get the correct change of viewmodel, but the view instance is confirmed to be the same for all viewmodel instance of the same type.

A full window code: MainWindow.xaml:


    
        
        
        
        
        
    
    
        
            
            
        
        
            
                
                
            
            
                
                    
                    
                
                
                    
                    
                
            
        

        
            
                
                    
                
                
                    
                
            
            
                
            
        
    

View models:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Input;

namespace MultiTab;

public interface IProjectViewModel : ICloneable
{
    public string ProjectTitle { get; set; }
    public float Margin { get; set; }
    public int W { get; }
    public int H { get; }
}

public class Project : IProjectViewModel
{
    public Project(string name) { ProjectTitle = name; }
    public float Margin { get; set; }
    public string ProjectTitle { get; set; }
    public int W { get; protected set; }
    public int H { get; protected set; }
    public object Clone() => MemberwiseClone();
    public override string ToString() => $"{ProjectTitle} {W}x{H}";
}

public class ProjectViewModel : Project
{
    public ProjectViewModel(string name) : base(name)
    {
        W = 2;
        H = 2;
    }
}

public class ProjectVarioViewModel : Project
{
    private Random _r = new();
    public ProjectVarioViewModel(string name) : base(name)
    {
        W = _r.Next(2, 16);
        H = _r.Next(2, 16);
    }
}

public enum ProjectType
{
    Triptych,
    Vario,
}

public class MainViewModel : INotifyPropertyChanged
{
    private IProjectViewModel? _selectedProject;
    private DelegateCommandListen? _newProjectCommand;
    private ICommand? _closeProjectCommand;

    private Dictionary _argToProjectType = new()
    {
        {"Triptych", ProjectType.Triptych},
        {"Vario", ProjectType.Vario},
    };

    public ObservableCollection Projects { get; } = new();

    public IProjectViewModel? SelectedProject
    {
        get => _selectedProject;
        set => SetField(ref _selectedProject, value);
    }

    public ICommand NewProjectCommand
    {
        get
        {
            return _newProjectCommand ?? (_newProjectCommand = new DelegateCommandListen(
                s =>
                {
                    var ptype = _argToProjectType[(string) s];
                    var name = $"{GetNewProjectTitle()} [{ptype.ToString()}]";
                    CreateNewProject(name, ptype);
                },
                s => true));
        }
    }
    
    public ICommand CloseProjectCommand => _closeProjectCommand ?? (_closeProjectCommand = new DelegateCommandListen(
        s =>
        {
            var closingvm = (IProjectViewModel)s;
            Projects.Remove(closingvm);
        },
        s =>  s is not null));

    private string GetNewProjectTitle() => $"new{Projects.Count}";

    public void CreateNewProject(string projectname, ProjectType ptype)
    {
        IProjectViewModel vm;
        switch (ptype)
        {
            case ProjectType.Triptych:
                vm = new ProjectViewModel(projectname);
                break;
            default:
            case ProjectType.Vario:
                vm = new ProjectVarioViewModel(projectname);
                break;
        }

        Projects.Add(vm);
        SelectedProject = vm;
    }

    #region inpc

    public event PropertyChangedEventHandler? PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null)
    {
        if (EqualityComparer.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    #endregion
}

Now views, PView and VarioView.

PView:


    
        
            
                
                
            
            
                
                
            
            
            
            
            
        
        
            
            
            
        
    

PView code behind:

using System.Windows.Controls;
using System.Xml;

namespace MultiTab
{
    /// 
    /// Interaction logic for TView.xaml
    /// 
    public partial class TView : UserControl
    {
        public TView()
        {
            InitializeComponent();
        }

        public UniqueId NameID { get; } = new();
    }
}

VarioView:


    
        
        
        
            
                
                
                
                    
                    x
                    
                
                
            
        
    

VarioView codebehind:

public partial class VarioView : UserControl
{
    private static Random _r = new ();

    private SolidColorBrush GetRnColor()
    {
        var rgb = new byte[3];
        _r.NextBytes(rgb);
        return new SolidColorBrush(Color.FromRgb(rgb[0], rgb[1], rgb[2]));
    }

    public ProjectVarioViewModel ViewModel
    {
        get => (ProjectVarioViewModel)DataContext;
        set => DataContext = value;
    }

    public UniqueId NameID { get; } = new();

    public VarioView()
    {
        InitializeComponent();
        Loaded += ApplyGrid;
    }

    private void ApplyGrid(object sender, RoutedEventArgs e)
    {
        var totCols = ViewModel.W;
        var totRows = ViewModel.H;

        var zoneWLen = new GridLength(1, GridUnitType.Star);

        for (int x = 0; x < totCols; x++)
            GridZonesContainer.ColumnDefinitions.Add(new ColumnDefinition { Width = zoneWLen });
        for (int y = 0; y < totRows; y++)
            GridZonesContainer.RowDefinitions.Add(new RowDefinition { Height = zoneWLen });

        for (int x = 0; x < totCols; x++)
        for (int y = 0; y < totRows; y++)
            AddZone(x, y);
    }

    private void AddZone(int gridx, int gridy)
    {
        var mz = new Rectangle
        {
            Fill = GetRnColor()
        };
        GridZonesContainer.Children.Add(mz);
        Grid.SetRow(mz, gridy);
        Grid.SetColumn(mz, gridx);
    }
}

The utility DelegateCommandListen:

using System.ComponentModel;
using System.Linq.Expressions;
using System.Windows.Input;

namespace MultiTab;

/// 
/// Implementation of ICommand with listening to 1+ properties change (INPC)
/// ICommand Zcommand = new DelegateCommandListen {
/// ExecuteDelegate = ZExecuteCommand,
/// CanExecuteDelegate =  CanExecuteZCommand }.ListenOn(this, o => o.INPCpropFromVM);;
/// 
public class DelegateCommandListen : ICommand
{
    private readonly List _controlEvent;
    private Action _executeDelegate;

    /// 
    /// Implementation of ICommand with listening to 1+ properties change (INPC)
    /// ICommand Zcommand = new DelegateCommandListen {
    /// ExecuteDelegate = ZExecuteCommand,
    /// CanExecuteDelegate =  CanExecuteZCommand }.ListenOn(this, o => o.INPCpropFromVM);;
    /// 
    public DelegateCommandListen()
    {
        _controlEvent = new List();
    }

    /// 
    /// Implementation of ICommand with listening to 1+ properties change (INPC)
    /// 
    /// 
    /// 
    public DelegateCommandListen(Action executeDelegate, Predicate canExecuteDelegate)
    {
        _controlEvent = new List();
        ExecuteDelegate = executeDelegate;
        CanExecuteDelegate = canExecuteDelegate;
    }

    public List PropertiesToListenTo { get; set; }

    public Predicate CanExecuteDelegate { get; set; }

    public Action ExecuteDelegate
    {
        get { return _executeDelegate; }
        set
        {
            _executeDelegate = value;
            ListenForNotificationFrom((INotifyPropertyChanged)_executeDelegate.Target);
        }
    }

    public void RaiseCanExecuteChanged()
    {
        if (_controlEvent is { Count: > 0 })
            _controlEvent.ForEach(ce => { ((EventHandler)ce.Target)?.Invoke(null, EventArgs.Empty); });
    }

    public DelegateCommandListen ListenOn
        (TObservedType viewModel, Expression> propertyExpression)
        where TObservedType : INotifyPropertyChanged
    {
        var propertyName = GetPropertyName(propertyExpression);
        viewModel.PropertyChanged += (s, e) =>
        {
            if (e.PropertyName == propertyName) RaiseCanExecuteChanged();
        };
        return this;
    }

    public void ListenForNotificationFrom(TObservedType viewModel)
        where TObservedType : INotifyPropertyChanged
    {
        viewModel.PropertyChanged += (s, e) => RaiseCanExecuteChanged();
    }

    private static string GetPropertyName(Expression> expression)
        where T : INotifyPropertyChanged
    {
        var lambda = expression as LambdaExpression;
        var memberInfo = GetMemberExpression(lambda).Member;
        return memberInfo.Name;
    }

    private static MemberExpression GetMemberExpression(LambdaExpression lambda)
    {
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression body)
        {
            var unaryExpression = body;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
            memberExpression = lambda.Body as MemberExpression;
        return memberExpression;
    }

    #region ICommand Members

    public bool CanExecute(object parameter) => CanExecuteDelegate == null || CanExecuteDelegate(parameter);

    public event EventHandler CanExecuteChanged
    {
        add
        {
            CommandManager.RequerySuggested += value;
            _controlEvent.Add(new WeakReference(value));
        }
        remove
        {
            CommandManager.RequerySuggested -= value;
            _controlEvent.Remove(_controlEvent.Find(r => (EventHandler)r.Target == value));
        }
    }

    public void Execute(object parameter) => ExecuteDelegate?.Invoke(parameter);

    #endregion
}

Git repo: MultiTab

c# wpf xaml