Trigger, Behavior und Action

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>

 

image

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.

Kommentare sind geschlossen