Viewmodels werden zweimal erzeugt

  • WPF

Es gibt 6 Antworten in diesem Thema. Der letzte Beitrag () ist von KingLM97.

    Viewmodels werden zweimal erzeugt

    Hallo zusammen,

    hab mich nebenbei wieder mal mit WPF & MVVM beschäftigt, nach einiger Abwesenheit ^^

    Hab mir also mein Grundlagen aufgebaut und als erstes Navigation eingebaut. Diese funktioniert auch sehr gut, allerdings habe ich ein Problem: meine ViewModels werden zweimal aufgerufen, sodass am Ende meine Bindings nicht funktionieren da die entsprechenden Eigenschaften nicht gesetzt sind.

    Für die ViewModels habe ich meine Basisklasse:
    Spoiler anzeigen

    C#-Quellcode

    1. ​public abstract class ViewModelBase : ModelBase
    2. {
    3. private static readonly string[] HostProcesses = new string[] { "XDesProc", "devenv", "WDExpress", "WpfSurface" };
    4. public abstract string Name { get; }
    5. protected bool IsInDesignMode
    6. {
    7. get
    8. {
    9. return HostProcesses.Contains(Process.GetCurrentProcess().ProcessName);
    10. //return (bool)(DesignerProperties.IsInDesignModeProperty.GetMetadata(typeof(DependencyObject)).DefaultValue);
    11. }
    12. }
    13. public abstract void Initialize();
    14. }

    Mit der Methode ​Initialize() rufe ich Viewmodelspezifische Methoden zu Initialisierung auf (zum Beispiel laden von Einstellungen).

    Zum Navigieren benutze ich folgende Methode:
    Spoiler anzeigen

    C#-Quellcode

    1. ​void NavigateTo(Navigate obj)
    2. {
    3. switch (obj)
    4. {
    5. case Navigate.Sql:
    6. CurrentViewModel = ViewModelContainer.Instance.GetViewModel(typeof(SqlViewModel));
    7. break;
    8. case Navigate.Ftp:
    9. CurrentViewModel = ViewModelContainer.Instance.GetViewModel(typeof(FtpViewModel));
    10. break;
    11. case Navigate.Übersicht:
    12. break;
    13. }
    14. CurrentViewModel?.Initialize();
    15. RaisePropertyChanged(nameof(CurrentViewModel));
    16. }


    Und hier hole ich mir das richtige Viewmodel:
    Spoiler anzeigen

    C#-Quellcode

    1. ​public sealed class ViewModelContainer
    2. {
    3. private static readonly Lazy<ViewModelContainer> lazy = new Lazy<ViewModelContainer>(() => new ViewModelContainer());
    4. public static ViewModelContainer Instance { get { return lazy.Value; } }
    5. private List<ViewModelBase> viewModels = new List<ViewModelBase>();
    6. private ViewModelContainer()
    7. {
    8. }
    9. public ViewModelBase GetViewModel(Type type)
    10. {
    11. var vm = viewModels.SingleOrDefault(v => v.GetType() == type);
    12. if (vm == null)
    13. {
    14. var newVM = (ViewModelBase)Activator.CreateInstance(type);
    15. viewModels.Add(newVM);
    16. return newVM;
    17. }
    18. return vm;
    19. }
    20. }


    Das funktioniert auch alles wundar, ich kann navigieren und die richtige View wird auch angezeigt, aber nicht die Daten, die ich in ​Initialize() lade.
    Hier ein Beispiel wie das aktuell aussieht:
    Spoiler anzeigen

    C#-Quellcode

    1. ​public override void Initialize()
    2. {
    3. Databases = new ObservableCollection<string>();
    4. LoadSqlSettings();
    5. }
    6. private void LoadSqlSettings()
    7. {
    8. var fileContent = SaveAndLoad.ReadFile("SqlSettings");
    9. if (!string.IsNullOrWhiteSpace(fileContent))
    10. {
    11. SqlSettings = SaveAndLoad.Deserialize<SqlSettings>(fileContent);
    12. RaisePropertyChanged();
    13. }
    14. else
    15. {
    16. SqlSettings = new SqlSettings();
    17. }
    18. }


    Meine Hauptview ist folgende:
    Spoiler anzeigen

    XML-Quellcode

    1. ​<Window x:Class="aixACC_Sonepar_Import.MainWindow"
    2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    4. xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    5. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    6. xmlns:local="clr-namespace:aixACC_Sonepar_Import"
    7. xmlns:VM="clr-namespace:aixACC_Sonepar_Import.ViewModels;assembly=aixACC_Sonepar_Import.ViewModels"
    8. xmlns:Views="clr-namespace:aixACC_Sonepar_Import.Views"
    9. xmlns:Shared="clr-namespace:aixACC_Sonepar_Import.Shared;assembly=aixACC_Sonepar_Import.Shared"
    10. mc:Ignorable="d"
    11. Title="Sonepar Bestellimport" Height="450" Width="800">
    12. <Window.DataContext>
    13. <VM:MainWindowViewModel />
    14. </Window.DataContext>
    15. <Window.Resources>
    16. <DataTemplate DataType="{x:Type VM:SqlViewModel}">
    17. <Views:Sql />
    18. </DataTemplate>
    19. <DataTemplate DataType="{x:Type VM:FtpViewModel}">
    20. <Views:Ftp />
    21. </DataTemplate>
    22. </Window.Resources>
    23. <DockPanel>
    24. <Menu DockPanel.Dock="Top">
    25. <MenuItem Header="Übersicht" Command="{Binding Path=NavigateCommand}" CommandParameter="{x:Static Shared:Navigate.Übersicht}" />
    26. <MenuItem Header="Einstellungen">
    27. <MenuItem Header="SQL-Verbindung" Command="{Binding Path=NavigateCommand}" CommandParameter="{x:Static Shared:Navigate.Sql}" />
    28. <MenuItem Header="FTP-Verbindung" Command="{Binding Path=NavigateCommand}" CommandParameter="{x:Static Shared:Navigate.Ftp}"/>
    29. </MenuItem>
    30. </Menu>
    31. <ContentControl Content="{Binding Path=CurrentViewModel}" />
    32. </DockPanel>
    33. </Window>


    Ich bin da aktuell ein wenig überfragt, warum genau meine Viewmodels zweimal erzeugt werden.
    Über jeden Tipp bin ich dankbar!

    KingLM97 schrieb:

    C#-Quellcode

    1. private static readonly Lazy<ViewModelContainer> lazy = new Lazy<ViewModelContainer>(() => new ViewModelContainer());
    Ich habe noch nicht mit Lazy gearbeitet, aber wenn ich diese Zeile richtig verstehe, wird hier eine anonyme Methode gespeichert die einen ViewModelContainer zurückgibt und beim Aufruf des statischen Feldes lazy ausgeführt wird.

    Wenn nun also jemand die statische Eigenschaft Instance aufruft, so wird das statische Feld lazy aufgerufen, was wiederum dazu führt, dass die anonyme methode aufgerufen wird, was dazu führt, dass man einen neuen ViewModelContainer erhält.
    Das Lazy ist (wenn auch an dieser Stelle unnötig) in Ordnung. Die Factory Funktion wird nur ein einziges Mal aufgerufen (beim Zugriff auf lazy.Value), danach wird die Instanz im Hintergrund gecached. Man hat somit nur einen ViewModelContainer in der ganzen App.



    @KingLM97 Für mich fehlen hier ein paar Infos, um dem Problem auf die Spur zu kommen. Beispielsweise bräuchte ich wsl. den gesamten MainWindowViewModel Code um den Flow deiner App zu verstehen. Woran erkennst du denn, dass sicher mehrere VM Instanzen erstellt werden? Die Implementierung deines ViewModelContainers stellt ja sicher, dass das nicht passiert. Benutzt du ihn denn für jedes VM? Für das MainWindowViewModel ja scheinbar nicht, denn dieses wird im XAML Code erzeugt (in <Window.DataContext>).

    Anhand deines Codes den du bisher gepostet hast würde ich das Problem an einer anderen Stelle vermuten (aber wie gesagt, mir fehlt der Code des Main-VMs, also ist das Folgende nur geraten!):

    C#-Quellcode

    1. public override void Initialize()
    2. {
    3. Databases = new ObservableCollection<string>();
    4. LoadSqlSettings();
    5. }
    6. private void LoadSqlSettings()
    7. {
    8. var fileContent = SaveAndLoad.ReadFile("SqlSettings");
    9. if (!string.IsNullOrWhiteSpace(fileContent))
    10. {
    11. SqlSettings = SaveAndLoad.Deserialize<SqlSettings>(fileContent);
    12. RaisePropertyChanged();
    13. }
    14. else
    15. {
    16. SqlSettings = new SqlSettings();
    17. }
    18. }

    In Zeile #13 rufst du nur RaisePropertyChanged(); ohne irgendeinem Property-Namen auf. Davon ausgehend dass diese Hilfsfunktion per [CallerMemberName] funktioniert würde hier als Property-Name automatisch "LoadSqlSettings" und nicht deine vermutlich beabsichtigte "SqlSettings" Property genutzt werden. Hast du denn im "SqlSettings" Property Setter auch einen "RaisePropertyChanged();" Call drin?

    Wenn das nicht das Problem ist, poste doch vllt mal das Projekt selbst - es scheint ja noch in den Anfängen zu stecken (und damit recht klein zu sein).
    Danke für eure antworten, das mit dem Lazy ist schon richtig, wie @shad es bereits erläutert hat.

    Ich habe den Code jetzt nicht mehr zu Hand, aber folgendes kurz erklärt:

    shad schrieb:

    Woran erkennst du denn, dass sicher mehrere VM Instanzen erstellt werden?

    Ich habe Testweiße einen Haltepunkt im Konstruktur gesetzt und dieser ist zweimal angesprungen.


    Meine NavigateTo-Methode wird durch einen Command aufgerufen (im View über das Menü). Als Parameter entsprechend den Enum-Wert, wohin ich navigieren möchte. Entsprechend hole ich mir von meinem ViewModelContainer das richtige Viewmodel und weiße es der Eigenschaft ​CurrentViewModel zu (woran auch mein ContentPresenter gebunden ist). Anhand dessen wird die View auch richtig angezeigt. Das navigieren funktioniert also korrekt, nur werden die Daten nicht angezeigt, da ​Initialize beim zweiten "mysteriösen" erstellen der Klasse nicht aufgerufen wird.

    Jetzt beim schreiben wirds mir vermutlich klar:
    In den Views habe ich ebenfalls den DataContext auf das richtige Viewmodel händisch gesetzt und deswegen wird das Viewmodel zweimal aufgerufen?
    Wenn ich bei der Annahme korrekt bin, wie mache ich das besser?

    Dieser Beitrag wurde bereits 1 mal editiert, zuletzt von „KingLM97“ ()

    KingLM97 schrieb:

    In den Views habe ich ebenfalls den DataContext auf das richtige Viewmodel händisch gesetzt und deswegen wird das Viewmodel zweimal aufgerufen?
    Wenn ich bei der Annahme korrekt bin, wie mache ich das besser?


    Das ist dann wahrscheinlich das Problem. Der Trick ist, wie du dir schon denken kannst, den DataContext nicht 2-mal zu setzen, bzw. nicht auf 2 verschiedene Instanzen. Gibt mehrere Möglichkeiten, das zu tun. Die leichteste ist, den DataContext nicht im XAML zu setzen, sondern im View-Konstruktor (also im Code-Behind). Hier kannst du dann auch deinen Container nutzen.

    C#-Quellcode

    1. class View : UserControl {
    2. public View() {
    3. InitializeComponent();
    4. DataContext = ViewModelContainer.Instance.GetViewModel(typeof(ViewModel));
    5. }
    6. }


    Kann man sicherlich auch anders lösen, aber man muss sich das Leben ja nicht schwer machen.
    Hallo

    Das problem ist viel simpler.
    Du setzt den DataContext im Window selbst. Das darfst du nicht. Sobald der DataContext des Window oder eines UserControls im XAML gesetzt wird ruft die WPF den Parameterlosen Konstruktor auf.
    Das kannst du einfach testen indem du einen Konstruktor mdit irgendeinem Parameter erstellst und den parameterlosen rausschmeißt. Dann meckert der Designer sofort.

    Aus diesem Grund setzt man im View bei MVVM auch nur den Designtime-DataContext und macht den Rest über Code.
    Der Idealweg ist das du nur ein Window hast und den Rest nur über UserControls machst. Das laden der UserControls geschied über DataTemplates.

    Grüße
    Sascha
    If _work = worktype.hard Then Me.Drink(Coffee)
    Seht euch auch meine Tutorialreihe <WPF Lernen/> an oder abonniert meinen YouTube Kanal.

    ## Bitte markiere einen Thread als "Erledigt" wenn deine Frage beantwortet wurde. ##

    Ok, dann werde ich morgen mal schauen wie sich das anders lösen lässt.

    Ich hatte schon versucht die DataTemplates anhand des Enums zu verwenden

    XML-Quellcode

    1. ​<DataTemplate x:Key="FtpViewKey" DataType="{x:Static Shared:Navigate.Ftp}">
    2. <Views:Ftp />
    3. </DataTemplate>


    Aber ich kann ja schlecht mein ContentPresenter an eine Enum-Eigenschaft binden, oder nicht?