如何将 TabControl 绑定到 ViewModel 集合?

基本上我的 mainviewmodel.cs 是:

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

但是,我需要不仅能够创建选项卡,而且能够在维护 MVVM 的同时加载选项卡内容并将其链接到适当的视图模型。

基本上,我怎样才能让一个用户控件作为一个标签项的内容加载,并让这个用户控件连接到一个适当的视图模型。造成这个困难的部分是 ViewModel 不应该构造实际的视图项,对吗?真的吗?

基本上,这样的 MVVM 是否合适:

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

我这样问是因为,我正在从 ViewModel 中构建一个 View (AddressControl) ,对我来说这听起来像是 MVVM 的禁忌。

85092 次浏览

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>

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.

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.

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.