MVVM Umsetzung mit WinForms

  • VB.NET
  • .NET (FX) 4.5–4.8

Es gibt 173 Antworten in diesem Thema. Der letzte Beitrag () ist von VaporiZed.

    Neu

    Haudruferzappeltnoch schrieb:

    wieso ist das ein Notfall?
    Weil es der Standardweg ist, eine tDS-Instanz auf dem Form zu erstellen und in dem Components zu haben (soweit gleich mit dem BS-VM-Paket) und dann eben auch direkt daran zu binden. Wenn man jetzt da das tDS rausnimmt, um bei der App diesbezüglich eine View-VM-Trennung zu machen, wird das auch eine Umstellung für den ein oder anderen "DDD"-Entwickler. Naja, oder vielleicht auch nicht. Man könnte ja ne BS für das tDS erstellen, so wie ich es früher gemacht habe. Damit konnte ich die formübergreifende tDS-Instanz-Übergabe besser bewerkstelligen.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Neu

    Haudruferzappeltnoch schrieb:

    Die Instanz auf dem Form zu haben ist ja auch nicht schlimm.
    hmm - ich fand das schon immer ein verheerendes WinForms-FehlDesign, dass jedes Form sein eigenes Dataset enthält (im DesignerCode instanziert).
    Aber ich hab halt einen Workaround gefunden - eine Methode, die alle Bindings umstöpselt von der lokalen tDS-Instanz auf die globale.

    Haudruferzappeltnoch schrieb:

    das ist in WPF der DataContext, da nimmt man sich auch die Instance der VM rein.
    Ein Wpf-Window instanziert kein Viewmodel, sondern bindet nur seinen DataContext ans globale Viewmodel. Da ist also von vornherein richtig gelöst, was in WinForms mein Workaround erst graderücken muss.

    Neu

    Moin moin

    Du kannst das Zellenformat vorab im Designer festlegen:

    Ja das weis ich und das hatte ich auch erst so gemacht. Aus irgendeinem Grund wurde das Datum und eine andere Spalte aber immer "falsch" formatiert.
    Z.B. bei der Luftfeuchte wurde immer 55.00 % angezeigt, beim Datum: 2024-07-14T17:36:15.4780243+02:00

    ​Das ist GUI-Arbeit, also in View,


    OK, dann habe ich ja doch wieder Code auf dem Form. Sehr verwirrend :!:

    ihr VM auf GUI-Unabhängigkeit zu prüfen


    Nunja das meiste funktioniert auch ohne eine GUI. Das sehe ich ja an den ganzen "Debug/Console-Ausgaben" die ich immer wieder in die Klassen / Methoden einbaue.
    Asperger Autistin. Brauche immer etwas um gewisse Sachen zu verstehen. :huh:

    Neu

    ErfinderDesRades schrieb:

    dass jedes Form sein eigenes Dataset enthält (im DesignerCode instanziert)
    Das lässt sich beheben indem man den Designer anfasst. Du nutzt ja die Shared Instanz, die kann man als DataSource auch im Designer bei der BindingSource angeben, allerdings lädt der Designer dann nicht mehr. Die Anwendung kompiliert und läuft aber weiterhin.
    Oder man könnte ein dummyDS aufs Form tun und so die DGVs einstellen und dann beim Aufruf des Forms die DataSource umstöpseln. Das ist wahrscheinlich was du machst.

    Der Designer meldet wenn man dort die globale Instanz verwenden will, dass der Member nicht existiert.
    Das ist der einzige Unterschied zum DataContext
    Aber das ist ja nicht die Schuld des DataSets, von daher ist an dieser Stelle WinForms altbackener als tDS

    Dieser Beitrag wurde bereits 3 mal editiert, zuletzt von „Haudruferzappeltnoch“ ()

    Neu

    offtopic
    Dann werf ich nochmals zu dem Thema ein, was ich ebenfalls schon ein paar Mal in den vergangenen Jahren dazu schrieb und hier auch kurz erwähnt hatte: Jedes Form hat seine tDS-Instanz FtDS, man packt ne BS BsTds auf's Form, die an FtDS bindet, alle anderes BSs binden an die BsTds statt an FtDS und wenn man dann ein Form F2 von einem anderen Form F1 aus aufruft, legt man die tatsächliche tDS-Instanz als DataSource der F2-BsTds fest, z.B. mit F2.BsTds.DataSource = F1.BsTds.DataSource. Alle BSs von F2 sind dann effektiv immer an das one-and-only tDS angebunden und alles ist gut.
    /offtopic

    Aber zurück zum Thema: @Amelie: Ich hätte mir denken müssen, dass Dich meine Aussage verwirrt, weil ich anfangs immer schrieb: kein Code im View. Ich hätte damals schon klar sagen müssen: kein Code im View, der nicht ausschließlich für die Optik zuständig ist. MVVM ist eine Schichten- und Aufgabentrennung. Code im View darf sich nur um Viewaufgaben kümmern. Und das ist eben Optik. Man könnte auch sagen: Um das View kümmert sich ein menschlicher Designer. Er kann den Code im View-CodeBehind so erstellen, dass das View hübsch aussieht. Für die Businesslogik ist jemand in einer anderen Abteilung zuständig. Der menschliche Designer soll sich nur darum kümmern, wie die Oberfläche/das View aussieht. Dazu darf er (und muss es manchmal auch) Code verwenden. Aber eben nur solchen, der die Daten zwar auswerten darf, aber diese nicht manipuliert. Also quasi ReadOnly.

    Ich hoffe, dass mir die anderen da zustimmen, ansonsten bitte präzisieren oder korrigieren.

    Amelie schrieb:

    Aus irgendeinem Grund wurde das Datum und eine andere Spalte aber immer "falsch" formatiert.
    Dann ist das eine Sache, die wir korrigieren können. Nur müssen wir das erstmal von Dir erfahren, dass es da ein Problem gibt. Jetzt weißt Du, dass es geht. Bei Datum kannst Du g festlegen, um Datum und Zeit in Kurzformat zu bekommen. Bei der Luftfeuchtigkeit kannst Du bei datentyp- und wertabhängig mal probieren. Vielleicht %, vielleicht # "%"
    Bilder
    • Formatfestlegung.png

      45,79 kB, 1.012×655, 19 mal angesehen
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Neu

    VaporiZed schrieb:

    Ich hoffe, dass mir die anderen da zustimmen, ansonsten bitte präzisieren oder korrigieren.
    Ja, ich denke, du beschreibst die MVVM-Forderungen ganz richtig.
    Nur würde ich das laxer handhaben.
    Weil Ich halte Winforms generell (noch?) für ungeeignet, um damit ein strenges MVVM umzusetzen.
    Da hilft auch kein CommandBinding, erst recht nicht, wenn so dermassen buggy.
    Mir scheint WinForms-MVVM nur mit unverhältnismässigem Zusatz-Aufwand möglich, und ohne wirklichen architektonischen Gewinn. ZB die berühmte GUI-Austauschbarkeit - Das ist doch eine theoretische Phantasterei, die wir Normalsterbliche niemals praktisch umsetzen werden.

    Beispielsweise so eine simple Anforderung an ein DGV, einen doppelgeklicksten Datensatz im Einzel-Formular editieren zu können.
    In Oldschool ist das banal: 5 Zeilen ins Codebehind, die jeder Anfänger versteht.
    In MVVM wäre das doch ein gewaltiger Aufriss, mit Factories ("Service" ist glaub der falsche Begriff dafür), Interfaces und Kram, oder irre ich mich?
    Und jeder machts auch noch anders, und jede Variante ist auf ihre Weise undurchsichtig - für was?

    Oder du selektierst 6 Datensätze, um sie mit einem Click zu löschen - das ist auch so ein Fall wo Oldschool sich langweilt, während MVVM einen Breakdance mit Kopfstand aufführt?

    Also ich bin sehr dafür, komplexere Datenverarbeitung möglichst aus dem CodeBehind herauszuverlagern ins Model, oder Viewmodel.
    Aber die Control-Eventhandler täte ich im CodeBehind belassen.
    Ist halt Winforms, und nicht Wpf.

    Neu

    @VaporiZednullHabe nun mal ein bissel weiter gemacht und ja klappt soweit. (siehe Bild)
    Ich hatte das Private Sub UpdateShow() zwar auch schon im ViewModel aber da hatte ich das Problem, das ich immer erst einmal mit dem Monat blättern musste, damit die Daten angezeigt wurden.
    Hab das bis jetzt auch nicht anders hinbekommen... wohl grade blind...

    PS: das mit dem % muss so sein im Designer:'%'

    Das Model:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. '/ file: DailyDataLoader.vb ( Model )
    2. Public Class DailyDataLoader
    3. Public Property CurrFilename As String
    4. Get
    5. Return $"DailyDatas_{Configuration.Instance.CurDatum.ToString("MM_yyyy")}.xml"
    6. End Get
    7. Set(value As String)
    8. End Set
    9. End Property
    10. ' Pfad zur XML-Datei mit den täglichen Wetterdaten
    11. Private ReadOnly DailyXmlPath As String = Path.Combine(Configuration.Instance.DataXmlFilePath, CurrFilename)
    12. ' Methode zum Laden aller täglichen Wetterdaten aus der XML-Datei
    13. Public Function LoadAllDailyWeatherData() As List(Of DailyWeatherData)
    14. Dim dailyWeatherDataList As New List(Of DailyWeatherData)()
    15. If File.Exists(DailyXmlPath) Then
    16. Dim ds As New DataSet()
    17. ds.ReadXml(DailyXmlPath)
    18. If ds.Tables.Contains("DailyMeasurements") AndAlso ds.Tables("DailyMeasurements").Rows.Count > 0 Then
    19. For Each row As DataRow In ds.Tables("DailyMeasurements").Rows
    20. Dim dailyWeatherData As New DailyWeatherData()
    21. dailyWeatherData.ID = If(IsDBNull(row("ID")), 0, Convert.ToInt32(row("ID")))
    22. dailyWeatherData.Datum = If(IsDBNull(row("Datum")), DateTime.MinValue, Convert.ToDateTime(row("Datum")))
    23. dailyWeatherData.Temp1 = If(IsDBNull(row("Temp1")), 0D, Convert.ToDecimal(row("Temp1")))
    24. dailyWeatherData.Temp2 = If(IsDBNull(row("Temp2")), 0D, Convert.ToDecimal(row("Temp2")))
    25. dailyWeatherData.Temp3 = If(IsDBNull(row("Temp3")), 0D, Convert.ToDecimal(row("Temp3")))
    26. dailyWeatherData.Humidi1 = If(IsDBNull(row("Humidi1")), 0D, Convert.ToDecimal(row("Humidi1")))
    27. dailyWeatherData.Humidi2 = If(IsDBNull(row("Humidi2")), 0D, Convert.ToDecimal(row("Humidi2")))
    28. dailyWeatherData.Humidi3 = If(IsDBNull(row("Humidi3")), 0D, Convert.ToDecimal(row("Humidi3")))
    29. dailyWeatherData.Pressure1 = If(IsDBNull(row("Pressure1")), 0D, Convert.ToDecimal(row("Pressure1")))
    30. dailyWeatherData.Pressure2 = If(IsDBNull(row("Pressure2")), 0D, Convert.ToDecimal(row("Pressure2")))
    31. dailyWeatherData.Pressure3 = If(IsDBNull(row("Pressure3")), 0D, Convert.ToDecimal(row("Pressure3")))
    32. ' Das DailyWeatherData-Objekt zur Liste hinzufügen
    33. dailyWeatherDataList.Add(dailyWeatherData)
    34. Next
    35. End If
    36. End If
    37. Return dailyWeatherDataList
    38. End Function
    39. End Class



    Das ViewModel:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. '/ file: P0_ShowDataViewModel.vb ( ViewModel )
    2. Partial Public Class ShowDataViewModel
    3. Inherits PropertyChange
    4. ' Singleton-Instanz des ViewModels
    5. Private Shared ReadOnly _instance As New Lazy(Of ShowDataViewModel)(Function() New ShowDataViewModel())
    6. Public Shared ReadOnly Property Instance As ShowDataViewModel
    7. Get
    8. Return _instance.Value
    9. End Get
    10. End Property
    11. ' BindingList zur Datenbindung mit dem DataGridView
    12. Public ReadOnly Property MonthlyWeatherDataList As New BindingList(Of WeatherData)
    13. Public ReadOnly Property DailyWeatherDataList As New BindingList(Of DailyWeatherData)
    14. Public Sub New()
    15. AktlMonth = Configuration.Instance.CurDatum
    16. End Sub
    17. ' Methoden zum Laden der Daten aus dem Model ins ViewModel
    18. Public Sub LoadMonthlyDataFromModel(MonthlyMeasurementsList As List(Of WeatherData))
    19. MonthlyWeatherDataList.Clear()
    20. For Each MonthlyMeasurement In MonthlyMeasurementsList
    21. MonthlyWeatherDataList.Add(MonthlyMeasurement)
    22. Next
    23. End Sub
    24. Public Sub LoadDailyDataFromModel(dailyMeasurementsList As List(Of DailyWeatherData))
    25. DailyWeatherDataList.Clear()
    26. For Each dailyMeasurement In dailyMeasurementsList
    27. DailyWeatherDataList.Add(dailyMeasurement)
    28. Next
    29. End Sub
    30. ' Methode für den vorherigen Monat
    31. Public Property MoveToPreviousMonthRelayCommand As New RelayCommand(AddressOf PreviousMonth)
    32. Private Sub PreviousMonth()
    33. Configuration.Instance.MoveToPreviousMonth()
    34. AktlMonth = Configuration.Instance.CurDatum
    35. End Sub
    36. ' Methode für den nächsten Monat
    37. Public Property MoveToNextMonthRelayCommand As New RelayCommand(AddressOf NextMonth)
    38. Private Sub NextMonth()
    39. Configuration.Instance.MoveToNextMonth()
    40. AktlMonth = Configuration.Instance.CurDatum
    41. End Sub
    42. ' Methode um den ausgewählten Monat anzuzeigen
    43. Private _aktlMonth As DateTime
    44. Public Property AktlMonth As DateTime
    45. Get
    46. Return _aktlMonth
    47. End Get
    48. Set(value As DateTime)
    49. If _aktlMonth <> value Then
    50. _aktlMonth = value
    51. OnPropertyChanged()
    52. End If
    53. End Set
    54. End Property
    55. End Class



    Das View:
    Spoiler anzeigen

    VB.NET-Quellcode

    1. '/ file: FrmSowCityDatas.vb
    2. Public Class FrmShowCityDatas
    3. Private ReadOnly _viewModel As ShowDataViewModel = ShowDataViewModel.Instance
    4. Public Sub New()
    5. InitializeComponent()
    6. DgvDailydatas.AutoGenerateColumns = False
    7. DgvMonthdatas.AutoGenerateColumns = False
    8. UpdateShow()
    9. End Sub
    10. Private Sub UpdateShow()
    11. LoadAndSetData()
    12. ShowDataInDGV()
    13. UpdateMonth()
    14. End Sub
    15. Private Sub LoadAndSetData()
    16. Dim monthlyDataLoader As New MonthlyDataLoader()
    17. Dim monthlyWeatherDataList As List(Of WeatherData) = monthlyDataLoader.LoadAllMonthlyWeatherData()
    18. _viewModel.LoadMonthlyDataFromModel(monthlyWeatherDataList)
    19. Dim dailyDataLoader As New DailyDataLoader()
    20. Dim dailyWeatherDataList As List(Of DailyWeatherData) = dailyDataLoader.LoadAllDailyWeatherData()
    21. _viewModel.LoadDailyDataFromModel(dailyWeatherDataList)
    22. End Sub
    23. Private Sub ShowDataInDGV()
    24. DgvDailydatas.DataSource = _viewModel.DailyWeatherDataList
    25. DgvMonthdatas.DataSource = _viewModel.MonthlyWeatherDataList
    26. End Sub
    27. Private Sub UpdateMonth()
    28. MonthTxtBoxMenuItem.Text = _viewModel.AktlMonth.ToString("MMMM yyyy")
    29. End Sub
    30. Private Sub MonthPrevMenuItem_Click(sender As Object, e As EventArgs) Handles MonthPrevMenuItem.Click
    31. _viewModel.MoveToPreviousMonthRelayCommand.Execute(Nothing)
    32. UpdateShow()
    33. End Sub
    34. Private Sub MonthNextMenuItem_Click(sender As Object, e As EventArgs) Handles MonthNextMenuItem.Click
    35. _viewModel.MoveToNextMonthRelayCommand.Execute(Nothing)
    36. UpdateShow()
    37. End Sub
    38. End Class

    Asperger Autistin. Brauche immer etwas um gewisse Sachen zu verstehen. :huh:

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

    Neu

    Würdest Du das Projekt hochladen wollen? Dann kann ich auch einen Blick auf die DataBindings werfen und besser damit rumspielen und nach Verbesserungen schauen.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Neu

    @VaporiZed

    Es ist echt zum <X mit dem S.....

    Haste das eine endlich zum laufen gebracht, funktionieren andere Sachen nicht mehr. :cursing:

    Nun klappt es mit den Monats-Dateien erstellen und im DGV anzuzeigen. Das Blättern der Monate funktioniert und dann..

    Gebe Daten ins DGV ein; Speichern. Datei wird geändert (Zeitstempel im Explorer) aber es werden keine Daten die ich eingegeben habe gespeichert.
    Obwohl ich mir Mühe gebe, kommt es mir vor als wenn ich zwischen Haufen von Dateien hinundher springe und dann auf dem Weg alles an Daten etc verliere....
    Asperger Autistin. Brauche immer etwas um gewisse Sachen zu verstehen. :huh:

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

    Neu

    1. In StartMain gibt es die Zeile

      VB.NET-Quellcode

      1. Dim mainViewModel As New MainViewModel(Function() windowService, configViewModel, dataService)
      und damit im mainViewModel-Konstruktor:

      VB.NET-Quellcode

      1. Public Sub New(windowServiceFactory As Func(Of IWindowService), configViewModel As ConfigViewModel, dataService As DataService)
      Warum verwendest Du dieses Func-Konstrukt?
    2. Wozu gibt es eine BindingSource im DataService? Die BindingSource ist eine WinForms-Komponente und gehört somit ins View/GUI/Form.
    3. Der Konstruktor in der WinFormsWindowService-Klasse:

      VB.NET-Quellcode

      1. Public Sub New(mainViewModel As MainViewModel, configViewModel As ConfigViewModel)
      Aber der mainViewModel-Parameter wird nicht verwendet. Wozu also?
    4. Warum wird in Public Sub ShowMain() das ConfigViewModel mitgegeben?
    5. Warum kennt die WinFormsWindowService-Klasse überhaupt konkrete ViewModels?
    6. Diese Lazy(Of …)(Function() New …)-Konstrukte find ich zwar interessant. Aber wozu brauchst Du sie?
    Zu 5.: Ich habe mal die konkreten Datentypen (bis auf die Forms) entfernt. Dadurch ersparst Du Dir die Übergabe und Aktualisierung der ViewModels in dem WindowService. Dem WindowService kann's ja wurscht sein, von welchem Typ das ViewModel ist, soll sich doch das Form darum kümmern: Wenn der WindowService aufgerufen wird, gibt man die gewünschte ViewModel-Instanz an den WindowService, welcher aber diese Instanz als Object entgegennimmt und an das gewünschte Form weiterreicht. Das Form castet das Teil dann wieder zurück in den Solltyp. Das nennt man Boxing/Unboxing. Was soll jetzt der Blödsinn, also erst in ein anonymes Paket einpacken und später wieder auspacken? Der WindowService muss nicht mehr wissen, was für ViewModels es gibt. Du wärst damit nur noch wenig davon entfernt, den WindowService so zu gestalten, dass er in anderen MVVM-Projekten auch verwendet werden kann. Dazu müssten wir nur noch die Form-Typen aus der WindowService-Klasse entfernen. Aber eines nach dem anderen.

    Dass das DataSet und die BindingSource im DataService nun direkt als Datenquelle hergenommen werden, ist net so dolle. Du kannst gern ein typisiertes DataSet verwenden. Aber untypisiert macht es die Sache fehleranfäiig. Grundsätzlich würde ich sogar Modelklassen verwenden, also sog. POCOs. Die übernehmen dann die Daten aus der Datenquelle und deren Daten werden beim Speichern auch wieder in die Datenquelle reingeschoben. Was diese Datenquelle ist und wie die Daten von A nach B kommen, bleibt Dir überlassen. Du kannst ne XML-Datei mit angeschlossenem tDS verwenden, Du kannst die Daten auch direkt als JSON abspeichern oder irgendwann ne Datenbank anschließen. Aber die direkte Verwendung eines untypisierten DataSets plus BindingSource in einer Datenklasse ist nicht empfehlenswert.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.

    Neu

    @VaporiZed

    Ich habe seit gestern noch wieter gemacht.. mit dem Resultat, das alles nicht mehr funktioniert.

    Es ist enfach zu frustrierend wenn irgendwann nichts mehr geht. Bin dann wohl zu blöd zum progrmaiern.. :cursing:

    Danke trotzdem. <3
    Asperger Autistin. Brauche immer etwas um gewisse Sachen zu verstehen. :huh:

    Neu

    Amelie schrieb:

    mit dem Resultat, das alles nicht mehr funktioniert.
    Aber schrittweise Backups machst Du schon, oder? Das musste ich über die Jahre schmerzhaft lernen: Wenn ein Fehler behoben oder ein neues Feature implementiert wurde, umgehend ein Backup des Ist-Zustands machen. Und dabei die Programmversionsnummer aktualisieren und die Änderung in Stichpunkten zusammenfassen.
    Dieser Beitrag wurde bereits 5 mal editiert, zuletzt von „VaporiZed“, mal wieder aus Grammatikgründen.

    Aufgrund spontaner Selbsteintrübung sind all meine Glaskugeln beim Hersteller. Lasst mich daher bitte nicht den Spekulatiusbackmodus wechseln.