Search code examples
wpfmvvmdatatemplatetabcontroldatacontext

WPF MVVM: Setting DataContext of Tab Views


I have experienced a weird binding behavior that is described here. I did a lot of troubleshooting and I came to the conclusion that the most likely problem lies at how I set the DataContext of each of the tab's view.

I have a TabControl whose ItemsSource is bound to a list of ViewModels.

MainView:
<TabControl ItemsSource="{Binding TabList}">
    <TabControl.Resources>
        <DataTemplate DataType="{x:Type vm:Tab1ViewModel}">
            <v:Tab1 />
        </DataTemplate>
    </TabControl.Resources>
...
</TabControl>

MainViewModel:
public ObservableCollection<TabViewModelBase> TabList { get; set; }
public MainViewModel()
{
    this.TabList = new ObservableCollection<TabViewModelBase>();

    // Tab1ViewModel is derived from TabViewModelBase
    this.TabList.Add(new Tab1ViewModel()); 
}

So, now the MainViewModel has a list of TabViewModelBase, which I believe is the correct MVVM way to do this. The view (Tab1) for TabViewModelBase is defined using DataTemplate.

This is where the problem is:

Tab1:
<UserControl.Resources>
    <vm:Tab1ViewModel x:Key="VM" />
</UserControl.Resources>
<UserControl.DataContext>
    <StaticResourceExtension ResourceKey="VM" />
</UserControl.DataContext>

I think most people would do this as well, but... There is something terribly wrong with this approach!

In MainViewModel, I have manually instantiated a Tab1ViewModel. In MainView, I used DataTemplate to tell the View to use a Tab1 whenever it sees a Tab1ViewModel. That means MainView would instantiate an object of Tab1 class.

Now, Tab1 needs its DataContext to do binding with its own Tab1ViewModel, so we use StaticResource to add one Tab1ViewModel, except that this is a brand new instance!

I need to set the DataContext back to the original one that I instantiated in MainViewModel. So, how do I set the DataContext of Tab1 within DataTemplate?


Solution

  • You don't have to specify vm:Tab1ViewModel new instance in your XAML. Neither do you need to define the DataContext explicitly. Every item of your list is a ViewModel whenever the type of a ViewModel matched with the type you have defined in DataTemplate a particulate view will be rendered and with the same DataContext as ViewModel. For example if list has two objects like below:

    public ObservableCollection<TabViewModelBase> TabList { get; set; }
    public MainViewModel()
    {
        this.TabList = new ObservableCollection<TabViewModelBase>();
        this.TabList.Add(new Tab1ViewModel1()); 
        this.TabList.Add(new Tab1ViewModel2()); 
    }
    

    and your DataTemplate is :

    <TabControl ItemsSource="{Binding TabList}">
    <TabControl.Resources>
        <DataTemplate DataType="{x:Type vm:Tab1ViewModel}">
            <v:Tab1 />
        </DataTemplate>
       <DataTemplate DataType="{x:Type vm:Tab1ViewModel2}">
            <v:Tab2 />
        </DataTemplate>
    </TabControl.Resources>
    

    ...

    then Two tabs will be renders Tab1 & Tab2 (cause list has 2 items). Tab1 will have Tab1ViewModel1as DataContext and and Tab2 will have Tab1ViewModel2 as DataContext. You need not specify DataContext explicitly.