Xamarin.Forms: Wie ist Dein Name?

27. April 2016

In WPF kann man XAML-Elemente nach ihrem Namen fragen. In Xamarin.Forms geht das nicht…

Wenn man im Code-Behind einer XAML-Datei auf UI-Elemente zugreifen will, muss man Ihnen einen Namen geben (Attribut x:Name). Dadurch wird ein entsprechender Member generiert, der den Zugriff erlaubt.

Ein zweites Szenario, bei dem der Name ins Spiel kommt, ist wiederverwendbarer Code. Anhand des Namens lässt sich über passende Konventionen ein automatisches Databinding gegen Properties im ViewModels durchführen, oder Buttons werden – ohne dass man ein ICommand zur Verfügung stellen muss – an Methoden gebunden. Caliburn.Micro setzt das entsprechend um, das lässt sich aber auch einfach selbst implementieren. Allerdings muss man dafür UI-Elemente nach ihrem Namen fragen können.

Um es deutlicher zur machen, die Möglichkeiten in WPF…

UI-Elemente benennen:

   1: <Button x:Name="btnNew" Content="New" Click="Button_Click" />

   2: <Button x:Name="btnDelete" Content="Delete" Click="Button_Click" />

Suche des UI-Elementes über seinen Namen (nicht den generierten Member, der nur im Code-Behind verfügbar ist):

   1: var btn = this.FindName("btnNew"); // oder über VisualTreeHelper suchen...

Ein UI-Element nach seinem Namen fragen:

   1: private void Button_Click(object sender, RoutedEventArgs e)

   2: {

   3:     FrameworkElement fe= (FrameworkElement)sender;

   4:     if (fe.Name == "btnNew")

   5:         //...

   6: }

 

 

Xamarin.Forms…
… kennt ebenfalls das x:Name-Attribut im XAML. Danach wird die Luft aber schon merklich dünner. Was noch funktioniert ist folgendes:

Man kann das Element suchen…

   1: var btn = this.FindByName<Button>("btnCreate");

… allerdings funktioniert das nur im Konstruktor  zuverlässig (dafür ist es gedacht – im generierten Code wird das benutzt um den generierten Member zu bestücken). Bei folgendem Code ist man aber schon von den Randbedingungen abhängig:

   1: public RecordingListPage()

   2: {

   3:     InitializeComponent();

   4:     this.Appearing += RecordingListPage_Appearing;

   5:     var btn = this.FindByName<Button>("btnCreate");    // OK!

   6: }

   7:

   8: private void RecordingListPage_Appearing(object sender, System.EventArgs e)

   9: {

  10:     var btn = this.FindByName<Button>("btnCreate");     // _kann_ null sein!!!

  11: }

Befindet man sich in der Hauptseite (ContentPage, TabbedPage oder ähnliches), dann findet man im Eventhandler die entsprechenden Elemente. Ist man jedoch in einer ContentPage, die eine Seite in einer TabbedPage darstellt, dann findet man dort nichts mehr. Übrigens auch nicht, wenn man über den Parent (also die TabbedPage) sucht.

Und die Anforderung den Namen eines gegebenen UI-Elementes zu erfragen können wir gleich begraben. Es gibt schlicht und einfach kein Name-Property.

 

Lösungsansatz…
Natürlich kann man sich jetzt mit Reflector durch Xamarin durchwühlen und per Reflection versuchen, aus den Innereien den Namen auszugraben. Man wird aber schnell feststellen, dass das eine Sackgasse ist (unter Xamarin ist ein Zugriff per Reflection auf private Member nicht möglich).

Andererseits: Wir reden über das Schreiben von generischem Code. Dafür müssen die Elemente identifizieren werden können, das muss aber nicht zwangsweise über x:Name geschehen. Ergo ist die einfachste Lösung ein eigenes Property, idealerweise als attached property:

   1: public static class Extensions

   2: {

   3:     public static readonly BindableProperty NameProperty = BindableProperty.CreateAttached("Name", typeof(string), typeof(Extensions), null);

   4:     public static string GetName(this BindableObject bo) { return (string)bo.GetValue(Extensions.NameProperty); }

   5:     public static void SetName(this BindableObject bo, string value) { bo.SetValue(Extensions.NameProperty, value); }

   6: }

Nutzung in XAML:

   1: <?xml version="1.0" encoding="utf-8" ?>

   2: <ContentPage [...]

   3:              xmlns:controls="clr-namespace:SDX.Xamarin.Common.UI.Controls;assembly=SDX.Xamarin.Common.UI" >

   4:     <ContentPage.ToolbarItems>

   5:         <ToolbarItem controls:Extensions.Name="tiCreate" Name="Add" Order="Primary" />

   6:     </ContentPage.ToolbarItems>

Und im Code:

   1: var name = this.ToolbarItems[0].GetName();

Und sollte Xamarin Microsoft sich irgendwann entschließen, das x:Name-Property doch noch einzuführen, dann lassen sich die Aufrufe in der Klasse Extensions einfach darauf umbiegen.

Fazit
Auch wenn Xamarin.Forms eine Menge kann, stößt man doch auch oft an Grenzen – manchmal auch bei Grundlagen, bei denen man das nicht erwartet hatte. Andererseits bietet es aber i.d.R. genügend Stellschrauben, um selbst Hand anzulegen.