Wie binde ich ein TabControl an eine Sammlung von ViewModels?

73

Grundsätzlich habe ich in meinem MainViewModel.cs:

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

Ich muss jedoch irgendwie in der Lage sein, nicht nur die Registerkarten zu erstellen, sondern auch den Inhalt der Registerkarten zu laden und mit den entsprechenden Ansichtsmodellen zu verknüpfen, während MVVM beibehalten wird.

Grundsätzlich, wie kann ich eine Benutzersteuerung als Inhalt eines Tabitems laden UND diese Benutzersteuerung mit einem geeigneten Ansichtsmodell verbinden lassen. Der Teil, der dies schwierig macht, ist, dass das ViewModel nicht die tatsächlichen Ansichtselemente erstellen soll, oder? Oder kann es?

Wäre dies grundsätzlich MVVM angemessen:

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

Ich frage nur, weil ich eine Ansicht (AddressControl) aus einem ViewModel heraus konstruiere, was für mich wie ein MVVM-No-No klingt.

Michael
quelle
+1 gute Frage. In den PRISM-Handbüchern wird dieser Fall nicht wirklich behandelt.
Markus Hütter
Sie haben es im Handbuch nicht behandelt, aber in der Referenzimplementierung.
PVitt
Dies ist eine reine C # / WPF / MVVM-Frage, ob PRISM integriert ist / verwendet wird oder nicht.
IAbstract

Antworten:

141

Dies ist keine MVVM. Sie sollten keine UI-Elemente in Ihrem Ansichtsmodell erstellen.

Sie sollten die ItemsSource der Registerkarte an Ihre ObservableCollection binden, und diese sollte Modelle mit Informationen zu den Registerkarten enthalten, die erstellt werden sollen.

Hier sind die VM und das Modell, das eine Registerkarte darstellt:

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; }
}

Und so sehen die Bindungen im Fenster aus:

<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>

(Hinweis: Wenn Sie unterschiedliche Inhalte in unterschiedlichen Registerkarten verwenden möchten, verwenden Sie DataTemplates. Entweder sollte das Ansichtsmodell jeder Registerkarte eine eigene Klasse sein, oder Sie erstellen eine benutzerdefinierte Klasse, DataTemplateSelectorum die richtige Vorlage auszuwählen.)

Ein UserControl in der Datenvorlage:

<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
quelle
4
Nun, der Inhalt einer Registerkarte ist jedoch eine Benutzersteuerung. Würde ich also nicht immer noch eine neue UI-Instanz in meinem ViewModel erstellen?
Michael
4
@michael: In Ihrem Beispiel erstellen Sie tatsächlich ein UI-Element in Ihrem ViewModel. In meinem Beispiel erstelle ich ein Modell vom Typ TabItem. In Ihrem Beispiel würde das TabControl (hypothetisch) die von Ihrem ViewModel instanziierten TabItems nehmen und dem Benutzer anzeigen. In meiner sieht es seine ItemsSource, erstellt für jede eine Registerkarte und bindet die Teile jeder Registerkarte entsprechend der Konfiguration des Elements in der Ansicht und den Arten der angezeigten Elemente. Es ist eine große Unterscheidung. Verstehen Sie es?
5
Es hat eine Weile gedauert, dies als Antwort zu markieren, aber ich habe endlich herausgefunden, was Sie mit dem DataTemplates-Teil gemeint haben. WPF verkabelt die Views / ViewModels automatisch basierend auf dem ViewModel-Typ auf der Registerkarte, solange ich die DataTemplate definiere.
Michael
3
TabItemist ein UI-Element, wenn Sie mich fragen. Warum wird das in einem Ansichtsmodell erstellt?
Gusdor
3
@ Gusdor nenne es wie du willst. "Gruppe", "foo", "pedantischer Kommentar". Was auch immer Ihr Design erfordert.
20

In Prism wird das Registersteuerelement normalerweise zu einem Bereich, sodass Sie nicht die Kontrolle über die Sammlung gebundener Registerkarten übernehmen müssen.

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

Jetzt können die Ansichten hinzugefügt werden, indem Sie sich in der Region MainRegion registrieren:

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

Und hier sehen Sie eine Spezialität von Prisma. Die Ansicht wird vom ViewModel instanziiert. In meinem Fall löse ich das ViewModel über einen Inversion of Control-Container (z. B. Unity oder MEF) auf. Das ViewModel erhält die Ansicht über die Konstruktorinjektion injiziert und legt sich selbst als Datenkontext der Ansicht fest.

Die Alternative besteht darin, den Typ der Ansicht im Regionscontroller zu registrieren:

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

Mit diesem Ansatz können Sie die Ansichten später zur Laufzeit erstellen, z. B. von einem Controller:

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

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

Da Sie den Typ der Ansicht registriert haben, wird die Ansicht in der richtigen Region platziert.

PVitt
quelle
1

Ich habe einen Konverter zum Entkoppeln der Benutzeroberfläche und des ViewModel, das ist der folgende Punkt:

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

Der Tab ist eine Aufzählung in meinem TabItemViewModel und der TabItemConverter konvertiert ihn in die reale Benutzeroberfläche.

Rufen Sie im TabItemConverter einfach den Wert ab und geben Sie eine Benutzersteuerung zurück, die Sie benötigen.

acai
quelle
-2

Möglicherweise so:

<UserControl x:Class="Test_002.Views.MainView"
         xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
         xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
         xmlns:local="clr-namespace:Test_002.Views"
         xmlns:generalView="clr-namespace:Test_002.Views.General"
         xmlns:secVIew="clr-namespace:Test_002.Views.Security"
         xmlns:detailsView="clr-namespace:Test_002.Views.Details"
         mc:Ignorable="d" 
         d:DesignHeight="400" d:DesignWidth="650">
<Grid>
    <DockPanel>
        <StackPanel Orientation="Horizontal" DockPanel.Dock="Bottom" Margin="2,5">
            <Button Command="{Binding btnPrev}" Content="Prev"/>
            <Button Command="{Binding btnNext}" Content="Next"/>
            <Button Command="{Binding btnSelected}" Content="Selected"/>
        </StackPanel>
        <TabControl>
            <TabItem Header="General">
                <generalView:GeneralView></generalView:GeneralView>
            </TabItem>
            <TabItem Header="Security">
                <secVIew:SecurityView></secVIew:SecurityView>
            </TabItem>
            <TabItem Header="Details">
                <detailsView:DetailsView></detailsView:DetailsView>
            </TabItem>
        </TabControl>
    </DockPanel>
</Grid>

Denke, das ist der einfachste Weg. Ist die MVVM kompatibel?

BigShady
quelle