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
?
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 Tab1ViewModel1
as DataContext
and and Tab2
will have
Tab1ViewModel2
as DataContext
. You need not specify DataContext
explicitly.
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