Aktuelles Architektur-Pattern in bestehenden Anwendungen nutzen

MVVM und WinForms – ein magisches Duo
Kommentare

Für WPF ist das Architektur-Pattern Model-View-ViewModel (MVVM) gesetzt. Benutzeroberfläche und Anwendungslogik sind vorbildlich getrennt, eine hohe Unit-Test-Abdeckung ist möglich. Das ist schön. Allerdings hilft uns das bei bestehenden Windows-Forms-Anwendungen nicht weiter. Oder doch? Kann ich MVVM auch dort nutzen?

Auch noch im Jahr 2014, zu Zeiten von WPF, Windows-8-Apps und Microsoft Azure, beschäftigen sich zahlreiche Entwickler hauptberuflich mit Windows-Forms-Anwendungen. Die Systeme sind teilweise über zehn Jahre alt, es wurde viel Zeit und Geld in sie investiert und sie leisten in ihrem Einsatzbereich unverändert gute Dienste. Diese so genannten Legacy-Anwendungen leiden allerdings oft unter einem heillosen Codechaos – und die Entwickler, die sie warten, mit ihnen. Teilweise reichen die Ursprünge des Quellcodes auf VB 5.0 oder gar MS Access zurück, und die Jahre, die Entwicklergenerationen und manchmal auch jeweils aktuelle Hypes sind ihm anzusehen. Kurz: „Historisch gewachsen“.

Motivation für MVVM mit WinForms

Mit MVVM gibt es ein modernes und für WPF/XAML inzwischen seit mehreren Jahren bewährtes Architektur-Pattern, das auch bei später hinzugekommenen Technologien (Silverlight, WinRT-Apps) wieder zur Anwendung kommt. Da Patterns prinzipiell technologieunabhängig sind, liegt der Gedanke nahe, MVVM auch an einer älteren Technologie wie Windows Forms auszuprobieren, um dem historisch gewachsenem Wildwuchs entgegenzutreten und ähnliche Vorzüge wie in WPF-Anwendungen zu erhalten:

  • Kein Code-Behind in der Form
  • Klare Trennung von Benutzeroberfläche (UI), UI-Logik  und Geschäftslogik
  • Unit-Test-geeignete UI-Logik und Geschäftslogik

Limitierungen

Um das Ergebnis vorwegzunehmen: Es funktioniert, und das Zauberwort heißt DataBindings – wer dabei nur an „Datenbank an TextBox“ denkt, wird überrascht sein. Aber Windows Forms ist eben kein XAML, sodass es Dinge gibt, die man mit Windows-Forms-Steuerelementen nicht, nicht so elegant oder nur anders machen kann als in WPF. Das ListView-Steuerelement stellt eine solche Einschränkung dar, die SelectedItem-Eigenschaft eine andere und Command Binding erfordert einen anderen Ansatz. Dieser Artikel beleuchtet die Grundlagen von Windows Forms mit MVVM und zeigt Limitierungen auf.

ViewModel

Der Fokus liegt dabei auf dem ViewModel, also der Schicht zwischen View und Model (Abb. 1). Pauschal ausgedrückt, ersetzen ViewModel-Klassen den Code-Behind der Forms und haben folgende Aufgaben:

  • Steuerelemente mit Daten versorgen
  • UI-Logik (Schaltflächen aktiveren/deaktivieren, Bereiche ein- und ausblenden …)
  • Benutzereingaben verarbeiten und an die Geschäftslogik weiterleiten

Abb. 1: MVVM-Schichten

Alle Windows-Forms-Steuerelemente unterstützen Datenbindung. Für einfache Steuerelemente wie Label, TextBox und CheckBox ist das sehr leicht. Mithilfe von DataBindings.Add binden wir jeweils eine Steuerelementeigenschaft an eine ViewModel-Eigenschaft (Listing 1)

' View (Form) 
Public Class AppWindow
   Public Sub New()
      InitializeComponent()
      ' Add any initialisation after the InitializeComponent() call. 
      Dim viewModel = New AppWindowViewModel()

      UserLabel.DataBindings.Add("Text", viewModel, "UserName")
      CommentTextBox.DataBindings.Add("Text", viewModel, "Comment")
      CommentTextBox.DataBindings.Add("Enabled", viewModel, "CanEditComment")
      CommentCheckBox.DataBindings.Add("Checked", viewModel, "CanEditComment")
   End Sub
End Class

' ViewModel
Public Class AppWindowViewModel
   Public Property UserName As String = "John Smith"
   Public Property CanEditComment As Boolean = False
   Public Property Comment As String = "Dein Kommentar"
End Class
UserLabel.DataBindings.Add("Text", viewModel, "UserName")

liest sich: „Verbinde UserLabel.Text mit der Eigenschaft UserName in der ViewModel-Klasse“. Beachten Sie, dass die Eigenschaft CanEditComment hier sowohl das Häkchen im Kontrollkästchen als auch die Aktivierung des Textfelds steuert. Das wenig überraschende Ergebnis beim Start der Anwendung ist der Name John Smith, ein deaktiviertes Textfeld und ein Kontrollkästchen ohne Häkchen.

Nachrichten ans ViewModel

Noch geschieht beim Anklicken des Kontrollkästchens nichts. Das beheben wir durch folgende Ergänzung der Datenbindung:

CommentCheckBox.DataBindings.Add("Checked", ViewModel, "CanEditComment", _
   False, DataSourceUpdateMode.OnPropertyChanged)

Das Textfeld wird nun über das Kontrollkästchen aktiviert und deaktiviert.

Für die Aktualisierung der Datenquelle – in unserem Fall AppWindowViewModel – bietet die Datenbindung drei Optionen: Never, OnValidation und OnPropertyChanged. Der Standardwert ist OnValidation. Bei vielen Steuerelementen beginnt die Validierung, wenn das Steuerelement den Fokus verliert, also der Benutzer das Element per Tab-Taste oder Mausklick verlässt. So auch beim Kontrollkästchen. Soll die Benutzereingabe gleich bei der Veränderung der Checked-Eigenschaft weitergegeben werden, ist OnPropertyChanged die richtige Wahl.Der vierte Parameter False ist in unserem Beispiel nicht relevant. Er dient ggf. zur formatierten Ausgabe gebundener Daten.

Command Binding

In WPF-Anwendungen definiert man eine Aktion (Command), die z. B. von einem Menüeintrag oder einer Schaltfläche ausgelöst wird, deklarativ im XAML. Wie im Abschnitt zur Limitierung angedeutet, lässt sich dieses Command Binding nicht eins zu eins auf Windows-Forms-Steuerelemente übertragen. Wohin nun mit OkButton_Click und Co.,  wenn es keinen Code-Behind geben soll? Listing 2 skizziert eine Lösung. Mithilfe eines Lambda-Ausdrucks fügen wir im Konstruktor der Form für das Ereignis ClearButton.Click die Methode AppWindowViewModel.ClearComment als Ereignisbehandlung hinzu.

' View (Form) 
Public Class AppWindow
   Public Sub New()
      ' ... 
      AddHandler ClearButton.Click, Sub() viewModel.ClearComment()
   End Sub
End Class

' ViewModel
Public Class AppWindowViewModel
   ' ... 
   Public Property Comment As String = "Dein Kommentar"

   Public Sub ClearComment()
      Comment = String.Empty
   End Sub
End Class

Wer es ausprobiert, wird feststellen, dass die Methode im ViewModel zwar aufgerufen, der Inhalt des Textfelds aber nicht gelöscht wird – trotz Datenbindung an die nun geleerte Comment-Eigenschaft. Warum das (noch) so ist, erfahren Sie im nächsten Abschnitt.

Aufmacherbild: Dancing Duo Photographed Smoke Trails von Shutterstock / Urheberrecht: Marilyn Volan

[ header = Seite 2: Nachrichten vom ViewModel ]

Nachrichten vom ViewModel

Genau wie WPF nutzt auch die Datenbindung in Windows Forms die Schnittstelle INotifyPropertyChange. Diese definiert das Ereignis PropertyChanged, das gebundene Steuerelemente gezielt über Änderungen in einzelnen Eigenschaften der Datenquelle informiert. Die Eigenschaft Comment in unserem Beispiel informiert noch niemanden, weshalb das Textfeld unverändert den alten Wert anzeigt.

In Listing 3 wandeln wir die Eigenschaft Comment in deren ausführliche Schreibweise um und lösen im Setter das benötigte PropertyChanged-Ereignis aus. Nun wird das Textfeld aktualisiert.

' ViewModel
Imports System.ComponentModel

Public Class AppWindowViewModel
   Implements INotifyPropertyChanged

   Public Event PropertyChanged(s As Object, e As PropertyChangedEventArgs) _
      Implements INotifyPropertyChanged.PropertyChanged

   ' ... 
   Private CommentValue As String = "Dein Kommentar"
   Public Property Comment As String
      Get
         Return CommentValue
      End Get
      Set(value As String)
         CommentValue = value
         RaiseEvent PropertyChanged(Me, _
            New PropertyChangedEventArgs("Comment"))
      End Set
   End Property
End Class

Listen-Steuerelemente

Für ListBox oder ComboBox erstellt man die Datenbindung über die DataSource-Eigenschaft. ViewModel-seitig bietet sich der Datentyp BindingList(Of T) an. Wenn Elemente hinzugefügt oder gelöscht werden, löst dieser Datentyp Ereignisse aus, die die Steuerelemente automatisch verarbeiten und entsprechend Zeilen einfügen bzw. entfernen.Leider verweigert die SelectedItem-Eigenschaft von ListBox und ComboBox die erwartete Zusammenarbeit mit DataSourceUpdateMode.OnPropertyChanged (siehe Abschnitt „Nachrichten ans ViewModel“). Die Datenquelle wird hier grundsätzlich nur aktualisiert, wenn das Steuerelement den Fokus verliert. Wer aber üblicherweise sofort auf die geänderte Auswahl reagieren möchte, muss den Umweg über die Eigenschaft SelectedIndex gehen.

In Listing 4 überträgt die Beispielanwendung Kommentare vor dem Löschen aus dem Textfeld in eine ListBox ClearedCommentsListBox. Per Klick in die Liste kommt der Kommentar via ClearCommentsIndex-Setter wieder zurück ins Textfeld.

' View (Form) 
Public Class AppWindow
   Public Sub New()
      ' ...
      ClearedCommentsListBox.DataSource = viewModel.ClearedComments
      ClearedCommentsListBox.DataBindings.Add("SelectedIndex", viewModel, _
         "ClearedCommentsIndex", False, DataSourceUpdateMode.OnPropertyChanged)
   End Sub
End Class

' ViewModel
Public Class AppWindowViewModel
   ' ... 
   Public Sub ClearComment()
      ClearedComments.Add(Comment)
      Comment = String.Empty
   End Sub

   Private ReadOnly ClearedCommentsValue As New BindingList(Of String)
   Public ReadOnly Property ClearedComments As BindingList(Of String)
      Get
         Return ClearedCommentsValue
      End Get
   End Property

   Private ClearedCommentsIndexValue As Integer = -1
   Public Property ClearedCommentsIndex() As Integer
      Get
         Return ClearedCommentsIndexValue
      End Get
      Set(ByVal value As Integer)
         ClearedCommentsIndexValue = value
         Comment = ClearedComments(value)
      End Set
   End Property 
End Class

Bei der Datenbindung der SelectedIndex-Eigenschaft ist es wichtig, diese mit einem gültigen Wert zu initialisieren, da wir andernfalls eine ArgumentOutOfRangeException verursachen. In Listing 4 muss der Startwert -1 sein, da die ListBox zu Beginn noch keine Einträge enthält. Im Gegensatz zu ListBox und ComboBox, die Auflistungen beliebiger Datentypen verarbeiten, ist der Inhalt des ListView-Steuerelements per Datenbindung nicht zu erreichen. Es besitzt keine DataSource-Eigenschaft, und die Elemente werden in einer speziellen ListViewItemCollection verwaltet. Für „MVVM-kompatible“ tabellenartige Darstellungen bietet das DataGridView-Steuerelement mächtige Möglichkeiten – eine detaillierte Betrachtung wäre einen eigenen Artikel wert.

Motivationskontrolle

Nach dieser Grundlagenforschung werfen wir einen Blick zurück auf unsere Ziele im Abschnitt „Motivation“. Hat sich die Arbeit gelohnt?

  • Kein Code-Behind in der Form: AppWindow enthält – abgesehen vom Konstruktor – keinen Code-Behind
  • Klare Trennung von UI, UI-Logik und Geschäftslogik: AppWindow enthält keine UI- oder Geschäftslogik
  • Unit-Test-geeignete Geschäfts- und UI-Logik: AppWindowViewModel ist frei von UI Elementen und bereit für Unit Tests

So weit, so gut. Zusätzlich fällt auf, dass Layoutentscheidungen freier werden. Die ListBox für die gelöschten Kommentare können wir mit minimalen Änderungen durch eine ComboBox ersetzen, das Label für den Benutzernamen durch eine TextBox oder auch eine GroupBox.

MVVM-Pattern in WinForms-Anwendungen – die Migration

Wie bringt man nun das MVVM Pattern in bestehende, klassisch entwickelte Windows-Forms-Applikationen? Erfreulicherweise kann ein ViewModel schrittweise, Form für Form, Steuerelement für Steuerelement und sogar Eigenschaft für Eigenschaft eingeführt werden. Ich persönlich bevorzuge es, eine separate Klassenbibliothek ApplicationName.ViewModel in meiner Projektmappe anzulegen, die ich in der Windows-Forms-Anwendung referenziere. Eine andere Möglichkeit ist ein Ordner/Namensraum VIEWMODEL direkt in der Windows-Forms-Anwendung.

Für eine Form erstellen wir dann eine zugehörige ViewModel-Klasse FormNameViewModel. Anders als im Beispiel verwenden wir in der Form zunächst statt der lokalen Variable eine Instanzvariable:

Private ReadOnly ViewModel As FormNameViewModel = New FormNameViewModel()

Anschließend ergeben sich für jede Steuerelementeigenschaft folgende drei Schritte:

  1. ViewModel-Klasse: Eigenschaft anlegen
  2. Konstruktor der Form: Steuerelementeigenschaft an die ViewModel-Eigenschaft binden
  3. Code-Behind der Form: Steuerelementeigenschaft durch die ViewModel-Eigenschaft ersetzen

Sobald eine Methode der Form nur noch ViewModel-Eigenschaften und lokale Variablen verwendet, können wir sie mit dem „Move Method“-Refactoring in die ViewModel-Klasse verschieben. Zugegebenermaßen liest sich das deutlich leichter, als es üblicherweise in historisch gewachsenen Anwendungen ist. Das Prinzip ist aber tatsächlich so einfach.

Empfehlenswert ist es, nur genau die Steuerelemente umzustellen, die für das aktuell zu implementierende Feature bzw. die Fehlerkorrektur angefasst werden. Kein Kunde zahlt schließlich für eine MVVM-Komplettumstellung, die er im UI nicht sieht. Punktuelle Umstellungen im Rahmen des normalen Entwicklungsprozesses amortisieren sich wie andere kleine Refactorings sofort oder sehr schnell, sodass der Kunde nicht damit belästigt werden muss.

Feintuning und Troubleshooting

Üblicherweise wird die Implementierung der INotifyPropertyChanged-Schnittstelle in eine abstrakte Basisklasse mit dem Namen NotificationObject oder ViewModelBase ausgelagert, von der dann die konkreten ViewModel-Klassen erben. Ab .NET 4.5 kann mithilfe des CallerMemberName-Attributs der Eigenschaftsname für das PropertyChanged-Event vom Compiler gesetzt werden.

Besondere Sorgfalt sollten Windows-Forms-MVVM-Entwickler auf die Zeichenketten verwenden, die sie bei der Datenbindung einsetzen. Stimmen diese nicht exakt mit dem Namen der Eigenschaft des Steuerelements bzw. des ViewModels überein oder enthalten sie ein Leerzeichen, verursacht das beim Start der Anwendung eine ArgumentException („An die Eigenschaft oder Spalte FehlerhafteZeichenkette für die DataSource kann nicht gebunden werden“).

Frameworks für WinForms mit MVVM

Das einzige mir bekannte MVVM-Framework mit WinForms-Unterstützung ist der WAF Windows Forms Adapter für das WPF Application Framework (WAF). Dieser ist allerdings 2010 im Alphastadium steckengeblieben und hat zumindest damals bei mir nicht intuitiv funktioniert.

Außerdem gibt es Posts auf Stackoverflow und Co., die für das Command Binding generischere Ansätze verfolgen als ich im gleichnamigen Abschnitt dieses Artikels. Wer das vertiefen möchte, liest am besten die Top-5-Treffer für den Suchbegriff „MVVM WinForms“ in Google.

Praxiserfahrung

Das MVVM Pattern ist auch für WinForms-Anwendungen sinnvoll und mit Bordmitteln aus dem .NET Framework machbar. Für gewachsene Anwendungen, die sich der Unwartbarkeit nähern, ist die schrittweise Einführung des Patterns ein gangbarer Weg zum Licht am Ende des Tunnels.

Bei komplexen bzw. überladenen Oberflächen mit zahlreichen Abhängigkeiten zwischen den Steuerelementen, die auf jede Benutzereingabe mit einem verzweifelten RefreshAll() reagieren, entfaltet MVVM seine besondere Magie: Im ViewModel werden Abhängigkeiten plötzlich viel klarer, und Aktualisierungen im UI gelingen gezielt per PropertyChanged-Event.

Codeästheten oder Unit-Test-Versteher, die einmal eine vollfunktionale Form gesehen haben, die außer ihrem Design nur einen Konstruktor mit einigen quasideklarativen Codezeilen enthält, wollen sowieso nichts anderes mehr. Lumos!

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -