WPF MVVM: Setting DataContext of Tab Views

2020-05-09 22:16发布

问题:

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?

回答1:

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.



回答2:

Just an addition to @KyloRen's answer: This is so called "ViewModel-First approach". Based on your viewmodel, view is selected -> you have viewmodel first.

However, you don't even need datatemplates for your views. It can be annoying to write datatemplate for each view.

There is alternative implementation of the same "ViewModel-First" principle:

<TabControl ItemsSource="{Binding TabList}">
  <TabControl.ItemTemplate>
     <DataTemplate>
        <ContentPresenter Content={Binding Converter={ViewModelToViewConverter}} />
     </DataTemplate>
  </TabControl.ItemTemplate>
</TabControl>

ViewModelToViewConverter takes ViewModel and based on naming convention creates view for it. This is especially usefull in page-based navigation scenarios, but it is universal approach that works in many situations (navigation, listboxes, itemscontrols, dynamic content presenters, etc..)

Example of the converter can be found here - just replace the IocContainer with Activator.CreateInstance