Tabcontrol und MVVM



  • Hallo

    In diesem Forum wurde ich darauf hingewiesen das MVVM Muster zu verwenden. Hat bisher auch ganz gut geklappt. Nun steh ich allerdings vor dem Problem dass ein anderes Tabitem den Focus bekommen soll wenn ich auf einen bestimmten Button klicke.

    Wie ich das nun über Databinding realisieren soll weiß ich nicht? Kann mir jemand helfen?



  • Das TabControl ist auch ein ItemsControl und hat somit ein SelectedItem.
    Das kannst du Binden.

    public class MainViewModel : ObservableObject
    {
        public ObservableCollection<PageViewModel> Pages { get; set; }
    
        public PageViewModel CurrentPage
        {
            get { return _currentPage; }
            set
            {
                _currentPage = value;
                NotifyPropertyChanged(() => CurrentPage);
            }
        }
        private PageViewModel _currentPage;
    
        private void SwitchToLastPage()
        {
            CurrentPage = Pages.LastOrDefault();
        }
    }
    
    <Window>
        <TabControl ItemsSource="{Binding Pages}" SelectedItem="{Binding CurrentPage}">
            <TabControl.ItemTemplate>
                <DataTemplate>
                    <!-- The Header -->
                    <TextBlock Text="{Binding Name}" />
                </DataTemplate>
            </TabControl.ItemTemplate>
            <TabControl.ContentTemplate>
                <DataTemplate>
                    <!-- Body -->
                    <Controls:MyControl />
                </DataTemplate>
            </TabControl.ContentTemplate>
        </TabControl>
    </Window>
    


  • Hei dank dir.

    Da ich aber immer auf das 2te Tab wechseln muss reicht es mir eigentlich wenn ich den SelectedIndex setze. Oder?



  • Richtig, SelectedIndex kannst du auch Binden.



  • Ok noch ne Frage dazu:

    Jede Tabseite zeigt nun ein eigenes Control an.

    <DataTemplate>
      <!-- Body -->
      <Controls:MyControl />
    </DataTemplate>
    

    Dies sollte ich ja dann auch über Binding machen. Aber wo hintelege ich den Name der View. Eigentlich wäre die Information dann doch in PageModel zu hinterlegen. Aber dann enthält ja mein Model Informationen über die View. Oder?



  • Wenn du unterschiedliche Views hast, hast du normalerweise auch unterschiedliche ViewModels.
    (Wenn nicht kannst du ja ein "Wrapper" ViewModel machen für jede View.)

    Dann bindest du die Collection der ViewModels gegen das TabControl, und in dessen Resourcen sagst du welche View genommen wird in Abhängigkeit zu dem ViewModels.
    Das ContentTemplate lässt du einfach weg. WPF versuchst das Template selber zu finden wenn keines definiert ist, da geht es nach dem DataType in den Resourcen, erst wenn er dort nichts findet macht er das ToString als fallback.

    public class MainViewModel
    {
    	public MainViewModel()
    	{
    		Pages = new ObservableCollection<Page> {new FirstViewModel("First"), new SecondViewModel("Second")};
    	}
    
    	public ObservableCollection<Page> Pages { get; set; }
    }
    
    public class FirstViewModel : Page
    {
    	public FirstViewModel(string title)
    		: base(title)
    	{
    	}
    }
    
    public class SecondViewModel : Page
    {
    	public SecondViewModel(string title)
    		: base(title)
    	{
    	}
    }
    
    public class Page
    {
    	public Page(string title)
    	{
    		Title = title;
    	}
    
    	public string Title { get; set; }
    }
    
    <TabControl ItemsSource="{Binding Pages}">
    	<TabControl.ItemTemplate>
    		<DataTemplate>
    			<TextBlock Text="{Binding Title}" />
    		</DataTemplate>
    	</TabControl.ItemTemplate>
    	<TabControl.Resources>
    		<DataTemplate DataType="{x:Type Local:FirstViewModel}">
    			<Controls:FirstView />
    		</DataTemplate>
    		<DataTemplate DataType="{x:Type Local:SecondViewModel}">
    			<Controls:SecondView />
    		</DataTemplate>
    	</TabControl.Resources>
    </TabControl>
    

    Es gilt, die ViewModels haben sich nicht für Views zu interessieren.



  • Ok.

    Sehe ich das nun richtig, dass Page dein Model ist und du das ViewModel davon ableitest?



  • Ne, page ist ein ViewModel welches die Basis der ViewModel darstellt (wegen den Titel)(Hätte es PageViewModel benennen müssen).
    Ich habe das gemacht damit nicht jedes ViewModel das property Titel implementieren muss.
    Das kannst du alles handhaben wie du möchtest.

    FirstViewModel sowie SecondViewModel sind lediglich Wrapperklassen mit den Daten die du in der Oberfläche Anzeigen willst.



  • Achso. Aber das ganze ist doch komplizierter wie gedacht. Wieso sind hier nun Daten in den ViewModels hinterlegt. Sollte es nicht noch ein PageModel geben in denen der Titel hinterlegt ist?



  • Wenn du es in den Modeldaten brauchst, why not, wenn du es nicht brauchst ist es durchaus legitim das es "Title" nur in den ViewModels gibt.
    ViewModels geben nicht nur die Models wieder (denken viele MVVM Einsteiger), sondern sie bereiten die Daten auf sodass sie von einer Ausgabe (z.B.: Xaml oder Console) konsumiert werden können (in irgend einer Form, ist für die ViewModels irrelevant).
    Wenn dazu gehört das es nur in den ViewModels ein Titel gibt, dann ist das eben so.

    In Prinzip ist es Simpel.
    Nehmen wir an du hast eine Person und du willst ein Tab mit einer "Basic" und "Advanced" Ansicht machen.
    Dann erstellst du dir ein BasicPersonViewModel sowie ein AdvancedPersonViewModel und gibst beiden die selbe Person.
    Beide ViewModels packst du in eine Liste (Pages z.B.)(Daher wäre eine gemeinsame Basisklasse gut, z.B. eine PersonViewModel [vor allem für den Titel]) und bindest es aus der Xaml heraus wie oben zu sehen.
    Die Xaml entscheidet dann selber wie es die ViewModels anzeigt, durch den unterschiedlichen Typ hat sie dann die Möglichkeit sie zu unterscheiden (DataType).



  • Danke für deine Antwort. Da stellt sich für mich nun aber noch eine Frage. Wo lege ich dann meine Person ab.

    Wenn ich ja zwei ViewModels für ein Model habe muss ich das Model ja woanders ablegen und in den Viewmodels nur noch verweise auf die Models ablegen. Oder?



  • Rüchtüg. Da C# eh nur mit den Referenzen arbeitet, hast du auch immer die Selbe, ist also kein Problem.
    Es gibt verschiedene Ansätze das Problem grundsätzlich zu Lösen, am Ende ist es dir überlassen.
    Man könnte es z.B. so machen:

    public class Person
    {
    	public string Name { get; set; }
    }
    
    public class ComponentObject<T>
    {
    	public T Component { get; private set; }
    
    	public ComponentObject(T component)
    	{
    		Component = component;
    	}
    }
    
    public class PageViewModel<T> : ComponentObject<T>, INotifyPropertyChanged
    {
    	public PageViewModel(T component)
    		: base(component)
    	{
    	}
    
    	public string Title
    	{
    		get { return _title; }
    		set
    		{
    			_title = value;
    			NotifyPropertyChanged(() => Title);
    		}
    	}
    	private string _title;
    
    	#region NotifyPropertyChanged
    	public event PropertyChangedEventHandler PropertyChanged;
    	protected void NotifyPropertyChanged<T>(Expression<Func<T>> property)
    	{
    		var handler = PropertyChanged;
    		if (handler != null)
    		{
    			var memberExpression = (MemberExpression)property.Body;
    			handler(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
    		}
    	}
    	#endregion NotifyPropertyChanged
    }
    
    public class PersonViewModel : PageViewModel<Person>
    {
    	public PersonViewModel(Person person)
    		: base(person)
    	{
    	}
    
    	public string Name
    	{
    		get { return Component.Name; }
    		set
    		{
    			Component.Name = value;
    			NotifyPropertyChanged(() => Name);
    		}
    	}
    }
    
    public class BasicPersonViewModel : PersonViewModel
    {
    	public BasicPersonViewModel(Person person)
    	: base(person)
    	{
    		Title = "Basic";
    	}
    
    	// Properties for the basic view
    }
    
    public class AdvancePersonViewModel : PersonViewModel
    {
    	public AdvancePersonViewModel(Person person)
    		: base(person)
    	{
    		Title = "Advance";
    	}
    
    	// Properties for the advance view
    }
    
    public class MainViewModel
    {
    	public MainViewModel()
    	{
    		var person = new Person {Name = "sausebrause"};
    		Pages = new ObservableCollection<PersonViewModel>
    		        	{new BasicPersonViewModel(person), new AdvancePersonViewModel(person)};
    	}
    
    	public ObservableCollection<PersonViewModel> Pages { get; set; }
    }
    

    Properties die in beiden Views benutzt werden, z.B. der Name, wird in PersonViewModel definiert, dadurch aktualisieren sich beide Views sobald sich das Property ändert.



  • Dank dir. Ich komm der Sache schon näher.

    Etwas erschreckt mich nun aber:

    Seit ich nun diesen Codeabschnitt in meinem Programm umgesetzt habe dauert es eine gefühlte Ewigkeit ( > 1 Sekunde) bis das Tabcontrol umschaltet

    <TabControl ItemsSource="{Binding Pages}">
        <TabControl.ItemTemplate>
            <DataTemplate>
                <TextBlock Text="{Binding Title}" />
            </DataTemplate>
        </TabControl.ItemTemplate>
        <TabControl.Resources>
            <DataTemplate DataType="{x:Type Local:FirstViewModel}">
                <Controls:FirstView />
            </DataTemplate>
            <DataTemplate DataType="{x:Type Local:SecondViewModel}">
                <Controls:SecondView />
            </DataTemplate>
        </TabControl.Resources>
    </TabControl>
    


  • Mach mal die UserControls leer bis auf eine TextBlock oder so.
    Ist es dann immer noch langsam?

    Ich vermute das liegt an den Bindings innerhalb der UserControls selber, diese werden erst ausgewertet sobald du das entsprechende Tab ausgewählt hast. (Beim ersten der direkt sichtbar ist (sofern so eingestellt) lädt es sobald das Fenster öffnet, dadurch merkst du sowas vermutlich nicht.)



  • Ja wenn ich nur nen Textblock rein mache geht es schnell.
    Was kann ich dagegen unternehmen. Und wieso geht es nur so langsam wenn ich es über DataTemplate mache?



  • Zeitpuntk des Ladens.
    Wenn du es direkt rein packs (Statische Elemente statt Liste) dann wird alles beim Start des Fensters geladen (dadurch merkst du es nicht).
    Ansonsten wird ein TabItem erst vollständig geladen wenn du es auswählst (Fang dir mal das Loaded Event des UserControls in dessen Codebehind rein, dann merkst du wann das rein kommt).

    Dagegen machen? Standard Performance beachten.

    Erster Anfang wäre die Xaml selber zu überarbeiten,
    - unnötige Controls raus schmeißen (Grid als einzelnes Element im Grid usw.)
    - Teure Controls sofern möglich gegen günstige austauschen (Das Grid ist sehr teuer, ein StackPanel oder DockPanel ist günstiger)
    - Farbverläufe, Animationen usw in die Resourcen auslagern (dann kann es gefreezed werden)
    - Wenn Daten im Model lazy geladen werden, dann eventuell über ein PriorityBinding nachdenken

    Jemand der sich auskennt müsste sich mal die echten UserControls anschauen und ggf optimieren.

    Kommentier doch mal Sachen ein und aus, eventuell findest du einen Übeltäter.



  • Dagegen machen? Standard Performance beachten.

    Versteh ich nicht was du mir damit sagen willst.



  • Na das man die Standardmittel ausnutzt um die Performance zu verbessern. http://msdn.microsoft.com/en-us/library/aa970683.aspx

    Ein paar Punke habe ich ja aufgeführt die mir als erstes einfallen.

    => Da gibt aber kein Allheilmittel. Was am besten funktioniert weiß man nur wenn man den echten Code kennt.

    //Edit
    Ich hatte auch schon mehrere TabControls derart in Benutzung, aber noch nie Performanceprobleme. Man muss nun forschen woher die kommen bzw, wie ich oben schon sagte, wer der Übeltäter ist.



  • Es liegt denke ich nicht am Tabcontrol sondern am Inhalt des ersten Tabs

    Das erste Tab beinhaltet ein UserControl.

    Dieses UserControl hat ein ItemsControl die eine Liste aus weiteren Usercontrols ist. Dieses UserControl beinhaltet wieder ein Itemscontrol mit Usercontrols. Und das ganze dann noch einmal. Das letzte ist dann nur noch eine Combobox.

    Ich habe sozusagen eine 3 Fache Rekursion.

    Vieleicht ein Beispiel: Oberste Ebene ist ein Haus (UserControl_1). Dies beinhaltet eine Liste von Zimmern (UserControl_2). Jedes Zimmer beinhaltet eine Liste von Bereichen (Usercontrol_3). Jeder Bereich beinhaltet eine Liste von Stühlen (UserControl_4).

    Besonderheit alles ist symetrisch. Also jedes Zimmer besteht aus der selben Anzahl von Bereichen, jeder Bereich hat die selbe Anzahl von Stühlen. Letzendlich kann man dann für jeden Stuhl über die Combobox eine individuele Farbe geben.

    Die Usercontrols bestehen fast ausschliesslich aus Itemcontrols. Maximal noch ein Label oder Button dazu. Panel ist dann immer ein Stackpanel.

    Die Rekursion macht hier das ganze langsam. Habe ich z.B. 12 Zimmer. Jedes Zimmer hat 2 Bereiche und jeder Bereich 4 Stühle. Sind dann insgesammt 96 Stühle dementsprechend 96 Comboboxen. Stellt die Combobox nur 3 Farben zur Auswahl wird die Applikation so langsam, dass wenn das Programm aus VS gestartet wird, die UserControls teilewiese gar nicht vollständig angezeigt werden. Erst nach einem Tabwechsel.

    Aussehen ist komplett in Resourcen ausgelagert. Die Geschwindigkeit erhöht sich in der Relation wie ich die untergeordneten Usercontrols wegnehme. Also es gibt nicht den "einen" Übeltäter.



  • (Von den Inhalt der Tabs habe ich doch geredet)

    Die Massen von UserControls sind die Übeltäter.

    Das Control vom Typen "UserControl" ist recht teuer.
    Warum sind es überhaupt UserControls wenn es so wenig beinhaltet?
    Geht doch alles über DataTemplates. Und so wie du es schilderst klingt das gar nach einer TreeView 😃

    Ein UserControl nur für eine ComboBox ist schlichtweg gesagt Blödsinn.
    Genauso ein UserControl das nur eine Liste von weiteren Controls hält, auch quatsch, pack doch die Liste direkt rein.

    Du solltest deine Exzessive Benutzung der UserControls überdenken.


Anmelden zum Antworten