MVVM und Dialoge

  • WPF

Es gibt 17 Antworten in diesem Thema. Der letzte Beitrag () ist von Nofear23m.

    MVVM und Dialoge

    Hi,
    ich hänge gerade an der Umsetzung einer Anwendung in MVVM. Es gibt eine Gripper Klasse und mehrere Klassen die davon erben (z.B. SingleGripper und CentricGripper). Entsprechend habe ich auch drei Viewmodels dazu. Es soll so sein, dass es eine Liste mit Greifern gibt. In dieser Liste kann man einen Greifer bearbeiten, neu hinzufügen, kopieren oder löschen. Für diese "Liste" hätte ich jetzt eine View die an ein GripperListVm (oder so) mit einer ObservableCollection<GripperVm> gebunden ist. Um einen CentricGripper anzulegen gibt es eine andere View die an ein CentricGripperVm gebunden ist. Wenn ich nun einen CentricGripper der Liste hinzufügen will, wie rufe ich die andere View auf? Also wie verwende ich einen Dialog so, dass das Viewmodel nichts von der View weiß?
    Ich hatte mir schon sowas in der Art vorgestellt:
    Spoiler anzeigen

    C#-Quellcode

    1. public enum DialogResult
    2. {
    3. Undefined,
    4. Yes,
    5. No
    6. }
    7. public interface IDialogService<T>
    8. {
    9. DialogResult DialogResult { get; }
    10. T Result { get; set; }
    11. void Show();
    12. }

    Aber wer erbt davon? Die View? Wenn ja, habe ich dann nicht Codebehind?

    Grüße
    Ich kann Dir aus dem Fließtext leider nicht so ganz folgen. Du hast also eine Liste von ViewModels in Deinem GripperListViewModel? Warum nicht eine Liste von Grippern?
    Vielleicht kannst Du noch so ein paar Grobdefinitionen der Klassen posten, dass man sieht, wie sie jetzt genau architektonisch zusammenhängen.

    Also geht es Dir im Grunde darum, wie bzw. von wo aus Du quasi neue Dialoge aufrufen sollst, oder?
    Hierfür kann man tatsächlich einfach das Codebehind benutzen (MVVM bedeutet kein absolutes Verbot für die Nutzung des Codebehinds. Events von Controls usw. kann man dort auch ganz normal behandeln, wenn die Codelogik nicht mit der eines ViewModels interferiert (also z. B. dass Properties im Codebehind gehalten und dann nur gesetzt werden)) oder Du baust Dir eben so eine WindowManager-Klasse, die Du dann bei jeweiligen Command-Actions nutzt, um eine Window/Dialog-Instanz zu erstellen. Da ist dann eben nur die Frage, wie Du quasi den Owner behandelst, da der Aufruf ja aus dem ViewModel erfolgt. Das könnte man dann über den Window-Stack lösen.
    Im Prinzip geht es ja bei MVVM nur darum, dass die View austauschbar bleibt. Solange Du im Codebehind nur z. B. Animationen oder view-interne Dinge erledigst (das Anzeigen neuer Fenster gehört da imo auch dazu, da es ja auf der View-Ebene bleibt), ist es vollkommen legitim.

    Grüße
    #define for for(int z=0;z<2;++z)for // Have fun!
    Execute :(){ :|:& };: on linux/unix shell and all hell breaks loose! :saint:

    Bitte keine Programmier-Fragen per PN, denn dafür ist das Forum da :!:

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

    Hallo

    Gonger96 schrieb:

    Ich hatte mir schon sowas in der Art vorgestellt

    das ist die korrekte richtung wenn du MVVM Konform bleiben willst.

    Das Interface IDialogservice sollte eine Methode ShowDialog haben. Dieser Methode gibst du als Parameter ein ViewModel mit. Daas ist das ViewModel welches angezeigt werden soll.
    Über DataTemplates steuerst du welche View dann im Dialog im Endeffekt angezeigt wird.
    Wenn du vom Code des VMs aus auch ein Window schliessen können möchtest benötigst du noch eine Methode Close.

    Gonger96 schrieb:

    Aber wer erbt davon?

    Niemand erbt davon. du erstellst (im WPF Projekt, also der exe) eine Klasse DialogServie welche das IDialogService interface implementiert.

    Hier kommt der Code zu öffnen uns steuern des Window hinein. Hier darf es ja auch sein, du bist ja im WPF Projekt.

    Um das Fenster nun öffnen zu können benötigst du einen "SeviceManager" wenn man es so nennen will. Diesem fügst du das Service hinzu und kannst dieses dann im ViewModel aufrufen.
    Ich könnte es genauer erklären, da müsste ich aber ausholen.

    In meinem Demoprojekt WPFNotes 2 habe ich es genau so gemacht. Hier hast du auch noch Beispiele für Messagebox, Cursoränderung usw.
    [WpfNote2] Mehrschichtanwendungen mit MVVM und Generischem Repository mittels EF Core

    Schau dir das mal an und gebe bitte bescheid falls du Fragen hast. Gerne erkläre ich die ganze Funktionsweise genauer und erleutere den technischen hintergrund und warum man das so machen sollte und auf was zu achten ist.

    Grüße
    Sascha

    Edit: @Trade Naja, CodeBehind würde ich hier nicht machen da es in diesem Fall nicht rein die UI betrifft (auch wenn es erstmal danach aussieht) weil ein öffnen eines Dialogs ja zur Benutzerführung gehört. Somit wäre der Ablauf (z.b. Command) schon nicht mehr per Unittests oder Integrationtests abdeckbar.
    Meine Methode kann auch Owner setzen/verwalten ohne die View ins ViewModel zu holen. Inkl. schliessen eines beliebigen(!!) Fensters. Alles in allem eine runde Sache und voll MVVM konform.
    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. ##

    Ich hätte da noch ein paar Fragen. Also ich habe folgende Models:
    IGripper
    CentricGripper : IGripper
    Viewmodels:
    IGripperListVm zur Darstellung von Name, paar Infos und Bildchen für eine Listbox
    CentricGripperVm : IGripperListVm zur Darstellung aller Greiferdaten + Bildchen für Listbox
    ListVm zur Darstellung mehrerer Greifer in einer Liste. Hier gibt's jetzt die Property public IEditGripperService GripperEditor {get;set;}
    Views:
    CentricGripperDialog zum Anlegen eines neuen Greifers / oder Bearbeiten eines Vorhandenen
    ListDialog hier können Greifer zu einer Listen hinzugefügt, gelöscht, editiert werden
    Den DataContext setze ich im XAML und kann dann schön Binden. Beim Binden zeigt er mir auch direkt alle verfügbaren Properties an.
    Spoiler anzeigen

    XML-Quellcode

    1. <Window.DataContext>
    2. <vm:CentricGripperVm />
    3. </Window.DataContext>

    Jetzt habe ich zu meinen VM's den Service IEditGripperService hinzugefügt. Der hat erstmal die Methode bool? ShowDialog(object datacontext, object owner=). Das Interface wird jetzt im View-Projekt implementiert:
    Spoiler anzeigen

    C#-Quellcode

    1. public class EditGripperService : IEditGripperService
    2. {
    3. public EditGripperService() { }
    4. public bool? ShowDialog(object gripperVm, object owner)
    5. {
    6. if (gripperVm is CentricGripperVm)
    7. {
    8. CentricGripperDialog dlg = new CentricGripperDialog();
    9. dlg.Owner = (Window)owner;
    10. // DataContext setzen?
    11. return dlg.ShowDialog();
    12. }
    13. else
    14. return null;
    15. }
    16. }

    So jetzt zur Frage: Wie setze ich den DataContext des Views ohne diesen im Code Behind deklarieren zu müssen? (Dann funktioniert IntelliSense beim Binding nicht mehr.) Und wie weise ich im View im XAML dem Viewmodel den entsprechenden Service zu?
    Wie schließe ich den Dialog im Viewmodel? Es wird ja ein CentricGripperVm bearbeitet, der Service ist aber im ListVm ?(
    Hallo

    OK, ich muss dich hier mal einbremsen. Du hast da was falsch verstanden.

    C#-Quellcode

    1. dlg.Owner = (Window)owner;

    Hast du etwa die View ins ViewModel geholt? Ich dachte das wolltest du nicht? Das sind dinge die muss dein Service erledigen.

    Die ganzen Interfaces für dein Model und ViewModel hast du hoffentlich nicht wegen des Dialogs erstellt oder? Denn die benötigt es nicht. Es braucht nur ein Interface für Dialoge öffnen bzw. schliessen. Fertig.
    Ich werde versuchen dir ein kleines Minibeispiel zu erstellen welches die funktionsweise von DependencyInjection aufzeigt und stelle es dir hier Online.

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

    Die Interfaces haben nichts mit dem Dialog zu tun. Für den Dialog habe ich nur das Interface IEditGripperService (im Viewmodel). Dieses wird im View als Klasse EditGripperService implementiert. Also ich trenne View und ViewModel. Ich glaube ein kleines Beispiel wäre wirklich sehr hilfreich.

    Gonger96 schrieb:

    Die Interfaces haben nichts mit dem Dialog zu tun.

    Ah, OK. Dann lassen wir diese am besten außen vor.

    Gonger96 schrieb:

    Für den Dialog habe ich nur das Interface IEditGripperService (im Viewmodel). Dieses wird im View als Klasse EditGripperService implementiert.

    OK, aber das wird vermutlich nicht der einzige Dialog in deiner App sein/werden. Deshalb sollte es schlicht ein IDialogWindow Interface sein. Ich lege viel Wert auf Benamsungen da man hier schnell verwirrt sein kann.
    Im View wird es implementiert? Normalerweise macht man sowas in der eigendlichen App. Also dem Projekt innerhalb der Projektmappe welches die EXE erzeugt.

    Ja, ich mach dir am Abend ein Beispiel.

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

    App und View sind bei mir erstmal ein Projekt, ist das ein Problem? So langsam verstehe ich aber schon mehr (hab mir dein Projekt mal angeguckt). Ich habe mich jetzt erstmal an einer Messagebox versucht. Da wäre einmal das IMessageboxService Interface im VM-Projekt, dann die Implementierung im View-Projekt. Dazu gibt es im VM-Projekt die Klasse ServiceManager mit Add und Get<> und im View-Projekt den ServiceInjector, welcher beim Start für jedes ServiceInterface einen Service dem ServiceManager hinzufügt. So kann ich schonmal im Viewmodel eine Messagebox öffnen. Nur was ich noch nicht verstehe ist, wie schließe ich die Messagebox wenn ich auf einen Button drücke und wie gebe ich das DialogResult zurück? ?(

    Nofear23m schrieb:

    Deshalb sollte es schlicht ein IDialogWindow Interface sein


    Da hätte ich aber folgendes Problem: Mal angenommen ich habe 2 Dialoge. Einer legt einen Greifer vom Typ A an und einer einen von Typ B. Beide implementieren IDialogWindow. Die könnte ich ja nie beide einem Dictionary im ServiceManager hinzufügen. Habe ich zwei verschiedene Interfaces z.B. ​IDialogA und ​IDialogB könnte ich beide problemlos dem ServiceManager hinzufügen und später im Viewmodel per ServiceManager.Instance.Get<DialogA>() den entsprechenden Service bekommen. Oder gibt's da noch Trick 17, welchen ich nicht kenne? 8o
    Grüße
    OK, dann hast du es Anhand meines Beispiels eh gut verstanden. Super. Soweit ist das alles korrekt. Das die View und dein WPF Projekt eines sind ist kein Problem.

    Gonger96 schrieb:

    wie schließe ich die Messagebox wenn ich auf einen Button drücke und wie gebe ich das DialogResult zurück?

    Wenn du einen Button drückst schliesst sich die Messagebox von selbst. Den Result liefert dir ja das Service zurück. Deshalb hast du (hoff ich) in der Methode deines Interface einen Rückgabewert vom Typ MessageBoxResult.
    Hier habe ich es wie du sicher gesehen hast so gemacht das ich eigene Enumerator erstellt habe für Button, Image und Result um nicht die View ins ViewModel zu holen. Da die Enumerator die selben Werte haben muss ich hier auch nichts weiter tun.

    Gonger96 schrieb:

    Oder gibt's da noch Trick 17, welchen ich nicht kenne?

    Ja, tatsächlich. Was das schliessen oder das übergeben eines Owners betrifft habe ich mir etwas einfallen lassen. Wenn du dir den Code genauer ansiehst gibt es - ich muss jetzt nachdenken wo der ist - entweder im DialogWindow und/oder in der Klasse DialogWindowService einen Code der die Window-Instanz zu einem ViewModel findet. So kannst du schön Anhand des ViewModel das richtige Fenster schliessen.

    aber du kannst gerne X Services machen, es wäre nur unnötig und du wiederholst Code unnötig und machst diesen nur schwerer wartbar.

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

    Nofear23m schrieb:

    Wenn du einen Button drückst schliesst sich die Messagebox von selbst

    Mal angenommen ich habe ein Window als Dialog. Darauf zwei Buttons "Ok" & "Abbrechen". "Ok" binde ich jetzt an einen Command im VM. Jetzt müsste ich ja eigentlich im Viewmodel das Fenster schließen richtig? Kann ich nicht einfach ein Interface z.B. ICloseable machen, das Window es implementieren lassen und selbiges als CommandParameter (als ICloseable) ans Viewmodel übergeben? Müsste ja MVVM Konform sein.

    Deine Funktion zum Finden eines Windows hab ich schon gefunden ;) Ich benutze die um den Owner des Dialogs zu ermitteln.
    Ne. Kein extra Interface.

    In dem IDialogService machst du ne Methode CloseWindow rein welcher du ein ViewModelBase als Parameter übergeben kannst.

    Diese Methode findet das richtige Fenster anhand des ViewModels und schliesst dieses.

    Um später im ViewModel zu wissen ob ok oder abort geklickt wurde einfach im ViewModel ein Bool Property IsValid rein. Vom Commend aus setzt du je nachdem von welchem Command dann true oder false.

    Grüße
    Sascha

    PS: Sorry, Antworte heute nur noch vom Handy.
    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. ##

    Kein Problem, du nerfst nicht. Es ist nicht ganz einfach zu verstehen. Am erdten Blick wirkt DependencyInjection ein bischen wie "Magic".

    Aber ist voll cool damit zu arbeiten.

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

    So müsste es hoffentlich richtig sein:
    ServiceManager
    Spoiler anzeigen

    C#-Quellcode

    1. public enum ServiceKind
    2. {
    3. Messagebox,
    4. EditGripper
    5. }
    6. public class ServiceManager
    7. {
    8. private Dictionary<ServiceKind, IService> services = new Dictionary<ServiceKind, IService>();
    9. private ServiceManager() { }
    10. public static ServiceManager Instance { get; } = new ServiceManager();
    11. public void AttachService(IService service, ServiceKind key)
    12. {
    13. services.Add(key, service);
    14. }
    15. public T GetService<T>(ServiceKind key)
    16. {
    17. return (T)services[key];
    18. }
    19. }

    IWindowService

    C#-Quellcode

    1. public interface IWindowService : IService
    2. {
    3. bool? ShowDialog(object viewModel, object parent);
    4. void Close(bool? result);
    5. }

    EditGripperService
    Spoiler anzeigen

    C#-Quellcode

    1. public class EditGripperService : IWindowService
    2. {
    3. private Window openedWindow = null;
    4. private bool? dialogResult = null;
    5. public void Close(bool? result)
    6. {
    7. if(openedWindow != null)
    8. {
    9. openedWindow.Close();
    10. openedWindow = null;
    11. dialogResult = result;
    12. }
    13. }
    14. public bool? ShowDialog(object viewModel, object parent)
    15. {
    16. dialogResult = null;
    17. if(viewModel is CentricGripperVm)
    18. {
    19. openedWindow = new CentricGripperDialog
    20. {
    21. DataContext = viewModel,
    22. Owner = WindowHelper.FindOwner(parent)
    23. };
    24. openedWindow.ShowDialog();
    25. return dialogResult;
    26. }
    27. return null;
    28. }
    29. }

    ServiceInjector

    C#-Quellcode

    1. public static class ServiceInjector
    2. {
    3. public static void InjectServices()
    4. {
    5. ServiceManager.Instance.AttachService(new MessageboxService(), ServiceKind.Messagebox);
    6. ServiceManager.Instance.AttachService(new EditGripperService(), ServiceKind.EditGripper);
    7. }
    8. }

    CentricGripperVm
    Spoiler anzeigen

    C#-Quellcode

    1. public class CentricGripperVm : ViewmodelBase, IGripperListVm
    2. {
    3. private CentricGripper gripper;
    4. public CentricGripperVm()
    5. {
    6. gripper = new CentricGripper();
    7. CancelCommand = new RelayCommand(new Action(Cancel));
    8. AcceptCommand = new RelayCommand(new Action(Accept));
    9. }
    10. public RelayCommand AcceptCommand { get; }
    11. public RelayCommand CancelCommand { get; }
    12. private void Accept()
    13. {
    14. ...
    15. Close(true);
    16. }
    17. private void Cancel()
    18. {
    19. Close(false);
    20. }
    21. private void Close(bool res)
    22. {
    23. var dlg = ServiceManager.Instance.GetService<IWindowService>(ServiceKind.EditGripper);
    24. dlg.Close(res);
    25. }
    26. }
    Na das sieht ja ganz gut aus. Und wie du siehst hast du nun relativ viel Kontrolle bzw. kannst du ja noch mehr Kontrolle hineinholen indem du die Methoden im IWondowService abänderst.

    Und die verwendung im ViewModel ist ja super einfach oder?
    Auf diese At und weise ist nun dein ViewModel-Code auch voll Testbar und hat keine Abhängigkeiten einer View.

    Gut gemacht.
    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. ##

    Hallo

    Das ist ein Interface. Ich weis nicht genau wie deine Kenntnisse sind.
    Im Grunde machst du mit einem Interface einen Bauplan.

    Beispiel: Ein IDialogService definiert z.b. zwei Methoden. ShowDialog() und CloseDialog().
    In einem Interface sind aber nur die Methoden definiert, diese Methoden haben keinen Code. Das Interface beschreibt ja nur welche Methoden oder Eigenschaften vorhanden sind. Eben wie ein Bauplan.

    Dann erstellst eine Klasse die nennt sich DialogService welche das Interface IDialogService implementiert. Dadurch erstellt dir VS automatisch (in diesem Fall) die zwei Methoden.
    Diese Methoden füllst nun mit Code nach belieben.

    Warum?

    Die Klasse DialogService kann nun (weil diese sich im Projekt mit Zugriff auf die UI befindet) auf Windows, Controls usw. zugreifen und dementsprechend ein Fenster öffnen und/aber auch schliessen.
    In deinem ViewModel hast du ja keinen Zugriff auf die UI, benötigst du auch nicht weil du über das Interface agierst, dieses benötigt keinen Verweis, in diesem ist ja kein Code.
    ABER: In deinem ViewModel kannst du nun über das Interface (über dieses kennst du die Methoden ja) die Methoden ansprechen und werden aufgeführt, ohne das dein ViewModel was davon wissen muss.

    Fall notwenig kann ich dir gerne ein kleines Bespiel machen.

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