WPF TabControl and DataTemplates

后端 未结 5 741
名媛妹妹
名媛妹妹 2021-02-03 11:24

I\'ve got a set of ViewModels that I\'m binding to the ItemsSource property of a TabControl. Let\'s call those ViewModels AViewModel, BViewModel, and CViewModel. Each one of tho

相关标签:
5条回答
  • 2021-02-03 11:56

    The easiest way would be to use the automatic template system, by including the DataTemplates in the resources of a ContentControl. The scope of the templates are limited to the element they reside within!

    <TabControl ItemsSource="{Binding TabViewModels}">
        <TabControl.ItemTemplate>
            <DataTemplate>
                <ContentControl Content="{Binding}">
                    <ContentControl.Resources>
                        <DataTemplate DataType="{x:Type AViewModel}">
                            ...
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type BViewModel}">
                            ...
                        </DataTemplate>
                        <DataTemplate DataType="{x:Type CViewModel}">
                            ...
                        </DataTemplate>
                    </ContentControl.Resources>
                </ContentControl>
            </DataTemplate>
        </TabControl.ItemTemplate>
        <TabControl.Resources>
            <DataTemplate DataType="{x:Type AViewModel}">
                ...
            </DataTemplate>
             <DataTemplate DataType="{x:Type BViewModel}">
                ...
            </DataTemplate>
            <DataTemplate DataType="{x:Type CViewModel}">
                ...
            </DataTemplate>
        </TabControl.Resources>
    </TabControl>
    
    0 讨论(0)
  • 2021-02-03 11:57

    You can remove the x:Key :) This will automatically apply the template when the given type is encountered (probably one of the most powerful and underused features of WPF, imo.

    This Dr. WPF article goes over DataTemplates pretty well. The section you'll want to pay attention to is "Defining a Default Template for a Given CLR Data Type".

    http://www.drwpf.com/blog/Home/tabid/36/EntryID/24/Default.aspx

    If this doesn't help your situation, you might be able to do something close to what you are looking for using a Style (ItemContainerStyle) and setting the content and header based on the type using a data trigger.

    The sample below hinges on your ViewModel having a property called "Type" defined pretty much like this (easily put in a base ViewModel if you have one):

    public Type Type 
    { 
       get { return this.GetType(); } 
    }
    

    So as long as you have that, this should allow you to do anything you want. Note I have "A Header!" in a textblock here, but that could easily be anything (icon, etc).

    I've got it in here two ways... one style applies templates (if you have a significant investment in these already) and the other just uses setters to move the content to the right places.

    <Window x:Class="WpfApplication1.Window1"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Window1" Height="300" Width="300"
            xmlns:local="clr-namespace:WpfApplication1">
        <Window.Resources>
            <CompositeCollection x:Key="MyCollection">
                <local:AViewModel Header="A Viewmodel" Content="A Content" />
                <local:BViewModel Header="B ViewModel" Content="B Content" />
            </CompositeCollection>
    
        <DataTemplate x:Key="ATypeHeader" DataType="{x:Type local:AViewModel}">
            <WrapPanel>
                <TextBlock>A Header!</TextBlock>
                <TextBlock Text="{Binding Header}" />
            </WrapPanel>
        </DataTemplate>
        <DataTemplate x:Key="ATypeContent" DataType="{x:Type local:AViewModel}">
            <StackPanel>
                <TextBlock>Begin "A" Content</TextBlock>
                <TextBlock Text="{Binding Content}" />
            </StackPanel>
        </DataTemplate>
    
        <Style x:Key="TabItemStyle" TargetType="TabItem">
            <Style.Triggers>
                <!-- Template Application Approach-->
                <DataTrigger Binding="{Binding Path=Type}" Value="{x:Type local:AViewModel}">
                    <Setter Property="HeaderTemplate" Value="{StaticResource ATypeHeader}" />
                    <Setter Property="ContentTemplate" Value="{StaticResource ATypeContent}" />
                </DataTrigger>
    
                <!-- Just Use Setters Approach -->
                <DataTrigger Binding="{Binding Path=Type}" Value="{x:Type local:BViewModel}">
                    <Setter Property="Header">
                        <Setter.Value>
                            <WrapPanel>
                                <TextBlock Text="B Header!"></TextBlock>
                                <TextBlock Text="{Binding Header}" />
                            </WrapPanel>
                        </Setter.Value>
                    </Setter>
                    <Setter Property="Content" Value="{Binding Content}" />
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <TabControl ItemContainerStyle="{StaticResource TabItemStyle}" ItemsSource="{StaticResource MyCollection}" />
    </Grid>
    

    HTH, Anderson

    0 讨论(0)
  • 2021-02-03 12:03

    In this example I use DataTemplates in the resources section of my TabControl for each view model I want to display in the tab items. In this case I map ViewModelType1 to View1 and ViewModelType2 to View2. The view models will be set as DataContext object of the views automatically.

    For displaying the tab item header, I use an ItemTemplate. The view models I bind to are of different types, but derive from a common base class ChildViewModel that has a Title property. So I can set up a binding to pick up the title to display it in the tab item header.

    In addition I display a "Close" Button in the tab item header. If you do not need that, just remove the button from the example code so you just have the header text.

    The contents of the tab items are rendered with a simple ItemTemplate which displays the view in a content control with Content="{Binding}".

    <UserControl ...>
        <UserControl.DataContext>
            <ContainerViewModel></ContainerViewModel>
        </UserControl.DataContext>      
            <TabControl ItemsSource="{Binding ViewModels}"
                        SelectedItem="{Binding SelectedViewModel}">
                <TabControl.Resources>
                    <DataTemplate DataType="{x:Type ViewModelType1}">
                        <View1/>
                    </DataTemplate>
                    <DataTemplate DataType="{x:Type ViewModelType2}">
                        <View2/>
                    </DataTemplate>             
                </TabControl.Resources>
                <TabControl.ItemTemplate>
                    <DataTemplate>
                        <DockPanel>
                            <TextBlock Text="{Binding Title}" />
                            <Button DockPanel.Dock="Right" Margin="5,0,0,0"
                                    Visibility="{Binding RemoveButtonVisibility}" 
                                    Command="{Binding DataContext.CloseItemCommand, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type TypeOfContainingView}}}"
                                    >
                                <Image Source="/Common/Images/ActiveClose.gif"></Image>
                            </Button>
                        </DockPanel>
                    </DataTemplate>
                </TabControl.ItemTemplate>
                <TabControl.ContentTemplate>
                    <DataTemplate>
                        <ContentControl Content="{Binding}"/>
                    </DataTemplate>
                </TabControl.ContentTemplate>
            </TabControl>
    </UserControl>      
    


    The user control which contains the tab control has a container view model of type ContainerViewModel as DataContext. Here I have a collection of all the view models displayed in the tab control. I also have a property for the currently selected view model (tab item).

    This is a shortened version of my container view model (I skipped the change notification part).

    public class ContainerViewModel
    {
        /// <summary>
        /// The child view models.
        /// </summary>
        public ObservableCollection<ChildViewModel> ViewModels {get; set;}
    
        /// <summary>
        /// The currently selected child view model.
        /// </summary>
        public ChildViewModel SelectedViewModel {get; set;}
    }
    
    0 讨论(0)
  • 2021-02-03 12:14

    One way would be to use DataTemplateSelectors and have each one resolve the resource from a separate ResourceDictionary.

    0 讨论(0)
  • 2021-02-03 12:14

    Josh Smith uses exactly this technique (of driving a tab control with a view model collection) in his excellent article and sample project WPF Apps With The Model-View-ViewModel Design Pattern. In this approach, because each item in the VM collection has a corresponding DataTemplate linking the View to the VM Type (by omitting the x:Key as Anderson Imes correctly notes), each tab can have a completely different UI. See the full article and source code for details.

    The key parts of the XAML are:

     <DataTemplate DataType="{x:Type vm:CustomerViewModel}">
       <vw:CustomerView />
     </DataTemplate>
    
    <DataTemplate x:Key="WorkspacesTemplate">
    <TabControl 
      IsSynchronizedWithCurrentItem="True" 
      ItemsSource="{Binding}" 
      ItemTemplate="{StaticResource ClosableTabItemTemplate}"
      Margin="4"
      />
    

    There is one downside - driving a WPF TabControl from an ItemsSource has performance issues if the UI in the tabs is big/complex and therefore slow to draw (e.g., datagrids with lots of data). For more on this issue, search SO for "WPF VirtualizingStackPanel for increased performance".

    0 讨论(0)
提交回复
热议问题