Benutzer mit den meisten Antworten
MVVM mehrere Views in Tabs geöffnet, beim wechseln der Tabs sollen die Views nicht aktualisieren/verändern

Frage
-
Hallo Zusammen,
ich befasse mich seit kurzem mit dem MVVM in WPF. Entwickelt wird mit Visual Studio 2013 und C#.
Ich kann links in der Anwendung neue Views erzeugen, die sich dann im mittleren Bereich in Tabs in einem Tabcontrol öffnen.Nun hat ein Arbeitsbereich beim erstmaligen öffnen ein gewisses Aussehen, z.B. befinden sich mehrere Expander darin,
die beim ersten erzeugen der View per Default geschlossen sind.Nehmen wir an, der Anwender arbeitet nun in diesem Arbeitsbereich und hat ein paar Expander geöffnet und muss nun
aus irgendeinem Grund in einen Arbeitsbereich (Tab) wechseln.Wenn der Anwender danach wieder zum ursprünglichen Tab zurückkehrt, dann sind die Expander alle wieder geschlossen.
Ich möchte aber, dass diese View genauso aussieht, wie zu dem Zeitpunkt, als er sie verlassen hat.Gibt es dafür eine generelle "einfache" Vorgehensweise, oder muss ich mir für jedes einzelne Element in der View so eine
Art "Merkmechanismus" schreiben?Viele Grüße
Torsten
Antworten
-
Hi Torsten,
ich habe das Beispiel mal nachgestellt und kann den von Dir genannten Zustand nicht reproduzieren (VS 2013, C# 4.5). Vermutlich ist da in Deinem Code noch ein Fehler. Hier mal meine Demo:<Window x:Class="WpfApplication1.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" xmlns:local="clr-namespace:WpfApplication1"> <Window.Resources> <local:ViewModel x:Key="vm"/> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource vm}}"> <TabControl> <TabControl.Items> <TabItem Header="Tab 1"> <Expander> <DataGrid ItemsSource="{Binding View1}" /> </Expander> </TabItem> <TabItem Header="Tab 2"> <Expander> <DataGrid ItemsSource="{Binding View2}" /> </Expander> </TabItem> </TabControl.Items> </TabControl> </Grid> </Window>
Dazu der ViewModel:
using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows.Data; namespace WpfApplication1 { class ViewModel { Random rnd = new Random(); public ViewModel() { // Produktliste = new ObservableCollection<Produkt>(); ProdukteLaden(); } private CollectionViewSource cvs1; public ICollectionView View1 { get { if (cvs1 == null) { cvs1 = new CollectionViewSource(); cvs1.Source = Produktliste; } return cvs1.View; } } private CollectionViewSource cvs2; public ICollectionView View2 { get { if (cvs2 == null) { cvs2 = new CollectionViewSource(); cvs2.Source = Produktliste; } return cvs2.View; } } public ObservableCollection<Produkt> Produktliste { get; set; } void ProdukteLaden() { for (int i = 0; i < 10; i++) { Produktliste.Add(new Produkt() { ProduktName = "Produkt " + i.ToString(), Cash = 1000 * rnd.NextDouble() }); } } } class Produkt { public string ProduktName { get; set; } public double Cash { get; set; } } }
--
Peter- Als Antwort vorgeschlagen Peter Fleischer Montag, 14. April 2014 20:06
- Als Antwort markiert Torsten-Müller Dienstag, 15. April 2014 08:16
-
Hi Torsten,
auch die programmatische Steuerung klappt bei mir problemlos. Hier mal meine Demo:XAML:
<Window x:Class="WpfApplication1.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" xmlns:local="clr-namespace:WpfApplication1"> <Window.Resources> <local:ViewModel x:Key="vm"/> </Window.Resources> <StackPanel DataContext="{Binding Source={StaticResource vm}}"> <Button Content="nächster" Command="{Binding NeuerTab}" Margin="5"/> <TabControl ItemsSource="{Binding Tabs}" Margin="5"/> </StackPanel> </Window>
Dazu die ViewModels:
using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; namespace WpfApplication1 { class ViewModel { Random rnd = new Random(); public ViewModel() { Produktliste = new ObservableCollection<Produkt>(); ProdukteLaden(); // TabListe = new ObservableCollection<TabItem>(); this.NeuerTab = new RelayCommand(CmdNeuerTab); } private ObservableCollection<TabItem> TabListe { get; set; } private CollectionViewSource cvs0; public ICollectionView Tabs { get { if (cvs0 == null) { cvs0 = new CollectionViewSource(); cvs0.Source = TabListe; } return cvs0.View; } } public ICommand NeuerTab { get; set; } private int index = 0; private void CmdNeuerTab(object obj) { if (index<5) { index++; TabItem t = new TabItem { Header = "Tab " + index.ToString() }; Expander exp = new Expander(); t.Content = exp; ViewModel2 vm = new ViewModel2(Produktliste); exp.Content = new DataGrid { ItemsSource = vm.View2 }; TabListe.Add(t); } else { cvs0.View.MoveCurrentTo(TabListe[rnd.Next(0,5)]); } } public ObservableCollection<Produkt> Produktliste { get; set; } void ProdukteLaden() { for (int i = 0; i < 10; i++) { Produktliste.Add(new Produkt() { ProduktName = "Produkt " + i.ToString(), Cash = 1000 * rnd.NextDouble() }); } } } class ViewModel2 { public ViewModel2(object Produktliste) { cvs2 = new CollectionViewSource(); cvs2.Source = Produktliste; } private CollectionViewSource cvs2; public ICollectionView View2 { get { return cvs2.View; } } } class Produkt { public string ProduktName { get; set; } public double Cash { get; set; } } }
Klicke auf den Button und ändere zwischendurch den Zustand des Expanders die Sortierfolge im Grid. Nach mehrmaligem Klicken erscheinen die vorherigen Einstellungen unverändert.
Du solltest also weiter die Fehler in Deinen ViewModels suchen.
--
Peter- Als Antwort vorgeschlagen Peter Fleischer Montag, 14. April 2014 20:06
- Als Antwort markiert Torsten-Müller Dienstag, 15. April 2014 08:16
Alle Antworten
-
Hi Torsten,
wenn du den Inhalt des Tabs nicht neu Lädst (bei einen Tab wechsle), sollte er eigentlich im alten Zustand bleiben. Ok ich hab es jetzt nicht ausprobiert, aber ein anders verhalten würde mich wundern.
Welches Event benutzt du um die Inhalte zu füllen.
Mit freundlichen Grüßen
Björn
-
Hi Björn,
Danke für deine Antwort.
Im ViewModel rufe ich im Standard Konstruktor ein paar Methoden auf,
die mir unterschiedliche Collections mit Daten füllen.Diese werden beim Tabwechsel auch nicht wieder neu geladen, was mir zeigt,
dass das ViewModel nicht wieder neu Instanziiert wird.In der View ist die IsExpanded Eigenschaft der Expander auf False gesetzt,
damit sie beim ersten Laden der View eben geschlossen sind.Wenn ich nun ein paar Expander öffne und über die Tabs die View wechsle und
wieder zurückkehre, sind die Expander wieder geschlossen also scheint sich
die View zu aktualisieren, was ich aber nicht "veranlasse".
Ich hab keine Ahnung wie ich das unterbinden soll. -
Hallo Zusammen,
jetzt gibt es noch ein weiteres Problem.
Hier ein kleiner Auszug aus meinem ViewModel:
#region Properties private ObservableCollection<TestDaten> _testDaten { get; set; } public ObservableCollection<TestDaten> _TestDaten { get { return _testDaten; } set { _testDaten = value; this.OnPropertyChanged("_TestDaten"); } } private ICollectionView _testDatenView { get; set; } public ICollectionView _TestDatenView { get { return _testDatenView; } set { _testDatenView = value; this.OnPropertyChanged("_TestDatenView"); } } #endregion Properties #region Konstruktor public TestDatenViewModel() { TestDatenHolen(); } #endregion Konstruktor #region Testdaten holen private void TestDatenHolen() { TestDatenDienstClient client = new TestDatenDienstClient(); try { //ObservableCollection mit Daten füllen _TestDaten = client.GetTestDaten(); client.Close(); //ICollectionView füllen _TestDatenView = new CollectionViewSource() { Source = _TestDaten }.View; _TestDatenView.SortDescriptions.Add( new SortDescription("Datum", ListSortDirection.Descending)); var liveShaping = _TestDatenView as ICollectionViewLiveShaping; if (liveShaping.CanChangeLiveSorting) { liveShaping.IsLiveSorting = true; liveShaping.LiveSortingProperties.Add("Datum"); } } catch (Exception ex) { ErrorInfoServices.Meldung = "Testdaten konnten nicht geholt werden: " + ex.Message; ErrorInfoServices.Farbe = new SolidColorBrush (Colors.Red); } } #endregion Testdaten holen
Wenn ich also den Tab durch klicken im Menü neu erzeuge, dann werden mir die Testdaten nach Datum sortiert
in einem DataGrid dargestellt.
Nun öffne ich einen weiteren anderen Tab und kehre dann wieder zu diesem hier zurück. Dann ist mein DataGrid plötzlich nicht mehr nach Datum, sondern nach Name sortiert und das ist noch nicht alles.
Wenn ich dann im DataGrid auf einen Spaltenkopf zum umsortieren Klicke, bekomme ich die Fehlermeldung, dass
der Objektverweiß nicht auf eine Objektinstanz festgelegt wurde.
Ich verstehe jetzt nicht ganz, warum die bei der Erzeugung erschaffene Objektinstanz nach dem Tabwechsel
auf einmal nicht mehr da ist.
Hat hier jemand eine Idee?Danke und Gruß
Torsten
-
Hi Björn,
das Gefühl habe ich irgendwie auch, nur kann ich den Fehler eben nicht feststellen.Als Auszug hier das gekürzte DataGrid, was an die oben beschriebene ICollectionView bindet.
<DataGrid ItemsSource="{Binding _TestDatenView}" SelectedItem="{Binding _TestDatenSatz}" IsSynchronizedWithCurrentItem="True" AutoGenerateColumns="False" CanUserAddRows="False" CanUserDeleteRows="False" SelectionMode="Single" SelectionUnit="FullRow"> <DataGrid.Columns> <DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="Auto"/> <DataGridTextColumn Header="Datum" Binding="{Binding Datum}" Width="Auto"/> </DataGrid.Columns> </DataGrid>
Der _TestDatenSatz der hier an SelectedItem gebunden ist, ist ein einzelnes Objekt vom Typ TestDaten und sieht so aus.
private TestDaten _testDatenSatz { get; set } public TestDaten _TestDatenSatz { get { return _testDatenSatz; } set { _testDatenSatz = value; this.OnPropertyChanged("_TestDatenSatz"); } }
Ich hab die ItemsSource des DataGrids inzwischen auch mal an die ObservableCollection anstatt an die ICollectionView gebunden.
Wenn ich da einen Tabwechsel mache, kann ich nachher das DataGrid durch klicken auf den Spaltenkopf
umsortieren, ohne das die Objektinstanz weg ist, so wie es bei der ICollectionView der Fall ist.Ich komm einfach nicht drauf, warum sich das so verhält.
-
Hi Torsten,
für mich kling es jetzt so als ob irgendwo die CollectionView neu gesetzt wir. Setzt da mal einen Braekpoint.
An den Code ausschnitte, die ich hier jetzt sehe, kann ich nichts erkennen.
Der Tab wechsel, sollte eigendlich keine Auswirkung haben, ausser wenn du da irgendwas mit dem ViewModel anstellst. z.B. neu Erzeugen für die 2. Tab.
MFG
Björn
-
Ja irgendwie scheint das so zu sein, aber die ICollectionView wird nicht neu gesetzt, sondern "vernichtet".
Das komische ist, dass das mit der ObservableCollection auf die die View bindet aber eben nicht passiert und die stehen ja wie du oben siehst in der selben Methode.
Die Tabs bauen sich so auf wie im folgenden Artikel, dem wahrscheinlich die meisten die mit MVVM beginnen
folgen.Ups . . . Scheinbar darf ich hier keinen Link einsetzen, weil ich meine Antwort sonst nicht senden kann.
Naja, bei der Suchmaschine deines Vertrauens solltest du aber mit dem folgenden Suchtext gleich als ersten
Treffer den MSDN Artikel erhalten."WPF-Anwendungen mit dem Model-View-ViewModel-Entwursmuster"
Ich werde mir da morgen mal nen Breakpoint setzen und schauen, ob ich den "Übeltäter" entlarven kann und dann melde ich mich wieder.
Bis dahin schon mal Danke an dich! -
Hi Björn,
ich hatte jetzt den Haltepunt erst mal in der Methode, die die Daten holt und wo auch die View gesetzt wird.
Dort passiert beim Tabwechsel nichts. Die Methode wird also nicht aufgerufen.
Aber das ViewModel will sich beim Tabwechsel über den "Getter" die View erneut holen und zwar an der folgenden Stelle.private ICollectionView _testDatenView { get; set; } public ICollectionView _TestDatenView { //In dieser Zeile greift der Haltepunkt nach dem Tabwechsel get { return _testDatenView; } set { _testDatenView = value; this.OnPropertyChanged("_TestDatenView"); } }
Kann ich das verhalten jetzt irgendwie verhindern? Macht es überhaupt Sinn das zu verhindern?
- Bearbeitet Torsten-Müller Montag, 14. April 2014 08:10 Schreibfehler
-
Hi Torsten,
ich habe das Beispiel mal nachgestellt und kann den von Dir genannten Zustand nicht reproduzieren (VS 2013, C# 4.5). Vermutlich ist da in Deinem Code noch ein Fehler. Hier mal meine Demo:<Window x:Class="WpfApplication1.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" xmlns:local="clr-namespace:WpfApplication1"> <Window.Resources> <local:ViewModel x:Key="vm"/> </Window.Resources> <Grid DataContext="{Binding Source={StaticResource vm}}"> <TabControl> <TabControl.Items> <TabItem Header="Tab 1"> <Expander> <DataGrid ItemsSource="{Binding View1}" /> </Expander> </TabItem> <TabItem Header="Tab 2"> <Expander> <DataGrid ItemsSource="{Binding View2}" /> </Expander> </TabItem> </TabControl.Items> </TabControl> </Grid> </Window>
Dazu der ViewModel:
using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows.Data; namespace WpfApplication1 { class ViewModel { Random rnd = new Random(); public ViewModel() { // Produktliste = new ObservableCollection<Produkt>(); ProdukteLaden(); } private CollectionViewSource cvs1; public ICollectionView View1 { get { if (cvs1 == null) { cvs1 = new CollectionViewSource(); cvs1.Source = Produktliste; } return cvs1.View; } } private CollectionViewSource cvs2; public ICollectionView View2 { get { if (cvs2 == null) { cvs2 = new CollectionViewSource(); cvs2.Source = Produktliste; } return cvs2.View; } } public ObservableCollection<Produkt> Produktliste { get; set; } void ProdukteLaden() { for (int i = 0; i < 10; i++) { Produktliste.Add(new Produkt() { ProduktName = "Produkt " + i.ToString(), Cash = 1000 * rnd.NextDouble() }); } } } class Produkt { public string ProduktName { get; set; } public double Cash { get; set; } } }
--
Peter- Als Antwort vorgeschlagen Peter Fleischer Montag, 14. April 2014 20:06
- Als Antwort markiert Torsten-Müller Dienstag, 15. April 2014 08:16
-
Hallo Peter,
vielen Dank für deine Antwort!
Dieser Teil des Problems hat sich durch deinen Code nun gelöst.Nach Änderung von:
private ICollectionView _testDatenView { get; set; } public ICollectionView _TestDatenView { get { return _testDatenView; } set { _testDatenView = value; this.OnPropertyChanged("_TestDatenView"); } }
in:
private CollectionViewSource cvs; public ICollectionView _TestDatenView { get { if (cvs == null) { cvs = new CollectionViewSource(); cvs.Source = _TestDaten; } return cvs.View; } }
Funktioniert es nun.
Ich werde deine Antwort später noch als "Als Antwort markieren" markieren, wenn der Themenstart dann
auch noch gelöst wurde, da ich die Befürchtung habe, dass keiner mehr hier rein schaut, wenn ich das Thema als beantwortet markiere.
Es gibt jetzt praktisch nur noch das Problem, dass sich die geöffnet Expander nach dem Tabwechsel wieder schließen.
Hat da evtl. noch jemand eine Idee zu dem Thema, wie ich den Expandern beibringen kann, dass sie sich
ihr letztes "Aussehen" merken können?Vielen Dank und viele Grüße
Torsten -
Wahrscheinlich liegt es daran, dass du deine Tabs manuell "hart" im Code stehen hast und bei mir werden die Tabs
dynamisch erstellt. Ich verstehe es aber trotzdem noch nicht ganz, denn wenn der Tab != null ist, dann wird in der "TabCollection" nur MoveCurrentTo("Tab") aufgerufen (siehe Code Ausschnitt).void TestDatenWorkspace() { TestDatenViewModel workspace = this.Workspaces.FirstOrDefault(vm => vm is TestDatenViewModel) as TestDatenViewModel; if (workspace == null) { workspace = new TestDatenViewModel(); this.Workspaces.Add(workspace); } this.SetActiveWorkspace(workspace); }
void SetActiveWorkspace(_WorkspaceViewModel workspace) { ICollectionView collectionView = CollectionViewSource.GetDefaultView(this.Workspaces); if (collectionView != null) { collectionView.MoveCurrentTo(workspace); } }
-
Hi Torsten,
auch die programmatische Steuerung klappt bei mir problemlos. Hier mal meine Demo:XAML:
<Window x:Class="WpfApplication1.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" xmlns:local="clr-namespace:WpfApplication1"> <Window.Resources> <local:ViewModel x:Key="vm"/> </Window.Resources> <StackPanel DataContext="{Binding Source={StaticResource vm}}"> <Button Content="nächster" Command="{Binding NeuerTab}" Margin="5"/> <TabControl ItemsSource="{Binding Tabs}" Margin="5"/> </StackPanel> </Window>
Dazu die ViewModels:
using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Input; namespace WpfApplication1 { class ViewModel { Random rnd = new Random(); public ViewModel() { Produktliste = new ObservableCollection<Produkt>(); ProdukteLaden(); // TabListe = new ObservableCollection<TabItem>(); this.NeuerTab = new RelayCommand(CmdNeuerTab); } private ObservableCollection<TabItem> TabListe { get; set; } private CollectionViewSource cvs0; public ICollectionView Tabs { get { if (cvs0 == null) { cvs0 = new CollectionViewSource(); cvs0.Source = TabListe; } return cvs0.View; } } public ICommand NeuerTab { get; set; } private int index = 0; private void CmdNeuerTab(object obj) { if (index<5) { index++; TabItem t = new TabItem { Header = "Tab " + index.ToString() }; Expander exp = new Expander(); t.Content = exp; ViewModel2 vm = new ViewModel2(Produktliste); exp.Content = new DataGrid { ItemsSource = vm.View2 }; TabListe.Add(t); } else { cvs0.View.MoveCurrentTo(TabListe[rnd.Next(0,5)]); } } public ObservableCollection<Produkt> Produktliste { get; set; } void ProdukteLaden() { for (int i = 0; i < 10; i++) { Produktliste.Add(new Produkt() { ProduktName = "Produkt " + i.ToString(), Cash = 1000 * rnd.NextDouble() }); } } } class ViewModel2 { public ViewModel2(object Produktliste) { cvs2 = new CollectionViewSource(); cvs2.Source = Produktliste; } private CollectionViewSource cvs2; public ICollectionView View2 { get { return cvs2.View; } } } class Produkt { public string ProduktName { get; set; } public double Cash { get; set; } } }
Klicke auf den Button und ändere zwischendurch den Zustand des Expanders die Sortierfolge im Grid. Nach mehrmaligem Klicken erscheinen die vorherigen Einstellungen unverändert.
Du solltest also weiter die Fehler in Deinen ViewModels suchen.
--
Peter- Als Antwort vorgeschlagen Peter Fleischer Montag, 14. April 2014 20:06
- Als Antwort markiert Torsten-Müller Dienstag, 15. April 2014 08:16
-
-
Noch ein kleiner Nachtrag von meiner Seite.
Ich habe jetzt abschließend noch mal die MVVM Demo App aus dem MSDN Artikel
"WPF Anwendungen mit dem Model View ViewModel Entwursmuster"
so bearbeitet, dass ich in beiden dort vorkommenden Views je einen Expander eingebaut habe.
Die Expander dort verhalten sich genauso wie die in meiner Anwendung (die ja auf der MVVM Demo App basiert).
Dadurch weiß ich schon mal, dass ich den Fehler nicht selbst nachträglich eingebaut habe.