NewsMVVM zum Ersten: Datum und Uhrzeit

1. Aufgabe

In dieser und den folgenden beiden Fallstudien widmen wir uns dem Thema WPF (Windows Presentation Foundation) und MVVM-Entwurfsparadigma (Model-View-ViewModel). Für eine WPF-Anwendung, die per se immer aus den beiden Bestandteilen XAML und Codebehind besteht, empfiehlt es sich – ganz im Sinne eines strukturierten SW-Entwurfs – die Implementierung auf drei Bestandteile aufzuteilen:

  • Model – Das Modell verwaltet die Daten der Anwendung. Dies können im einfachsten Fall Datenstrukturen sein, die sich im Hauptspeicher ausbreiten oder in praxisnäheren Anwendungen Daten einer Datenbank oder anderer persistenter Ablagen. In jedem Fall kümmert sich ein Modell nicht um die grafische Visualisierung des Datenbestands.

  • View – Dieser Teil des Entwurfsmusters ist für die grafische Darstellung der Daten verantwortlich, also für das, was der Anwender zu sehen bekommt. Häufig gibt es in einer Anwendung mehrere Views, also mehrere Softwarebausteine, die jeder für sich eine bestimmte Teilmenge des gesamten Datenbestands grafisch darstellen.

  • ViewModel – Zwischen der Datenschicht (Model) und der Darstellung der Daten (View) ist das ViewModel eingefügt. Es stellt gewissermaßen das Bindeglied zwischen dem Modell und der oder den Views dar und verwaltet den Fluss von Daten und Aktivitäten in beiden Richtungen. Werden in der View Benutzereingaben (Bedienereignisse) entgegengenommen, so werden diese im ViewModel geeignet aufbereitet (datentypspezifisch angepasst, umgewandelt oder auch verdichtet), um sie schließlich an das Modell weiterzureichen. Ändert sich der Zustand des Modells, ist wiederum das ViewModel dafür verantwortlich, dass alle Views ihre Oberfläche entsprechend aktualisieren.

Wir nähern uns dem MVVM-Entwurfsparadigma mit langsamen Schritten in einer ersten, sehr einfachen Anwendung. Erstellen Sie eine Anwendung, die das aktuelle Datum inklusive Uhrzeit in einer WPF-Anwendung auf Basis des MVVM-Entwurfsparadigmas darstellt. An die Oberfläche werden keine großen Anforderungen gestellt, wie man in Abbildung 1 erkennen kann:

Eine einfache MVVM-Anwendung zur Darstellung von Datum und Uhrzeit.

Abbildung 1. Eine einfache MVVM-Anwendung zur Darstellung von Datum und Uhrzeit.

2. Lösung

Quellcode: Siehe auch https://github.com/peterloos/Wpf_MVVM_SimpleClock.

In einer publizierbaren Lösung dieser Aufgabe sollten wir darauf achten, dass die View ausschließlich mit XAML, das Model als auch das ViewModel ausschließlich in C# erstellt sind. Wenn es uns nun noch gelingt, dass das Model nichts von einem ViewModel und das ViewModel wiederum nichts von einer View weiß, sind wir schon recht gut aufgestellt. Betrachten wir die einzelnen Bausteine des MVVM-Entwurfsmusters in der Reihenfolge Bottom-Up und fangen in Listing 1 mit der Implementierung des Modells an:

01: namespace Wpf_MVVM_SimpleClockSample_Model
02: {
03:     public delegate void DateChangedEventHandler (DateTime date);
04:     public delegate void TimeChangedEventHandler (TimeSpan time);
05: 
06:     public class ClockModel
07:     {
08:         public event DateChangedEventHandler DateChanged;
09:         public event TimeChangedEventHandler TimeChanged;
10: 
11:         private DispatcherTimer dispatcherTimer;
12:         private DateTime lastTime;
13: 
14:         public ClockModel()
15:         {
16:             this.DateChanged = null;
17:             this.TimeChanged = null;
18: 
19:             this.lastTime = DateTime.Now;
20: 
21:             this.dispatcherTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
22:             this.dispatcherTimer.Tick += this.TimerTick;
23:         }
24: 
25:         public DateTime LastDate
26:         {
27:             get
28:             {
29:                 return this.lastTime.Date;
30:             }
31:         }
32: 
33:         public TimeSpan LastTime
34:         {
35:             get
36:             {
37:                 return this.lastTime.TimeOfDay;
38:             }
39:         }
40: 
41:         public void Start()
42:         {
43:             this.dispatcherTimer.Start();
44:         }
45: 
46:         public void Stop()
47:         {
48:             this.dispatcherTimer.Stop();
49:         }
50: 
51:         private void TimerTick (Object sender, EventArgs e)
52:         {
53:             DateTime last = this.lastTime;
54:             this.lastTime = DateTime.Now;
55: 
56:             if (this.lastTime.Date != last.Date)
57:                 if (this.DateChanged != null)
58:                     this.DateChanged.Invoke(this.lastTime.Date);
59: 
60:             if (this.lastTime.TimeOfDay != last.TimeOfDay)
61:                 if (this.TimeChanged != null)
62:                     this.TimeChanged.Invoke(this.lastTime.TimeOfDay);
63:         }
64:     }
65: }

Beispiel 1. Implementierung eines Modells: Klasse ClockModel.


Auf einige Aspekte in der Realisierung des Modells wollen wir aufmerksam machen. Da wäre zunächst einmal die Variable lastTime in Zeile 12. Wenngleich nicht sehr voluminös, so repräsentiert diese einzelne Variable in unserem Beispiel den Datenbestand des Modells. Der Datentyp DateTime wurde mit Bedacht gewählt. In typischen Realisierungen eines Modells findet man in der Regel Standarddatentypen des .NET-Frameworks vor, da diese ja eben den Zugriff auf die Datenschicht einer Anwendung ermöglichen. In Real-Word-Applikationen könnten dies beispielsweise Instanzen der Klassen SqlDataRecord, CloudBlockBlob, XmlDocument, etc. sein.

Zur Aktualisierung der Tageszeit gelangt eine Instanz der Klassen DispatcherTimer zum Einsatz. Dahinter verbirgt sich ein kleiner Kunstkniff, denn mit dieser Klasse umgehe ich das Problem, dass das Eintreffen von Ereignissen prinzipiell in beliebigen Threads erfolgen kann. Für Tick-Ereignisse eines DispatcherTimer-Objekts ist immer garantiert, dass diese im UI-Thread der Anwendung eingespeist werden. Auf diese Weise kann ich die Beispielanwendung von unnötigem Ballast wie etwa dem Einsatz eines Dispatcher-Objekts befreien.

Die Klasse ClockModel besitzt zwei Eigenschaften LastDate und LastTime, die das aktuelle Datum und die aktuelle Uhrzeit enthalten. Benutzer des Modells können sich mit den beiden Ereignissen DateChanged und TimeChanged über Änderungen informieren lassen. Auch hier wurde darauf geachtet, dass die delegate-Typen DateChangedEventHandler und TimeChangedEventHandler in ihren Methodensignaturen Standarddatentypen verwenden: Den Strukturtyp DateTime für ein Datum und TimeSpan für eine Uhrzeit. In den Zeilen 56 und 60 wiederum wird sichergestellt, dass die beiden Ereignisse DateChanged und TimeChanged nur dann ausgelöst werden, wenn die zu Grunde liegende Eigenschaft sich auch tatsächlich geändert hat. Dies ist aber keine Besonderheit des MVVM-Entwurfsmusters, sondern gilt prinzipiell immer für .NET-Ereignisse. In Listing 2 fahren wir mit dem ViewModel der Anwendung fort:

01: using Wpf_MVVM_SimpleClockSample_Model;
02: 
03: namespace Wpf_MVVM_01_SimpleClockSample
04: {
05:     public class ClockViewModel : INotifyPropertyChanged
06:     {
07:         public event PropertyChangedEventHandler PropertyChanged;
08: 
09:         private ClockModel model;
10: 
11:         private String currentTime;
12:         private String currentDate;
13: 
14:         public ClockViewModel()
15:         {
16:             this.model = new ClockModel();
17: 
18:             this.currentDate = String.Format("{0:00}.{1:00}.{2:0000}", 
19:                 this.model.LastDate.Day,
20:                 this.model.LastDate.Month,
21:                 this.model.LastDate.Year);
22: 
23:             this.currentTime = String.Format("{0:00}:{1:00}:{2:00}",
24:                 this.model.LastTime.Hours,
25:                 this.model.LastTime.Minutes,
26:                 this.model.LastTime.Seconds);
27: 
28:             this.model.DateChanged += this.Model_DateChanged;
29:             this.model.TimeChanged += this.Model_TimeChanged;
30: 
31:             this.model.Start();
32:         }
33: 
34:         public String CurrentTime
35:         {
36:             set
37:             {
38:                 this.currentTime = value;
39: 
40:                 if (this.PropertyChanged != null)
41:                     this.PropertyChanged
42:                         (this, new PropertyChangedEventArgs("CurrentTime"));
43:             }
44: 
45:             get
46:             {
47:                 return this.currentTime;
48:             }
49:         }
50: 
51:         public String CurrentDate
52:         {
53:             set
54:             {
55:                 this.currentDate = value;
56: 
57:                 if (this.PropertyChanged != null)
58:                     this.PropertyChanged
59:                         (this, new PropertyChangedEventArgs("CurrentDate"));
60:             }
61: 
62:             get
63:             {
64:                 return this.currentDate;
65:             }
66:         }
67: 
68:         private void Model_TimeChanged(TimeSpan time)
69:         {
70:             this.CurrentTime = String.Format("{0:00}:{1:00}:{2:00}",
71:                 time.Hours, time.Minutes, time.Seconds);
72:         }
73: 
74:         private void Model_DateChanged(DateTime date)
75:         {
76:             this.CurrentDate = String.Format("{0:00}.{1:00}.{2:0000}",
77:                 date.Day, date.Month, date.Year);
78:         }
79:     }
80: }

Beispiel 2. Implementierung eines ViewModells: Klasse ClockViewModel.


Im ViewModel finden wir in den Zeilen 11 und 12 zwei Instanzvariablen currentTime und currentDate vor. Zu Demonstrationszwecken soll gezeigt werden, wie das ViewModel die Daten des Modells in Bezug auf ihren Datentyp umwandelt. Natürlich wäre dies bei Originaldaten des Typs DateTime bzw. TimeSpan nicht wirklich erforderlich gewesen, aber spätestens bei einem SqlDataRecord-Objekt ist man hierzu gezwungen. Es ist eben eine der Hauptaufgaben des ViewModels, die Daten für die Visualisierung in einer neutralen Repräsentation aufzubereiten, zum Beispiel in Zeichenketten.

Die beiden privaten Hilfsmethoden Model_TimeChanged (Zeile 68) und Model_DateChanged (Zeile 75) verbinden das ViewModel mit dem unterlagerten Modell. Das Modell selbst kennt sein überlagertes ViewModel nicht. Es stellt aber Ereignisse auf der Basis des .NET-Standardereignismodells zur Verfügung, um eine Integration in das MVVM-Entwurfsmuster zu ermöglichen.

Nun schreiten wir im MVVM-Entwurfsmuster eine weitere Ebene nach oben, sprich wir nähern uns einer View. Die beiden Eigenschaften CurrentTime und CurrentDate sind zum einen vom Typ String, also geeignet für den Gebrauch in einer Visualisierung. Zum anderen lösen sie das PropertyChanged-Ereignis aus. Dies ist für Views erforderlich, wenn diese sich (in XAML) mit dem Mechanismus der Datenbindung mit einem ViewModel verknüpfen wollen.

Damit sind wir schon in der letzten Ebene des Entwurfsmusters angekommen, der View. Diese realisieren wir ausschließlich in XAML, also rein deklarativ, wie wir in Listing 3 erkennen können:

01: <Window
02:     x:Class="Wpf_MVVM_SimpleClockSample.MainWindow"
03:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
04:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
05:     xmlns:local="clr-namespace:Wpf_MVVM_01_SimpleClockSample"
06:     Title="Date and Time with MVVM" Height="120" Width="400">
07: 
08:     <Window.Resources>
09:         <local:ClockViewModel x:Key="MyClockViewModel"/>
10:     </Window.Resources>
11: 
12:     <StackPanel DataContext="{StaticResource MyClockViewModel}">
13:         <TextBlock
14:             Text="{Binding CurrentTime, StringFormat='Time: {0}'}"
15:             Margin="10" FontFamily="Consolas" FontSize="18" Background="LightGray" />
16:         <TextBlock
17:             Text="{Binding CurrentDate, StringFormat='Date: {0}'}"
18:             Margin="10" FontFamily="Consolas" FontSize="18" Background="LightGray" />
19:     </StackPanel>
20: </Window>

Beispiel 3. Implementierung einer View: Klasse MainWindow.


Wie werden eine View und ein unterlagertes ViewModel miteinander verbunden? Wie gehabt gilt zunächst die Spielregel, dass ein ViewModel seine Views nicht kennt. Damit eine Kommunikation möglich ist, stellt das ViewModel – wiederum auf der Basis des .NET-Standardereignismodells – eine Implementierung der INotifyPropertyChanged-Schnittstelle zur Verfügung. Diese Schnittstelle definiert ein Ereignis PropertyChanged, den zentraler Anker der WPF-Datenbindung.

Wie können wir in einer deklarativ beschrieben View überhaupt ein ViewModel-Objekt anlegen? Hierzu gibt es mehrere Möglichkeiten, ich habe mich für den Weg einer statischen Ressource entschieden, siehe dazu den Abschnitt

<Window.Resources>
    <local:ClockViewModel x:Key="MyClockViewModel"/>
</Window.Resources>

in den Zeilen 8 bis 10. Damit kommen wir zum letzten Hinweis zu Listing 3. Für die Datenbindung ist es erforderlich, dass die View ihr Bezugsobjekt für die Datenbindung mitgeteilt bekommt. Für diesen Zweck besitzen WFP-UI-Klassen die Eigenschaft DataContext. In Zeile 12 bekommt diese die Objektreferenz MyClockViewModel zugewiesen. Alle nachfolgenden Datenbindungen, zum Beispiel

Text="{Binding CurrentTime, StringFormat='Time: {0}'}"

in Zeile 14 wissen nun, an welchem Objekt sie (mit Hilfe des PropertyChanged-Ereignisses) zu den Daten für die Visualisierung gelangen.