Das Entwurfsmuster MVVM lässt einfach viel interpretationsspielraum zu. Im folgenden versuche ich meine Gedanken zu Command Binding zu ordnen. Die selbst gestellte Aufgabe definiert ein Formular mit Button und Textblock, der beim Click eins hochzählt. Ein Einzeiler mit Event Click getriggerten Code.
In MVVM werden diese Click Events auch in das View Model gelegt. Mit welcher Intensität das getrieben wird, bleibt jedem selbst überlassen. Ich halte nach wie vor Code im UI Teil (also der View) für zulässig. Obwohl XAML, finden sich je nach Frontend WPF, Win 8 oder Silverlight speziell beim Command Binding höchst unterschiedliche Lösungsansätze. Selbst Funktionell Identes, wird unterschiedlich benannt (DelegateCommand, RelayCommand oder ActionCommand?). Dieser Artikel setzt auf den Status Quo anhand von Silverlight 5.
Es gibt eine XAML Datei und eine Klasse für die Daten und Methoden, die als ViewModel bezeichnet wird.
Basis ist ein Viewmodel mit einer Eigenschaft Count, die das UI per Event über Änderungen benachrichtigen kann und eine Click Ersatz Methode die hochzählt.
1: Public Sub MyClick()
2: counter += 1
3: End Sub
4:
5: Private _counter As Integer
6: Public Property counter() As Integer
7: Get
8: Return _counter
9: End Get
10: Set(ByVal value As Integer)
11: _counter = value
12: OnPropertyChanged()
13:
14: End Set
15: End Property
Eine sehr kleine Randnotiz zum parameterlosen Aufruf von OnPropertyChanged. Seit .net 4.5 gibt es ein Attribut CallerMemberName. Da dies durch den Compiler aufgelöst, wird lässt es sich auch in Silverlight 5 nutzen. Allerdings erst mit 4 Zeilen Code oder per Nuget Microsoft.bcl einbinden.
1: Public Event PropertyChanged(sender As Object, e As PropertyChangedEventArgs)
Implements INotifyPropertyChanged.PropertyChanged
2:
3:
4: Protected Sub OnPropertyChanged(<CallerMemberName> Optional propertyName As String = Nothing)
5:
6: ' Protected Sub OnPropertyChanged(propertyName As String) 'nuget microsoft.bcl
7: RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
8: End Sub
Expression Blend bringt bei Silverlight zwei wunderbare Funktionen mit, die MVVM wirklich einfach gestalten. Wenn man aus dem Modell die Methode auf das Control zieht erstellt Blend ein Behavior. Zunächst muss das ViewModell aber per Deklaration an den Datacontext gebunden werden.
1: <UserControl.DataContext>
2: <local:DemoVM />
3: </UserControl.DataContext>
Ein Control kann auch mehrere Behaviors erhalten. Im XAML Code sieht das dann so aus.
1: <Button Content="Blend" HorizontalAlignment="Left" Margin="27,37,0,0"
VerticalAlignment="Top" Width="75" >
2: <i:Interaction.Triggers>
3: <i:EventTrigger EventName="Click">
4: <ei:CallMethodAction MethodName="MyClick" TargetObject="{Binding}"/>
5: </i:EventTrigger>
6: </i:Interaction.Triggers>
7: </Button>
Windows 8 kann das nicht. Soweit angekündigt werden in 8.1 Behaviors über ein SDK nachgeliefert.
Die zweite Methode stammt aus dem Namensraum Microsoft.Expression.Interactivity.Core, das ActionCommand. Grundsätzlich bieten einige wenige Controls (z.B. Button oder Hyperlink) ein DependencyProperty Command das man zum binden nutzen kann. Gemeinsames Interface ist ICommand.
1: <Button Content="Action" HorizontalAlignment="Left" Margin="27,128,0,0"
2: Command="{Binding meinCommand2}" VerticalAlignment="Top" Width="75"/>
3:
Um dieses Command im ViewModel verwenden zu können, ist nur wenig VB.NET Programmcode nötig, wie das Beispiel zeigt.
1: Public Class DemoVM
2: Implements INotifyPropertyChanged
3: Public Property meinCommand2() As ICommand
4: ..
5: Public Sub New()
6: _counter = 0
7: Me.meinCommand2 = New ActionCommand(AddressOf MyClick)
8: ...
Nun gibt es beides nicht in Windows 8. Wie wäre also der Weg, um selbst ein Command zu erstellen. Dazu braucht es eine Klasse, die das ICommand Interface implementiert. Bleibt das Problem wie man darin auf die Daten in Viewmodel zugreift. Eine Möglichkeit ist, den DataContext beim Aufruf einfach per Referenz mitzugeben.
Wichtig ist, das CanExecute beim initialen Aufruf true zurück liefert oder per CanExecuteChanged über die Zustandsänderung benachrichtig. Andernfalls kann schnell mal ein Button grau also disabled sein.
1: Public Class MyClickIbase
2: Implements ICommand
3: Dim _demoVM As DemoVM
4: Public Sub New()
5:
6: End Sub
7: Public Sub New(data As DemoVM)
8: _demoVM = data
9: End Sub
10:
11: Public Function CanExecute(parameter As Object) As Boolean Implements ICommand.CanExecute
12: Return True
13: End Function
14:
15: Public Event CanExecuteChanged(sender As Object, e As EventArgs)
Implements ICommand.CanExecuteChanged
16:
17: Public Sub Execute(parameter As Object) Implements ICommand.Execute
18: _demoVM.counter += 1
19:
20: End Sub
21: End Class
Um im Viewmodell Zugriff auf diese Command Klasse zu haben, wird ein Property zum lesen benötigt.
1: Private _MyClickI As MyClickIbase
2: Public ReadOnly Property MyClickI() As MyClickIbase
3: Get
4:
5: If IsNothing(_MyClickI) Then _MyClickI = New MyClickIbase(Me)
6:
7: Return _MyClickI
8: End Get
9:
10: End Property
Der XAML Button wird dann wieder direkt an diese Eigenschaft gebunden
1: <Button Content="icommand" HorizontalAlignment="Left" Margin="27,84,0,0"
VerticalAlignment="Top" Width="75"
2: Command="{Binding MyClickI}" >
3:
In der Praxis treibt niemand diesen Aufwand für jedes Command einzeln, sondern abstrahiert mit einer weiteren Klasse, meist RelayCommand genannt.
1: Public Class myRelayCommand
2: Implements ICommand
3:
4: Private _handler As Action
5: Public Sub New(ByVal handler As Action)
6: IsEnabled = True
7: _handler = handler
8: End Sub
9:
10: Private _isEnabled As Boolean
11: Public Property IsEnabled() As Boolean
12: Get
13: Return _isEnabled
14: End Get
15: Set(ByVal value As Boolean)
16: If value <> _isEnabled Then
17: _isEnabled = value
18: RaiseEvent CanExecuteChanged(Me, EventArgs.Empty)
19: End If
20: End Set
21: End Property
22:
23: Public Function CanExecute(ByVal parameter As Object) As Boolean
Implements System.Windows.Input.ICommand.CanExecute
24: Return IsEnabled
25: End Function
26:
27: Public Event CanExecuteChanged As EventHandler
Implements System.Windows.Input.ICommand.CanExecuteChanged
28:
29: Public Sub Execute(ByVal parameter As Object)
Implements System.Windows.Input.ICommand.Execute
30: _handler()
31: End Sub
32:
33: End Class
Es wird der Funktionsaufruf delegiert (Delegate), entweder per Anonyme Methode (Lambda) oder mit dem VB typischen Addressof
1: Public Property handmade() As myRelayCommand
2: ' Get
3: ' Return New myRelayCommand(Sub()
4: ' MyClick()
5: ' End Sub)
6: ' End Get
7: 'End Property
8: ....
9: Public Sub New()
10: ...
11: handmade = New myRelayCommand(AddressOf MyClick)
Handmade wird dann wieder mit bekannter Binding Syntax im XAML an das Command Attribut gebunden.
Da es viele Sonderfälle gibt die behandelt werden wollen, greift viele Entwickler in der Praxis auf Frameworks zurück die diesen Overhead Code verstecken. Von Microsoft stammt PRISM, ein weiteres ist MVVM Light. Letzteres habe ich per Nuget hinzugefügt. Dann wird nur noch Property und Methodenzuordnung benötigt.
1: Imports GalaSoft.MvvmLight.Command
2: Public Class DemoVM
3: ..
4: Public Property mvvmlightCommand As relayCommand
5: ...
6: Public Sub New()
7: Me.mvvmlightCommand = New RelayCommand(AddressOf MyClick)
8: ....
Mir ist bewusst, das man über das Thema reichlich und kontrovers diskutieren kann. Einfach eine Mail an mich.