29

I have a TabControl in an MVVM WPF application. It is defined as follows.

<TabControl Style="{StaticResource PortfolioSelectionTabControl}" SelectedItem="{Binding SelectedParameterTab}" >
    <TabItem Header="Trades" Style="{StaticResource PortfolioSelectionTabItem}">
        <ContentControl Margin="0,10,0,5" Name="NSDetailTradeRegion" cal:RegionManager.RegionName="NSDetailTradeRegion" />
    </TabItem>
    <TabItem Header="Ccy Rates" Style="{StaticResource PortfolioSelectionTabItem}">
        <ContentControl Margin="0,10,0,5" Name="NSDetailCcyRegion" cal:RegionManager.RegionName="NSDetailCcyRegion" />
    </TabItem>
    <TabItem Header="Correlations / Shocks" Style="{StaticResource PortfolioSelectionTabItem}">
        <ContentControl Name="NSDetailCorrelationRegion" cal:RegionManager.RegionName="NSDetailCorrelationRegion" />
    </TabItem>
    <TabItem Header="Facility Overrides" Style="{StaticResource PortfolioSelectionTabItem}" IsEnabled="False">
        <ContentControl Name="NSDetailFacilityOverrides" cal:RegionManager.RegionName="NSDetailFacilityOverrides" />
    </TabItem>
</TabControl>

So each tab item content has its own view associated with it. Each of those views has the MEF [Export] attribute and is associated with the relevant region through view discovery, so the above code is all I need to have the tab control load and switch between them. They all reference the same shared ViewModel object behind them and so all interact seamlessly.

My problem is that when the user navigates to the parent window, I want the tab control to default to the second tab item. That is easy enough to do when the window is first loaded, by specifying in XAML IsSelected="True" in TabItem number 2. It is less easy to do when the user navigates away from the screen and then comes back to it.

I thought about having a SelectedItem={Binding SelectedTabItem} property on the tab control, so I could programmatically set the selected tab in the ViewModel, but the problem is I have no knowledge of the TabItem objects in the ViewModel as they are declared above in the XAML only, so I have no TabItem object to pass to the setter property.

One idea I had was to make the child Views (that form the content of each of the tab items above) have a style on the UserControl level of their XAML, something along the following.

<Style TargetType={x:Type UserControl}>
    <Style.Triggers>
        <DataTrigger Property="IsSelected" Value="True">
             <Setter Property="{ElementName={FindAncestor, Parent, typeof(TabItem)}, Path=IsSelected", Value="True" />
        </DataTrigger>
    </Style.Triggers>
</Style>

I know the findancestor bit isn't correct; I've just put it there to specify my intent, but I am not sure of the exact syntax. Basically for each UserControl to have a trigger that listens to a property on the ViewModel (not sure how I would distinguish each different UserControl as obviously they can't all listen to the same property or they would all select simultaneously when the property is set to True, but having a property for each usercontrol seems ugly) and then finds its parent TabItem container and sets the IsSelected value to true.

Am I on the right track with a solution here? Is it possible to do what I am pondering? Is there a tidier solution?

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
NZJames
  • 4,963
  • 15
  • 50
  • 100
  • why just not to create event that will be selecting second tab? – Sasha Nov 08 '13 at 10:56
  • I ideally wanted to do it in MVVM so having nothing in code behind. Agree that I could just put an event in the code behind but was hoping for an MVVM solution – NZJames Nov 08 '13 at 10:58
  • 1
    TabItem has no meaning in MVVM. TabItem is just UI. SelectedTab behaviour will be in the code behind. – Xaruth Nov 08 '13 at 11:01
  • @user1122909 you can use interactivity library and then, for example, when you application will get focus, or some other event, depends on what you need, you can write method in your viewModel and not in code behind. – Sasha Nov 08 '13 at 11:06

5 Answers5

61

If you look at the TabControl Class page on MSDN, you'll find a property called SelectedIndex which is an int. Therefore, simply add an int property into your view model and Bind it to the TabControl.SelectedIndex property and then you can select whichever tab you like at any time from the view model:

<TabControl SelectedIndex="{Binding SelectedIndex}">
    ...
</TabControl>

UPDATE >>>

Setting a 'startup' tab is even easier using this method:

In view model:

private int selectedIndex = 2; // Set the field to whichever tab you want to start on

public int SelectedIndex { get; set; } // Implement INotifyPropertyChanged here
Jérémie Bertrand
  • 3,025
  • 3
  • 44
  • 53
Sheridan
  • 68,826
  • 24
  • 143
  • 183
  • 1
    Simple answer and good result. Another idea I thought of is if you want to control which tab item is the startup tab by specifying in the XAML, just put a binding on that TabItem such as IsSelected={Binding CcyTabSelected} and have a single bool property on ViewModel called CcyTabSelected. When you want to return to the startup tab, you just set that property to true. Slightly different application, this one allows you to return to your home tab easily, your solution allows selection of any tab item in the list by index – NZJames Nov 08 '13 at 11:25
  • 4
    -1 for being impolite, +1 for a good answer. SO is a great place to find clear answers to very specific questions. I think it's good for people to "sum up" what's already available on the MSDN page. SO tends to be a lot more readable than MSDN and a better, quicker resource when you know what your question is. – Simon F May 13 '14 at 14:32
  • 2
    Am I not allowed to point out that users can find these answers themselves, quickly and easily on MSDN? – Sheridan May 13 '14 at 16:00
  • 4
    -1 for being impolite and poor use of MVVM. Binding the selected page index as an **int** is *not type safe*. Using **enum** s and **IValueConverter** would be more in line with good MVVM practice –  May 19 '14 at 02:52
  • @HelloWorld - [IValueConverter sample](http://stackoverflow.com/questions/397556/how-to-bind-radiobuttons-to-an-enum) –  Dec 04 '14 at 23:07
  • How to get the SelectedItem in Josh Smith's MvvMDemoApp, here is the link to my question http://stackoverflow.com/questions/28036479/how-to-find-the-current-active-tab-in-josh-smiths-mvvm-demo-app?noredirect=1#comment44505371_28036479 Any help is greatly appreciated. – savi Jan 21 '15 at 22:24
  • @savi, if you have a question, please ask a new question and if relevant, add a link to this or any other question. – Sheridan Jan 22 '15 at 08:57
  • I don't see anything 'impolite' here. Thanks @Sheridan – Jadav Bheda May 16 '17 at 04:54
  • Thanks @JadavBheda, but the 'impoliteness' was edited away a long time ago... I didn't think that it was so impolite anyway, but you know some people. ;) – Sheridan Jun 15 '17 at 15:36
14

Just FYI, I gone through the same issue where I add tabs dynamically using ObservableCollection source but last added Tab do not get selected. I have done same changes what Sheridan said to select Tab as per SelectedIndex. Now last added Tab gets selected but it was not getting focused. So to focus the Tab we have to add set Binding IsAsync property True.

<TabControl ItemsSource="{Binding Workspaces}" Margin="5" SelectedIndex="{Binding TabIndex, Mode=OneWay,UpdateSourceTrigger=PropertyChanged, IsAsync=True}">
Sudhir Bhapkar
  • 931
  • 8
  • 7
10

The below code sample will create a dynamic tab using MVVM.

XAML

<TabControl Margin="20" x:Name="tabCategory"
                ItemsSource="{Binding tabCategory}"
                SelectedItem="{Binding SelectedCategory}">

    <TabControl.ItemTemplate>
        <DataTemplate>
            <HeaderedContentControl Header="{Binding TabHeader}"/>
        </DataTemplate>
    </TabControl.ItemTemplate>

    <TabControl.ContentTemplate>
        <DataTemplate>
            <ContentControl Content="{Binding TabContent}" />
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>

Modal Class

TabCategoryItem represents each tab item. On two properties, TabHeader will display a tab caption and TabContent contains the content/control to fill in each tab.

Public Class TabCategoryItem

    Public Property TabHeader As String
    Public Property TabContent As UIElement
End Class

VM Class

Public Class vmClass

    Public Property tabCategory As ObjectModel.ObservableCollection(Of TabCategoryItem)
    Public Property SelectedCategory As TabCategoryItem
End Class

The below code will fill and bind the content. I am creating two tabs, tab1 and tab2. Both tabs will contain text boxes. You can use any UIelement instead of text boxes.

Dim vm As New vmClass

vm.tabCategory = New ObjectModel.ObservableCollection(Of TabCategoryItem)

'VM.tabCategory colection will create all tabs

vm.tabCategory.Add(New TabCategoryItem() With {.TabHeader = "Tab1", .TabContent = new TextBlock().Text = "My first Tab control1"})
vm.tabCategory.Add(New TabCategoryItem() With {.TabHeader = "Tab2", .TabContent = new TextBlock().Text = "My first Tab control2"})

mywindow.DataContent = vm
Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Rosh
  • 101
  • 1
  • 4
0

The accepted answer is not working with DependencyObject on your ViewModel .

I'm using MVVM with DependencyObject and Just setting the TabControl didn't work for me.The problem I had was the the property was not getting update on the View when I was setting the tab selectedIndex from the ViewModel.

I did set the Mode to be two ways but nothing was working.

<TabControl  SelectedIndex="{Binding SelectedTab,Mode=TwoWay}" >
    ...
</TabControl>

The ViewModel property "SelectedTab" was getting updated all the time when I navigated between tabs. This was confirming my binding was working properly. Each time I would navigate the tabs both the Get and Set would get called in my ViewModel. But if I try to set the SelectedIndex in the ViewModel it would not update the view. ie: SelectedTab=0 or SelectedTab=1 etc... When doing the set from the ViewModel the SelectedTab 'set' method would be called, but the view would never do the 'get'.

All I could find online was example using INotifyPropertyChanged but I do not wish to use that with my ViewModel.

I found the solutions in this page: http://blog.lexique-du-net.com/index.php?post/2010/02/24/DependencyProperties-or-INotifyPropertyChanged

With DependencyObject, you need to register the DependencyProperties. Not for all properties but I guess for a tabcontrol property you need to.

Below my code:

view.xaml

//Not sure below if I need to mention the TwoWay mode
<TabControl  SelectedIndex="{Binding SelectedTab,Mode=TwoWay}" >
        ...
</TabControl>

ViewModel.cs

public class ViewModel : DependencyObject
{
       public static readonly DependencyProperty SelectedTabDP =  DependencyProperty.Register("SelectedTab", typeof(int), typeof(ViewModel));

       public int SelectedTab
       {
          get { return (int)GetValue(SelectedTabDP); }
          set { SetValue(SelectedTabDP, value); }
       }
}

Basically all I had to do was to actually register the dependency property (DependencyProperty) as you can see above.

What made this hard to figure out was that I have a bunch of other Properties on that view and I didn't need to register them like that to make it work two ways. For some reason on the TabControl I had to register the property like I did above.

Hope this help someone else.

Turns out my problem were because my components have names:

x:Name="xxxxxxxx"

Giving names to components at the same time of biding them with DependencyObject seems to be the main cause of all my issues.

Pat M
  • 41
  • 3
0

In order to improve semantic of my viewmodel and to not work with an int when using code to check for the selected tab, I made some additions to the accepted answer so to use an Enum instead of an int. These are the steps:

  • Define an Enum representing the different tabs:

     public enum RulesVisibilityMode {
         Active,
         History
     }
    
  • Expose the SelectedTab as a property using the enum instead of the int:

    public RulesVisibilityMode SelectedTab { get; set; }
    
  • Create a converter to convert from an int to your enum (I don't need the ConvertBack because I never select the active tab from the code, but you can add it too):

    internal class RulesVisibilityModeConverter : IValueConverter
    {
         public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
         {
             throw new NotImplementedException("Conversion from visibility mode to selected index has not been implemented");
         }
    
         public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
         {
             int selectedTabIndex;
             if (int.TryParse(value.ToString(), out selectedTabIndex))
             {
                 return (RulesVisibilityMode)selectedTabIndex;
             }
             return null;
         }
     }
    
    
  • Bind the tabcontrol to the SelectedTab property through the converter:

    <TabControl SelectedIndex="{Binding SelectedTab, Mode=OneWayToSource, Converter={StaticResource RulesVisibilityModeConverter}}" ...
    

Now every time you need to check for the selected tab in the code you deal with a readable enum:

if (this.SelectedTab != RulesVisibilityMode.Active) ...
Daniele Armanasco
  • 7,289
  • 9
  • 43
  • 55