77

Basically I have in my MainViewModel.cs:

ObservableCollection<TabItem> MyTabs { get; private set; }

However, I need to somehow be able to not only create the tabs, but have the tabs content be loaded and linked to their appropriate viewmodels while maintaining MVVM.

Basically, how can I get a usercontrol to be loaded as the content of a tabitem AND have that usercontrol wired up to an appropriate viewmodel. The part that makes this difficult is the ViewModel is not supposed to construct the actual view items, right? Or can it?

Basically, would this be MVVM appropriate:

UserControl address = new AddressControl();
NotificationObject vm = new AddressViewModel();
address.DataContext = vm;
MyTabs[0] = new TabItem()
{
    Content = address;
}

I only ask because well, i'm constructing a View (AddressControl) from within a ViewModel, which to me sounds like a MVVM no-no.

IAbstract
  • 19,551
  • 15
  • 98
  • 146
michael
  • 14,844
  • 28
  • 89
  • 177

4 Answers4

155

This isn't MVVM. You should not be creating UI elements in your view model.

You should be binding the ItemsSource of the Tab to your ObservableCollection, and that should hold models with information about the tabs that should be created.

Here are the VM and the model which represents a tab page:

public sealed class ViewModel
{
    public ObservableCollection<TabItem> Tabs {get;set;}
    public ViewModel()
    {
        Tabs = new ObservableCollection<TabItem>();
        Tabs.Add(new TabItem { Header = "One", Content = "One's content" });
        Tabs.Add(new TabItem { Header = "Two", Content = "Two's content" });
    }
}
public sealed class TabItem
{
    public string Header { get; set; }
    public string Content { get; set; }
}

And here is how the bindings look in the window:

<Window x:Class="WpfApplication12.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <ViewModel
            xmlns="clr-namespace:WpfApplication12" />
    </Window.DataContext>
    <TabControl
        ItemsSource="{Binding Tabs}">
        <TabControl.ItemTemplate>
            <!-- this is the header template-->
            <DataTemplate>
                <TextBlock
                    Text="{Binding Header}" />
            </DataTemplate>
        </TabControl.ItemTemplate>
        <TabControl.ContentTemplate>
            <!-- this is the body of the TabItem template-->
            <DataTemplate>
                <TextBlock
                    Text="{Binding Content}" />
            </DataTemplate>
        </TabControl.ContentTemplate>
    </TabControl>
</Window>

(Note, if you want different stuff in different tabs, use DataTemplates. Either each tab's view model should be its own class, or create a custom DataTemplateSelector to pick the correct template.)

A UserControl inside the data template:

<TabControl
    ItemsSource="{Binding Tabs}">
    <TabControl.ItemTemplate>
        <!-- this is the header template-->
        <DataTemplate>
            <TextBlock
                Text="{Binding Header}" />
        </DataTemplate>
    </TabControl.ItemTemplate>
    <TabControl.ContentTemplate>
        <!-- this is the body of the TabItem template-->
        <DataTemplate>
            <MyUserControl xmlns="clr-namespace:WpfApplication12" />
        </DataTemplate>
    </TabControl.ContentTemplate>
</TabControl>
IAbstract
  • 19,551
  • 15
  • 98
  • 146
  • 4
    Well, the content of a tab is a usercontrol though, so wouldn't I still be making new UI instance in my ViewModel? – michael Apr 13 '11 at 16:44
  • 4
    @michael: In your example, you are actually creating a UI element in your ViewModel. In my example, I am creating a Model of type TabItem. In your example, the TabControl (hypothetically) would take the TabItems instantiated by your ViewModel and display them to the user. In mine, it sees its ItemsSource, creates a tab for each one, and binds the parts of each tab according to the configuration of the element in the View and the types of items it is displaying. Its a major distinction. Do you understand it? –  Apr 13 '11 at 17:25
  • 1
    @Will: Well, In your example wouldn't I still have to do `Tabs.Add(new TabItem { Header = "One", Content = new AddressView() });` which still is declaring a new UI control (AddressView) in the ViewModel. I guess I'm just having a hard time seeing how this avoids creating an instance of the AddressView in the ViewModel. – michael Apr 13 '11 at 17:44
  • @michael: No, you wouldn't. *Its the magic of WPF!* See for yourself. Create a new WPF app called WpfApplication12 (sorry about the name), replace all xaml in MainWindow.xaml with the xaml in my answer, and add the two classes from my answer to your project (in the namespace WpfApplication12!). Run it and see the results. –  Apr 13 '11 at 18:26
  • @Will: But in your example you're just replacing Content with "One's Content" whereas I need my content to be an actual UserControl. So if I take your example verbatim and want the content to actually be a usercontrol instead of some text, I would have to do `Content = new AddressView()` which doesn't avoid the issue of creating a View inside my ViewModel. – michael Apr 13 '11 at 19:04
  • @michael: You are operating under incorrect assumptions. Edited to show how you would insert a UserControl. The UserControl's DataContext would be the TabItem. More complex requirements can be served; there is no limitation here by using MVVM properly. –  Apr 13 '11 at 19:10
  • @Will, no. I'm not asking about turning the TabControl into a UserControl, but for the TabControl to host other UserControls, so one Tab would have a header of "1" and the content of that TabItem would be a UserControl (AddressView). A different TabItem would have a header of "2" and the content of that TabItem would be a different UserControl (CustomerView). Basically, the TabControl would host different UserControls as the content of each TabItem. I don't believe the posted solution does that. – michael Apr 14 '11 at 00:30
  • @michael: It does not, but your requirements are not at all clear. If your tabs must be dynamic, my solution works. If you need dynamic tabs that contain different user controls, you can bind the ItemsSource to a collection of objects, each a different type and each that presents different data appropriate to each tab. You can then create DataTemplates [(a different one for each type)](http://msdn.microsoft.com/en-us/library/system.windows.datatemplate.datatype.aspx) and the default selector will choose the appropriate template when binding –  Apr 14 '11 at 13:31
  • @michael: If your tabs do NOT have to be dynamic, you can just define them in xaml, bind them as is appropriate, and be done with it. So, which is it? –  Apr 14 '11 at 13:32
  • 5
    It took awhile to mark this as the answer, but I finally figured out what you meant by the DataTemplates part. WPF automatically wires up the Views/ViewModels based upon the type of ViewModel in the tab as long as I define the DataTemplate. – michael Sep 23 '11 at 17:18
  • This post is a little old, but I am facing the same scenario as Will predicted. I am generating the tabs dynamically for showing ssrs Reports. The first tab is the report selection tab and the reports generated would be shown in subsequent tabs. I am not able to figure out how does the above code sample bind the Usercontrol in the tabcontrol datatemplate in contenttemplate with the DataContext. Any idea? – Shakti Prakash Singh Apr 11 '12 at 14:50
  • Well, thank for the answer. But I am confused with my design. I have a ViewModel that has the observable collection and a view that hosts the TabControl. Now once the first tab is bound, and I select a report and click on generate, how to I add this item to the observable collection. Seems what I am trying to do is let a child control create a sibling. I am not sure if it is feasible, but this is what my requirement is. – Shakti Prakash Singh Apr 11 '12 at 15:25
  • The reason the user can't select the reports from mainwindow is cause Reports is a Menu out of several others. Once the user selects a report, several list boxes are shown and the user selects the Report Paramters from them. Now I don't have the enough space on the window to show both the parameters and the tabcontrol on the same window. – Shakti Prakash Singh Apr 11 '12 at 15:27
  • 4
    `TabItem` is a UI Element if you ask me. Why is that being created in a view model? – Gusdor Nov 05 '14 at 10:32
  • 3
    @Gusdor call it whatever you want. "Group", "foo", "pedantic comment". Whatever your design requires. –  Nov 05 '14 at 14:16
  • 3
    @Will be thankful that i didn't pass judgement on the use of `sealed` ;) – Gusdor Nov 05 '14 at 14:31
  • can someone tell me from where does the "MyUserControl" come from? I do not get that part. – gts13 Apr 18 '17 at 14:12
  • @GTS13 it's a hypothetical user control that you would have written that is designed against the `TabItem` class (or whatever is within the collection bound to `TabControl.ItemsSource` –  Apr 18 '17 at 15:20
  • 2
    I really dont get the third part of your code in your answer. What I dont get is that the keeps only one specific TabItem of the TabControl. What if I have another TabItem-UserControl class? Let's say MyUserControl2. Do I need to write another TabControl to host the MyUserControl2? – gts13 Apr 19 '17 at 07:43
  • @Will if you don't get my question, I have the same here: http://stackoverflow.com/questions/43410133/wpf-binding-different-usercontrols-in-a-datatemplate-of-tabcontrol – gts13 Apr 19 '17 at 08:08
20

In Prism you usually make the tab control a region so that you don't have to take control over the bound tab page collection.

<TabControl 
    x:Name="MainRegionHost"
    Regions:RegionManager.RegionName="MainRegion" 
    />

Now the views can be added via registering itself into the region MainRegion:

RegionManager.RegisterViewWithRegion( "MainRegion", 
    ( ) => Container.Resolve<IMyViewModel>( ).View );

And here you can see a speciality of Prism. The View is instanciated by the ViewModel. In my case I resolve the ViewModel throught a Inversion of Control container (e.g. Unity or MEF). The ViewModel gets the View injected via constructor injection and sets itself as the View's data context.

The alternative is to register the view's type into the region controller:

RegionManager.RegisterViewWithRegion( "MainRegion", typeof( MyView ) );

Using this approach allows you to create the views later during runtime, e.g. by a controller:

IRegion region = this._regionManager.Regions["MainRegion"];

object mainView = region.GetView( MainViewName );
if ( mainView == null )
{
    var view = _container.ResolveSessionRelatedView<MainView>( );
    region.Add( view, MainViewName );
}

Because you have registered the View's type, the view is placed into the correct region.

PVitt
  • 11,500
  • 5
  • 51
  • 85
1

I have a Converter to decouple the UI and ViewModel,thats the point below:

<TabControl.ContentTemplate>
    <DataTemplate>
        <ContentPresenter Content="{Binding Tab,Converter={StaticResource TabItemConverter}"/>
    </DataTemplate>
</TabControl.ContentTemplate>

The Tab is a enum in my TabItemViewModel and the TabItemConverter convert it to the real UI.

In the TabItemConverter,just get the value and Return a usercontrol you need.

acai
  • 11
  • 2
0

My solution uses ViewModels directly, so I think it might be useful to someone:

First, I bind the Views to the ViewModels in the App.xaml file:

<Application.Resources>
        <DataTemplate DataType="{x:Type local:ViewModel1}">
            <local:View1/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:ViewModel2}">
            <local:View2/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type local:ViewModel3}">
            <local:View3/>
        </DataTemplate>
</Application.Resources>

The MainViewModel looks like this:

    public class MainViewModel : ObservableObject
        {
            private ObservableCollection<ViewModelBase> _viewModels = new ObservableCollection<ViewModelBase>();
            

            public ObservableCollection<ViewModelBase> ViewModels
            {
                get { return _viewModels; }
                set
                {
                    _viewModels = value;
                    OnPropertyChanged();
                }
            }
    
            private ViewModelBase _currentViewModel;
 
           public ViewModelBase CurrentViewModel
            {
                get { return _currentViewModel; }
                set
                {
                    _currentViewModel = value;
                    OnPropertyChanged();
                }
            }
    
            private ICommand _closeTabCommand;
    
            public ICommand CloseTabCommand => _closeTabCommand ?? (_closeTabCommand = new RelayCommand(p => closeTab()));
            
private void closeTab()
            {
                ViewModels.Remove(CurrentViewModel);
                CurrentViewModel = ViewModels.LastOrDefault();
            }
    
    
            private ICommand _openTabCommand;
    
            public ICommand OpenTabCommand => _openTabCommand ?? (_openTabCommand = new RelayCommand(p => openTab(p)));
            
private void openTab(object selectedItem)
            {
                Type viewModelType;
    
                switch (selectedItem)
                {
                    case "1":
                        {
                            viewModelType = typeof(ViewModel1);
                            break;
                        }
                    case "2":
                        {
                            viewModelType = typeof(ViewModel2);
                            break;
                        }
                    default:
                        throw new Exception("Item " + selectedItem + " not set.");
                }
    
                displayVM(viewModelType);
            }
    
            private void displayVM(Type viewModelType)
            {
                if (!_viewModels.Where(vm => vm.GetType() == viewModelType).Any())
                {
                    ViewModels.Add((ViewModelBase)Activator.CreateInstance(viewModelType));
                }
                CurrentViewModel = ViewModels.Single(vm => vm.GetType() == viewModelType);
            }
    
        }
    }

MainWindow.XAML:

<Window.DataContext>
        <local:MainWindowViewModel x:Name="vm"/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>
        <Menu Grid.Row="0">
            <MenuItem Header="1" Command="{Binding OpenTabCommand}" CommandParameter="1"/>
            <MenuItem Header="2" Command="{Binding OpenTabCommand}" CommandParameter="2"/>
            <MenuItem Header="3" Command="{Binding OpenTabCommand}" CommandParameter="3"/>
        </Menu>
        <TabControl Grid.Row="1" ItemsSource="{Binding ViewModels}" SelectedItem="{Binding CurrentViewModel}">
            <TabControl.ItemTemplate>
                <DataTemplate DataType="{x:Type MVVMLib:ViewModelBase}">
                    <TextBlock Text="{Binding Title}">
                    <Hyperlink Command="{Binding RelativeSource={RelativeSource AncestorType=Window, Mode=FindAncestor}, Path=DataContext.CloseWindowCommand}">X</Hyperlink>    
                    </TextBlock>                    
                </DataTemplate>
            </TabControl.ItemTemplate>                        
        </TabControl>
    </Grid>

I translated some parts to make it easier to understand, there might be some typos.

rod0302
  • 121
  • 1
  • 5