Search code examples
c#wpfxamlmvvmcaliburn.micro

Declaring TabItems in XAML, binding SelectedItem to view model


I'd like to bind the SelectedItem of a TabControl to a corresponding field in my view model, however while still declaring the available items within the TabControl itself (as opposed to using ItemsSource) and retrieving their Content as for the actual SelectedItem. Meaning: If a tab is being selected, it's Content should end up as the SelectedItem (not the TabItem) and vice-versa.

Sample view model, inheriting a Caliburn.Micro conductor without (!) collection:

public class MyViewModelConductor : Conductor<ConductedViewModelBase> {
  
  public ViewModelA { get; set; }

  public ViewModelB { get; set; }

}

And the corresponding TabControl in XAML:

<TabControl SelectedItem="{Binding ActiveItem}">
  <TabItem cal:View.Model="{Binding ViewModelA}">
    <TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
  </TabItem>
  <TabItem cal:View.Model="{Binding ViewModelB}">
    <TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
  </TabItem>
</TabControl>

I'm aware I could just use e.g. Conductor<'1>.Collection.OneActive and bind to ItemsSource, but there are a few reasons I'd like to refrain from doing so:

  • The available view models from the conductor's side is a fixed set, declared by the exposed properties, not an infinite collection
  • For the UI's side, for each TabItem I need to declare a specific header with lots of just UI related stuff (icon, color) which is platform-specific, hence I would not like to leak it into my view models.

I've tried utilizing SelectedValue and SelectedValuePath and binding to the TabControl itself (e.g. SelectedValue="{Binding RelativeSource={RelativeSource Self}, Path=SelectedItem}" SelectedValuePath="Content"), but there WPF tries to look for a Content property on my bound view models as soon as one becomes selected.


Solution

  • You would have to bind the SelectedValue to your view model class. Then use the SelectedValuePath to declare which property of the SelectedItem is used as the SelectedValue.

    <TabControl SelectedValuePath="Content"
                SelectedValue="{Binding ViewModelSelectedContentProperty}">
      <TabItem cal:View.Model="{Binding ViewModelA}">
        <TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
      </TabItem>
      <TabItem cal:View.Model="{Binding ViewModelB}">
        <TabItem.Header> <!-- vm specific fancy UI stuff --> </TabItem.Header>
      </TabItem>
    </TabControl>
    

    But I don't recommend this. You should bind the ItemSource to a set of data models and define DataTemplates to give you a clean design and dynamic/extensible behavior. Usually you would use DataTemplate for the TabControl.ContentTemplate to template the content and TabControl.ItemTemplate to template the TabItem (TabControl header).

    Don't hardcode the ItemsControl items. They're is absolutely no reason to avoid data templating.