Actions als BindableProperty

Ihr erstellt in Xamarin.Forms gerade ein eigenes ContentView und fragt euch, wie ihr jetzt Properties erstellt, die anschließend mit einem Binding versehen werden können?
Easy going, das Stichwort heißt: BindableProperty.

Beispiel

Fangen wir mit einem einfachen Beispiel an. Ich habe ein ContentView mit einem Button, welches eine dahinter definierte Aufgabe ausführt (welche Aufgabe das ist, spielt in dem Falle keine Rolle).

public static readonly BindableProperty ExecutableTaskProperty = BindableProperty.Create(
  nameof(ExecutableTask),
  typeof(string),
  typeof(TaskStarter),
  string.Empty);

public string ExecutableTask
{
  get { return GetValue(ExecutableTaskProperty)?.ToString(); }
  set { SetValue(ExecutableTaskProperty, value); }
}

So, was haben wir hier? Unser eigenetliches Property heißt ExceutableTask. Diesen Namen werden wir bei späterer Benutzung auch von aussen sehen. Wir erstellen uns hier ein public static readonly Property vom Typ BindableProperty. Der Name unserer BindableProperty muss exakt der selbe sein, wie unsere eigentliche Property, PLUS das Postfix „Property“ (hat jemand gezählt wie oft das Wort „Property“ in diesem Beitrag vorkommt? ;)) — also „ExecutableTaskProperty„. Ist dies nicht der Fall, kann Xamarin das Verhältnis beider nicht auflösen und wir wundern uns warum es nicht funktioniert.

Create-Methode und Parameter

Die Create-Methode erstellt uns unsere bindbare Variable, verlangt aber vorher eine Reihe an Parameter.
Hier einmal die Pflicht-Parameter (es gibt noch ein paar optionale):

  • propertyName — Der Name, der später nach aussen für die Benutzung publiziert wird (mit nameof vermeidet man hart codierte Strings)
  • returnType — Der Typ unserer Property, in unserem Falle string
  • declaringType —  Achtung! Der Typ unseres ContentViews (mein ContentView heißt TaskStarter)

Als optionale Parameter gibts dann folgende:

  • defaultValue — Welcher Wert per default zurück gegeben wird. string.Empty bietet sich an um null zu vermeiden
  • defaultBindingMode — Standardmäßig „OneWay“, kann aber auch auf jeden anderen gesetzt werden (mit dem enum BindingMode)

Als drittes nimmt die Methode auch noch einige Delegate an um auf Änderungen des Properties zu reagieren:

validateValue — dient zum Prüfen des Inputs
propertyChanged — wird invoked, nachdem das Property geändert wude
propertyChanging — wird invoked, bevor das Property geändert wird

GetValue und SetValue

schauen wir uns unser eigentliches Property an, werden wir feststellen das dort im Getter und Setter jeweils die Methode GetValue() und SetValue() aufgerufen wird. Diese sind nötig, um vom BindableProperty die Daten herauszuholen, bzw. hinein zu befördern, wenn wir in unserer ContentView-Klasse damit arbeiten. Es dient also quasi als „storage“ für unseren Wert. Der Aufruf der beiden Methoden erklärt sich denke ich von selbst.

Benutzen des ContentViews

Die Implementation der bindbaren Property ist ebenso trivial:

<scheduler:TaskStarter Grid.Row="2" 
  OnTaskFinished="{Binding OnMyTaskFinished}" 
  ExecutableTask="{Binding MyTask}"
  HorizontalOptions="FillAndExpand" />

Unser ContentView bietet uns die Property ExecutableTask an und wir können sie benutzen. Hier sieht man auch gleich noch eine zweite Property, welche ich erstellt habe: OnTaskFinished.

Actions und Delegaten nutzen

Da wir als Typ alles nehmen können, was uns zur Verfügung steht, können wir hier selbstverständlich auch mit Delegaten arbeiten. Um die Implementation von oben aufzugreifen, schauen wir uns das an der Property „OnTaskFinished“ an:

public static readonly BindableProperty OnTaskFinishedProperty = BindableProperty.Create(nameof(OnTaskFinished),
      typeof(Action<bool, string>),
      typeof(TaskStarter),
      null);

public Action<bool, string> OnTaskFinished
{
  get 
  { 
    return (Action<bool, string>) GetValue(OnTaskFinishedProperty); 
  }
  set { SetValue(OnTaskFinishedProperty, value); }
}

Als Typ wurde hier Action<bool, string> verwendet, um später zurück zu liefern, ob die Task erfolgreich war und noch eine Message dazu. Der Name beinhaltet hier ebenfalls das Postfix „Property“, kombiniert mit dem eigentlichen Namen.

In unserem Code der ContentView können wir nun also Prima die Delegate Invoken, wenn wir meinen, die Task wurde ausgeführt (Fragezeichen für null-Prüfung nicht vergessen):

OnTaskFinished?.Invoke(success, msg);

Troubleshooting

Euer Property ist immer NULL? Dem Problem stand ich auch gegenüber und musste eine Weile suchen. Die Lösung war recht simpel: ich nutzte in meinem ContentView ein ViewModel und wollte dies natürlich als BindingContext setzen. Mein Problem war aber, das ich den BindingContext von dem ContentView selbst genommen habe, also this. Das geht nicht(!). Stattdessen mus man den BindingContext vom Content nehmen.

aus

this.BindingContext = viewModel = new TaskStarterViewModel();

wird also

Content.BindingContext = viewModel = new TaskStarterViewModel();

Und jetzt klappt auch alles weitere.