Mai 2019

Band 34, Nummer 5

[XAML]

Benutzerdefinierte XAML-Steuerelemente

Von Jerry Nixon

Als Entwickler im Unternehmen sind Sie mit SQL Server vertraut. Sie kennen die .NET-Webdienste. Und für Sie ist das Entwerfen ansprechender XAML-Schnittstellen (wahrscheinlich mit Windows Presentation Foundation [WPF]) ein Kinderspiel. Wie bei Tausenden anderen karriereorientierten Entwicklern finden sich Microsoft-Technologien in Ihrem Lebenslauf, und Sie schneiden Artikel wie diesen aus MSDN Magazine aus und heften sie an Ihr Kanban-Board an. Holen Sie schon einmal Ihre virtuelle Schere, dieser Artikel ist es wert.

Es ist an der Zeit, Ihr Fachwissen zu XAML-Steuerelementen zu erweitern. Das XAML-Framework bietet eine umfangreiche Bibliothek von Steuerelementen für die Benutzeroberflächenentwicklung. Um aber das zu erreichen, was Sie möchten, brauchen Sie mehr. In diesem Artikel zeige ich Ihnen, wie Sie mithilfe von benutzerdefinierten XAML-Steuerelementen die gewünschten Ergebnisse erzielen können.

Benutzerdefinierte Steuerelemente

Es gibt zwei Ansätze zum Erstellen von benutzerdefinierten Steuerelementen in XAML: Benutzersteuerelemente und Steuerelemente mit Vorlagen. Benutzersteuerelemente sind ein einfacher, designerfreundlicher Ansatz zur Erstellung eines wiederverwendbaren Layouts. Steuerelemente mit Vorlagen bieten ein flexibles Layout mit einer anpassbaren API für Entwickler. Wie in jeder Sprache können anspruchsvolle Layouts Tausende von Zeilen XAML generieren, in denen es schwierig sein kann, produktiv zu navigieren. Benutzerdefinierte Steuerelemente sind eine effektive Strategie, um Layoutcode zu reduzieren.

Die Wahl des richtigen Ansatzes wirkt sich darauf aus, wie erfolgreich Sie die Steuerelemente in Ihrer Anwendung verwenden und wiederverwenden können. Die folgenden Überlegungen erleichtern Ihnen die ersten Schritte.

Einfachheit. Einfach ist nicht immer unkompliziert, aber unkompliziert ist immer einfach. Benutzersteuerelemente sind einfach und unkompliziert. Entwickler aller Kenntnisstufen können sie bereitstellen, ohne sich zuvor viel mit Dokumentation beschäftigen zu müssen.

Entwurfserlebnis. Viele Entwickler sind vom XAML-Designer begeistert. Steuerelementlayouts mit Vorlagen können im Designer erstellt werden, aber es ist das Benutzersteuerelement, das die Entwicklererfahrung zur Entwurfszeit bestimmt.

API-Oberfläche. Beim Erstellen einer intuitiven API-Oberfläche können Entwickler diese einfach nutzen. Benutzersteuerelemente unterstützen benutzerdefinierte Eigenschaften, Ereignisse und Methoden, aber Steuerelemente mit Vorlagen sind am flexibelsten.

Flexible visuelle Elemente. Durch die Bereitstellung einer großartigen Standardbenutzeroberfläche können Entwickler Steuerelemente ganz einfach nutzen. Aber flexible Steuerelemente unterstützen visuelle Elemente, für die ein Re-Templating ausgeführt wurde. Nur Steuerelemente mit Vorlagen unterstützen Re-Templating.

Zusammenfassend lässt sich sagen, dass Benutzersteuerelemente optimal hinsichtlich Einfachheit und Entwurfserlebnis sind, während Steuerelemente mit Vorlagen Ihnen die beste API-Oberfläche und die flexibelsten visuellen Elemente bieten.

Wenn Sie sich entscheiden, mit einem Benutzersteuerelement zu beginnen und zu einem Steuerelement mit Vorlagen zu migrieren, haben Sie einiges an Arbeit vor sich. Aber es ist machbar. Dieser Artikel beginnt mit einem Benutzersteuerelement und geht dann zu einem Steuerelement mit Vorlagen über. Es ist wichtig zu wissen, dass viele wiederverwendbare Layouts nur Benutzersteuerelemente erfordern. Wenn Sie eine Branchenlösung öffnen, ist es auch angemessen, wenn Sie sowohl Benutzersteuerelemente als auch Steuerelemente mit Vorlagen vorfinden.

Neue Benutzer Ihrer App

Sie müssen neue Benutzer veranlassen, dem Endbenutzer-Lizenzvertrag (EULA) in Ihrer neuen Anwendung zuzustimmen. Wie wir alle wissen, möchte kein Benutzer einem Endbenutzer-Lizenzvertrag zustimmen. Dennoch muss die gesetzliche Vorgabe erfüllt werden, dass Benutzer „Ich stimme zu“ aktivieren, bevor sie fortfahren. Selbst wenn der Endbenutzer-Lizenzvertrag verwirrend sein sollte, stellen Sie trotzdem sicher, dass die XAML-Benutzeroberfläche strukturiert und intuitiv ist. Sie beginnen mit dem Prototy: Sie fügen ein TextBlock-, ein CheckBox- und ein Button-Element hinzu (wie in Abbildung 1 gezeigt), und dann beginnen Sie, nachzudenken.

Prototyp der Benutzeroberfläche
Abbildung 1: Prototyp der Benutzeroberfläche

Das Erstellen von Prototypen im XAML-Designer geht schnell. Und es ist einfach, weil Sie sich Zeit genommen haben, die Tools kennenzulernen. Aber wie sieht es mit anderen Formularen in Ihrer Anwendung aus? Möglicherweise benötigen Sie diese Funktionalität auch an anderer Stelle. Kapselung ist ein Entwurfsmuster, mit dem komplexe Logik vor dem Consumer verborgen wird. DRY (Don’t Repeat Yourself, wiederhole dich nicht) ist ein weiteres Entwurfsmuster, das sich auf die Wiederverwendung von Code bezieht.  XAML bietet beide Entwurfsmuster über Benutzersteuerelemente und benutzerdefinierte Steuerelemente. Als XAML-Entwickler wissen Sie, dass benutzerdefinierte Steuerelemente leistungsfähiger sind als Benutzersteuerelemente, aber sie sind nicht so einfach wie diese. Sie entscheiden sich dafür, mit einem Benutzersteuerelement zu beginnen. Vielleicht kann es die gewünschte Aufgabe erledigen. Spoiler-Alarm: Das kann es nicht.

Benutzersteuerelemente

Benutzersteuerelemente sind einfach. Sie bieten konsistente, wiederverwendbare Schnittstellen und benutzerdefinierte, gekapselte CodeBehind-Logik. Um ein Benutzersteuerelement zu erstellen, wählen Sie im Dialogfeld „Neues Element hinzufügen“ die Option „Benutzersteuerelement“ aus, wie in Abbildung 2 gezeigt.

Dialogfeld „Neues Element hinzufügen“
Abbildung 2: Dialogfeld „Neues Element hinzufügen“

Benutzersteuerelemente sind in der Regel ein untergeordnetes Element eines anderen Steuerelements. Ihr Lebenszyklus ist jedoch dem von Fenstern und Seiten so ähnlich, dass ein Benutzersteuerelement der Wert sein kann, der auf die Window.Current.Content-Eigenschaft festgelegt wurde. Benutzersteuerelemente sind voll funktionsfähig und unterstützen visuelle Zustandsverwaltung, interne Ressourcen und jedes andere Merkmal des XAML-Frameworks. Ihre Erstellung ist kein Kompromiss aus der verfügbaren Funktionalität. Mein Ziel ist es, sie auf einer Seite wiederzuverwenden und dabei ihre Unterstützung für visuelle Zustandsverwaltung, Ressourcen, Formatvorlagen und Datenbindung zu nutzen. Ihre XAML-Implementierung ist einfach und designerfreundlich:

<UserControl>
  <StackPanel Padding="20">
    <TextBlock>Lorem ipsum.</TextBlock>
    <CheckBox>I agree!</CheckBox>
    <Button>Submit</Button>
  </StackPanel>
</UserControl>

Dieses XAML rendert meinen früheren Prototyp und zeigt, wie einfach ein Benutzersteuerelement sein kann. Natürlich gibt es noch kein benutzerdefiniertes Verhalten, sondern nur das integrierte Verhalten der von mir deklarierten Steuerelemente.

Fast Path für Text. EULAs sind lang, beschäftigen wir uns also mit der Textleistung. Der TextBlock (und nur der TextBlock) wurde optimiert, um Fast Path, geringen Speicherbedarf und CPU-Rendering zu nutzen. Er wurde für Schnelligkeit konzipiert – aber das kann ich zunichte machen:

<TextBlock Text="Optimized" />
<TextBlock>Not optimized</TextBlock>

Die Verwendung von TextBlock-Inlinesteuerelementen wie <Run/> und <LineBreak/> zerstört die Optimierung. Eigenschaften wie CharacterSpacing, LineStackingStrategy und TextTrimming können dies ebenfalls. Lassen Sie sich hierdurch nicht verwirren. Es gibt einen einfachen Test:

Application.Current.DebugSettings
  .IsTextPerformanceVisualizationEnabled = true;

IsTextPerformanceVisualizationEnabled ist eine wenig bekannte Debugeinstellung, mit der Sie sehen können, welcher Text in Ihrer Anwendung optimiert ist, während Sie debuggen. Wenn der Text nicht grün ist, ist es an der Zeit, eine Untersuchung durchzuführen.

Mit jedem Release von Windows wirken sich weniger Eigenschaften auf Fast Path aus, aber es gibt immer noch mehrere Eigenschaften, die die Leistung negativ und unerwartet beeinflussen. Mit ein wenig gezieltem Debuggen ist dies kein Problem.

Unveränderliche Regeln. Es gibt so viele Meinungen wie Optionen, wo sich Geschäftslogik befinden sollte. Eine allgemeine Regel positioniert weniger änderbare Regeln näher am Steuerelement. Dies ist im Allgemeinen schneller und einfacher und optimiert die Verwaltbarkeit.

Geschäftsregeln unterscheiden sich übrigens von der Datenüberprüfung. Die Reaktion auf die Dateneingabe und die Überprüfung auf Textlänge und numerische Bereiche ist einfach eine Überprüfung. Regeln bestimmen die Art des Benutzerverhaltens.

Eine Bank verwendet beispielsweise eine Geschäftsregel, die besagt, dass sie Kunden mit einer Kreditwürdigkeit unter einem bestimmten Wert keinen Kredit gewährt. Ein Klempner hat die Regel, nicht zu einem Kunden außerhalb einer bestimmten Postleitzahl zu fahren. Bei Regeln geht es um das Verhalten. In einigen Fällen ändern sich die Regeln jeden Tag, etwa welche Kreditwürdigkeit die Vergabe neuer Kredite beeinflusst. In anderen Fällen ändern sich die Regeln nie, z.B. wird ein Mechaniker niemals an einem Subaru arbeiten, der vor 2014 gebaut wurde.

Betrachten Sie nun diese Akzeptanzkriterien: Ein Benutzer kann erst auf das Button-Element klicken, wenn das CheckBox-Element mit einem Häkchen versehen wurde. Dies ist eine Regel, und sie ist so unveränderlich wie irgend möglich. Ich werde sie in der Nähe meiner Steuerelemente implementieren:

<StackPanel Padding="20">
  <TextBlock>Lorem ipsum.</TextBlock>
  <CheckBox x:Name="AgreeCheckBox">I agree!</CheckBox>
  <Button IsEnabled="{Binding Path=IsChecked,
    ElementName=AgreeCheckBox}">Submit1</Button>
  <Button IsEnabled="{x:Bind Path=AgreeCheckBox.IsChecked.Value,
    Mode=OneWay}">Submit2</Button>
</StackPanel>

In diesem Code erfüllt Datenbindung meine Anforderung perfekt. Die Schaltfläche „Submit1“ verwendet klassische WPF-Datenbindung (und klassische UWP-Datenbindung). Die Schaltfläche „Submit2“ verwendet moderne UWP-Datenbindung.

Beachten Sie in Abbildung 3, dass „Submit2“ aktiviert ist. Ist das richtig? Nun, in Visual Studio Designer hat klassische Datenbindung den Vorteil, dass das Rendering zur Entwurfszeit erfolgt. Kompilierte Datenbindung (x:Bind) erfolgt vorerst nur zur Laufzeit. Die Wahl zwischen klassischer und kompilierter Datenbindung ist die schwierigste einfache Entscheidung, die Sie treffen müssen. Einerseits ist kompilierte Bindung schnell. Aber andererseits ist klassische Bindung einfach. Kompilierte Bindung ist verfügbar, um das schwierige Leistungsproblem von XAML zu lösen: Datenbindung. Da klassische Bindung Laufzeitreflektion erfordert, ist sie von Natur aus langsamer und führt zu Schwierigkeiten bei der Skalierung.

Implementieren einer Geschäftsregel mit Datenbindung
Abbildung 3: Implementieren einer Geschäftsregel mit Datenbindung

Viele neue Funktionen wurden der klassischen Bindung hinzugefügt, z.B. die asynchrone Bindung, und es sind mehrere Muster entstanden, die Entwicklern helfen. Doch als UWP die Nachfolge von WPF antrat, zeigte sich das gleiche schleppende Problem. Hierüber sollten Sie einmal nachdenken: Die Möglichkeit, klassische Bindung in einem asynchronen Modus zu verwenden, wurde nicht aus WPF in UWP portiert. Interpretieren Sie da hinein, was Sie möchten, aber es ermutigt Unternehmensentwickler, in kompilierte Bindung zu investieren. Kompilierte Bindung nutzt den XAML-Codegenerator und erstellt dabei die CodeBehind-Logik automatisch und koppelt die Bindungsanweisungen mit den zur Laufzeit erwarteten echten Eigenschaften und Datentypen.

Aufgrund dieser Kopplung können nicht übereinstimmende Typen Fehler verursachen, ebenso wie der Versuch, eine Bindung mit anonymen Objekten oder dynamischen JSON-Objekten herzustellen. Diese Grenzfälle werden von vielen Entwicklern berücksichtigt, sind aber nun verschwunden:

  • Kompilierte Bindung löst die Leistungsprobleme der Datenbindung unter Einführung bestimmter Einschränkungen.
  • Abwärtskompatibilität gewährleistet die Unterstützung klassischer Bindung, während für UWP-Entwickler eine bessere Option bereitgestellt wird.
  • Innovationen und Verbesserungen in der Datenbindung werden in kompilierte Bindung investiert, nicht in klassische Bindung.
  • Features wie Funktionsbindung sind nur mit kompilierter Bindung verfügbar, bei der die Bindungsstrategie von Microsoft klar fokussiert ist.

Doch die Einfachheit und die Entwurfszeitunterstützung von klassischer Bindung hält das Argument am Leben und drängt das Microsoft-Team für Entwicklertools, kompilierte Bindung und die Entwicklererfahrung weiter zu verbessern. Beachten Sie, dass in diesem Artikel die Wahl der einen oder der anderen Option nahezu unermessliche Auswirkungen haben wird. Einige Beispiele zeigen klassische Bindung, während andere kompilierte Bindung verwenden. Es liegt an Ihnen, sich für eine Option zu entscheiden. Die Entscheidung ist natürlich in großen Apps am wichtigsten.

Benutzerdefinierte Ereignisse. Benutzerdefinierte Ereignisse können nicht in XAML deklariert werden, daher verarbeiten Sie sie in CodeBehind-Logik. Beispielsweise kann ich das Klickereignis der Schaltfläche „Submit“ (Senden) an ein benutzerdefiniertes Klickereignis für mein Benutzersteuerelement weiterleiten:

public event RoutedEventHandler Click;
public MyUserControl1()
{
  InitializeComponent();
  SubmitButton.Click += (s, e)
    => Click?.Invoke(this, e);
}

Hier löst der Code die benutzerdefinierten Ereignisse aus und leitet die RoutedEventArgs von der Schaltfläche weiter. Entwickler können diese Ereignisse deklarativ wie jedes andere Ereignis in XAML verarbeiten:

<controls:MyUserControl1 Click="MyUserControl1_Click" />

Der Vorteil dabei ist, dass Entwickler kein neues Paradigma erlernen müssen: Benutzerdefinierte Steuerelemente und standardmäßige Steuerelemente von Erstanbietern verhalten sich funktional gleich.

Benutzerdefinierte Eigenschaften. Damit Entwickler ihre eigenen EULAs bereitstellen können, kann ich das x:FieldModifier-Attribut für den TextBlock festlegen. Dadurch wird das XAML-Kompilierungsverhalten aus dem privaten Standardwert geändert:

<TextBlock x:Name="EulaTextBlock" x:FieldModifier="public" />

Aber „einfach“ bedeutet nicht gut. Diese Methode bietet wenig Abstraktion und erfordert, dass Entwickler die interne Struktur verstehen. Darüber hinaus ist CodeBehind-Logik erforderlich. Daher werde ich in diesem Fall die Verwendung des Attributansatzes vermeiden:

public string Text
{
  get => (string)GetValue(TextProperty);
  set => SetValue(TextProperty, value);
}
public static readonly DependencyProperty TextProperty =
  DependencyProperty.Register(nameof(Text), typeof(string),
    typeof(MyUserControl1), new PropertyMetadata(string.Empty));

Ebenso einfach (und ohne die Einschränkungen) ist eine Abhängigkeitseigenschaft, die an die Text-Eigenschaft des TextBlock-Elements datengebunden ist. Dies ermöglicht es dem Entwickler, die benutzerdefinierte Text-Eigenschaft zu lesen, zu schreiben oder zu binden:

<StackPanel Padding="20">
  <TextBlock Text="{x:Bind Text, Mode=OneWay}" />
  <CheckBox>I agree!</CheckBox>
  <Button>Submit</Button>
</StackPanel>

Die Abhängigkeitseigenschaft ist erforderlich, um Datenbindung zu unterstützen. Robuste Steuerelemente unterstützen grundlegende Anwendungsfälle wie Datenbindung. Außerdem fügt die Abhängigkeitseigenschaft meiner Codebasis nur eine einzige Zeile hinzu:

<TextBox Text="{x:Bind Text, Mode=TwoWay,
  UpdateSourceTrigger=PropertyChanged}" />

Bidirektionale Datenbindung für benutzerdefinierte Eigenschaften in Benutzersteuerelementen wird ohne INotifyPropertyChanged unterstützt. Dies liegt daran, dass Abhängigkeitseigenschaften interne Änderungsereignisse auslösen, die das bindende Framework überwacht. Es ist seine eigene Form von INotifyPropertyChanged.

Der vorhergehende Code erinnert uns daran, dass UpdateSourceTrigger bestimmt, wann Änderungen registriert werden. Mögliche Werte sind Explicit, LostFocus und PropertyChanged. Das letztgenannte Ereignis tritt auf, wenn Änderungen vorgenommen werden.

Hindernisse. Der Entwickler möchte möglicherweise die Inhaltseigenschaft des Benutzersteuerelements festlegen. Dies ist ein intuitiver Ansatz, wird aber von Benutzersteuerelementen nicht unterstützt. Die Eigenschaft ist bereits auf das deklarierte XAML festgelegt:

<Controls:MyUserControl>
  Lorem Ipsum
</Controls:MyUserControl>

Diese Syntax überschreibt die Inhaltseigenschaft: TextBlock, CheckBox und Button. Wenn ich erwäge, Re-Templating für einen einfachen XAML-Anwendungsfall auszuführen, liefert mein Benutzersteuerelement kein vollständiges, robustes Erlebnis. Benutzersteuerelemente sind einfach, bieten aber wenig Kontrolle oder Erweiterbarkeit. Eine intuitive Syntax und Unterstützung für Re-Templating sind Teil einer allgemeinen Erfahrung. Sehen wir uns nun Steuerelemente mit Vorlagen an.

Steuerelemente mit Vorlagen

Für XAML wurden massive Verbesserungen in Bezug auf Speicherverbrauch, Leistung, Barrierefreiheit und visuelle Konsistenz vorgenommen. Entwickler schätzen XAML aufgrund der Flexibilität. Steuerelemente mit Vorlagen sind dafür ein typisches Beispiel.

Steuerelemente mit Vorlagen können etwas völlig Neues definieren, sind aber in der Regel eine Kombination aus mehreren vorhandenen Steuerelementen. Das hier gezeigte Beispiel mit einem TextBlock-, CheckBox- und Button-Element bildet zusammen ein klassisches Szenario.

Verwechseln Sie übrigens nicht Steuerelemente mit Vorlagen mit benutzerdefinierten Steuerelementen. Ein Steuerelement mit Vorlagen ist ein benutzerdefiniertes Layout. Ein benutzerdefiniertes Steuerelement ist einfach eine Klasse, die ein vorhandenes Steuerelement ohne jede benutzerdefinierte Formatierung erbt. Wenn alles, was Sie brauchen, eine zusätzliche Methode oder Eigenschaft für ein vorhandenes Steuerelement ist, sind benutzerdefinierte Steuerelemente manchmal eine gute Option. Ihre visuellen Elemente und ihre Logik sind bereits vorhanden, und Sie erweitern sie einfach.

Steuerelementvorlage. Das Layout eines Steuerelements wird durch ein ControlTemplate-Element definiert. Diese spezielle Ressource wird zur Laufzeit angewendet. Jedes Feld und jede Schaltfläche befindet sich im ControlTemplate-Element. Auf die Vorlage eines Steuerelements kann einfach durch die Template-Eigenschaft zugegriffen werden. Diese Template-Eigenschaft ist nicht schreibgeschützt. Entwickler können sie auf ein benutzerdefiniertes ControlTemplate-Element festlegen und die visuellen Elemente und das Verhalten eines Steuerelements an ihre speziellen Bedürfnisse anpassen. Darin liegt die Leistungsfähigkeit von Re-Templating:

<ControlTemplate>
  <StackPanel Padding="20">
    <ContentControl Content="{TemplateBinding Content}" />
    <CheckBox>I agree!</CheckBox>
    <Button>Submit1</Button>
  </StackPanel>
</ControlTemplate>

ControlTemplate-XAML sieht wie jede andere Layoutdeklaration aus. Beachten Sie im vorherigen Code die spezielle TemplateBinding-Markuperweiterung. Diese spezielle Bindung ist für unidirektionale Vorlagenvorgänge optimiert. Seit Windows 10, Version 1809, wird die x:Bind-Syntax in ControlTemplate-Definitionen von UWP unterstützt. Dies ermöglicht leistungsfähige, kompilierte, bidirektionale Bindungen und Funktionsbindungen in Vorlagen. TemplateBinding funktioniert in den meisten Fällen hervorragend.

Generic.XAML. Um ein Steuerelement mit Vorlagen zu erstellen, wählen Sie im Dialogfeld „Neues Element hinzufügen“ die Option „Steuerelement mit Vorlagen“ aus. Dies führt zu drei Dateien: der XAML-Datei, der CodeBehind-Datei und „themes/generic.xaml“, in der das ControlTemplate-Element enthalten ist. Die Datei „themes/generic.xaml“ ist identisch mit WPF. Sie ist ein Sonderfall. Das Framework mergt sie automatisch in die Ressourcen Ihrer App. Ressourcen, die hier definiert werden, beziehen sich auf die Anwendungsebene:

<Style TargetType="controls:MyControl">
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="controls:MyControl" />
    </Setter.Value>
  </Setter>
</Style>

ControlTemplates werden mit impliziten Formatvorlagen angewendet: Formatvorlagen ohne Schlüssel. Explizite Formatvorlagen besitzen einen Schlüssel, mit dem sie auf Steuerelemente angewendet werden können. Implizite Formatvorlagen werden basierend auf TargetType angewendet. Daher müssen Sie DefaultStyleKey festlegen:

public sealed class MyControl : Control
{
  public MyControl() => DefaultStyleKey = typeof(MyControl);
}

Dieser Code legt DefaultStyleKey fest und bestimmt, welche Formatvorlage implizit auf Ihr Steuerelement angewendet wird. Ich lege das Element auf den entsprechenden Wert des TargetType in ControlTemplate fest:

<ControlTemplate TargetType="controls:MyControl">
  <StackPanel Padding="20">
    <TextBlock Text="{TemplateBinding Text}" />
    <CheckBox Name="AgreeCheckbox">I agree!</CheckBox>
    <Button IsEnabled="{Binding IsChecked,
      ElementName=AgreeCheckbox}">Submit</Button>
  </StackPanel>
</ControlTemplate>

TemplateBinding bindet die Text-Eigenschaft des TextBlocks an die benutzerdefinierte Abhängigkeitseigenschaft, die aus dem Benutzersteuerelement in das Steuerelement mit Vorlagen kopiert wurde. TemplateBinding ist unidirektional, sehr effizient und in der Regel die beste Option.

Abbildung 4 zeigt das Ergebnis meiner Arbeit im Designer. Das in ControlTemplate deklarierte benutzerdefinierte Layout wird auf mein benutzerdefiniertes Steuerelement angewendet, und die Bindung wird zur Entwurfszeit ausgeführt und gerendert:

 

<controls:MyControl Text="My outside value." />

Transparente Vorschau mithilfe von internen Eigenschaften
Abbildung 4: Transparente Vorschau mithilfe von internen Eigenschaften

Die Syntax zum Verwenden meines benutzerdefinierten Steuerelements ist einfach. Ich verbessere sie noch, indem ich Entwicklern die Verwendung von Inlinetext erlaube. Dies ist die intuitivste Syntax, um den Inhalt eines Elements festzulegen. XAML stellt ein Klassenattribut zur Verfügung, das mich dabei unterstützt, wie Abbildung 5 zeigt.

Abbildung 5: Verwenden das Klassenattributs zum Festlegen der Content-Eigenschaft

[ContentProperty(Name = "Text")]
public sealed class MyControl : Control
{
  public MyControl() => DefaultStyleKey = typeof(MyControl);
  public string Text
  {
    get => (string)GetValue(TextProperty);
    set => SetValue(TextProperty, value);
  }
  public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register(nameof(Text), typeof(string),
      typeof(MyControl), new PropertyMetadata(default(string)));
}

Beachten Sie das ContentProperty-Attribut, das aus dem Windows.UI.Xaml.Markup-Namespace stammt. Es gibt an, in welche Eigenschaft direkte, in XAML deklarierte Inlineinhalte geschrieben werden sollen. Also kann ich jetzt meinen Inhalt folgendermaßen deklarieren:

<controls:MyControl>
  My inline value!
</controls:MyControl>

Das ist fantastisch. Steuerelemente mit Vorlagen geben Ihnen die Flexibilität, Steuerelementinteraktionen und Syntax so zu gestalten, wie es Ihnen am intuitivsten erscheint. Abbildung 6 zeigt das Ergebnis der Einführung von ContentProperty in das Steuerelement.

Entwurfsvorschau
Abbildung 6: Entwurfsvorschau

ContentControl. Wo ich zuvor eine benutzerdefinierte Eigenschaft erstellen und diese Eigenschaft dem Inhalt des Steuerelements zuordnen musste, stellt XAML ein bereits dafür entwickeltes Steuerelement zur Verfügung. Es trägt den Namen ContentControl, und seine Eigenschaft heißt Content. ContentControl stellt auch die Eigenschaften ContentTemplate und ContentTransition zur Verfügung, um Visualisierungen und Übergänge zu verwalten. Button, CheckBox, Frame und viele XAML-Standardelemente erben ContentControl. Dies wäre auch für meinen Code möglich, nur verwende ich Content anstelle von Text:

public sealed class MyControl2 : ContentControl
{
  // Empty
}

Beachten Sie in diesem Code die knappe Syntax, um ein benutzerdefiniertes Steuerelement mit einer Content-Eigenschaft zu erstellen. ContentControl wird automatisch als ContentPresenter gerendert, wenn das Element deklariert wird. Es ist eine schnelle und einfache Lösung. Es gibt jedoch eine Einschränkung: ContentControl unterstützt keine literalen Zeichenfolgen in XAML. Da es gegen mein Ziel verstößt, dass das Steuerelement literale Zeichenfolgen unterstützt, werde ich bei Control bleiben und ContentControl ein anderes Mal berücksichtigen.

Zugriff auf interne Steuerelemente. Die x:Name-Anweisung deklariert den Feldnamen, der in CodeBehind-Logik automatisch generiert wird. Weisen Sie einem Kontrollkästchen einen x:Name von MyCheckBox zu, und der Generator erstellt ein Feld namens MyCheckBox in Ihrer Klasse.

Im Gegensatz dazu erstellt x:Key (nur für Ressourcen) kein Feld. Das Element fügt einem Wörterbuch nicht aufgelöster Typen Ressourcen hinzu, bis diese zum ersten Mal verwendet werden. Für Ressourcen bietet x:Key Leistungsverbesserungen.

Da x:Name ein Unterstützungsfeld erstellt, kann das Element nicht in einem ControlTemplate-Element verwendet werden. Vorlagen sind von allen Unterstützungsklassen entkoppelt. Stattdessen verwenden Sie die Name-Eigenschaft.

Name ist eine Abhängigkeitseigenschaft für FrameworkElement, ein Vorgängerelement von Control. Sie verwenden Name, wenn Sie x:Name nicht verwenden können. Name und x:Name schließen sich in einem bestimmten Bereich aufgrund von FindName gegenseitig aus.

FindName ist eine XAML-Methode zum Auffinden von Objekten nach Namen. Sie funktioniert mit Name oder x:Name. Sie ist in der CodeBehind-Logik zuverlässig, aber nicht in Steuerelementen mit Vorlagen, in denen Sie GetTemplateChild verwenden müssen:

protected override void OnApplyTemplate()
{
  if (GetTemplateChild("AgreeCheckbox") is CheckBox c)
  {
    c.Content = "I really agree!";
  }
}

GetTemplateChild ist eine Hilfsmethode, die in der OnApplyTemplate-Überschreibung verwendet wird, um Steuerelemente zu finden, die von einem ControlTemplate-Element erstellt wurden. Verwenden Sie sie, um nach Verweisen auf interne Steuerelemente zu suchen.

Ändern der Vorlage. Das Re-Templating des Steuerelements ist einfach, aber ich habe die Klasse so erstellt, dass sie Steuerelemente mit bestimmten Namen erwartet. Ich muss sicherstellen, dass die neue Vorlage diese Abhängigkeit beibehält. Erstellen wir ein neues ControlTemplate-Element:

<ControlTemplate
  TargetType="controls:MyControl"
  x:Key="MyNewStyle">

Kleine Änderungen an einem ControlTemplate-Element sind normal. Sie müssen nicht von Grund auf neu beginnen. Klicken Sie im Bereich „Dokumentgliederung“ von Visual Studio mit der rechten Maustaste auf ein beliebiges Steuerelement, und extrahieren Sie eine Kopie seiner aktuellen Vorlage (siehe Abbildung7).

Extrahieren einer Steuerelementvorlage
Abbildung 7: Extrahieren einer Steuerelementvorlage

Wenn ich seine Abhängigkeiten beibehalte, kann mein neues ControlTemplate-Elemente die visuellen Elemente und das Verhalten eines Steuerelements vollständig verändern. Wenn Sie eine explizite Formatvorlage für das Steuerelement deklarieren, wird das Framework angewiesen, die standardmäßige implizite Formatvorlage zu ignorieren:

<controls:MyControl Style="{StaticResource MyControlNewStyle}">

Doch dieses Umschreiben von ControlTemplate ist mit einer Warnung verbunden. Steuerelementdesigner und Entwickler müssen darauf achten, dass sie Barrierefreiheits- und Lokalisierungsfunktionen unterstützen. Es kann leicht geschehen, dass diese versehentlich entfernt werden.

TemplatePartAttribute. Es wäre praktisch, wenn ein benutzerdefiniertes Steuerelement die benannten Elemente kommunizieren könnte, die es erwartet. Einige benannte Elemente können nur in Grenzfällen benötigt werden. In WPF steht TemplatePartAttribute zur Verfügung:

[TemplatePart (
  Name = "EulaTextBlock",
  Type = typeof(TextBlock))]
public sealed class MyControl : Control { }

Diese Syntax zeigt, wie das Steuerelement interne Abhängigkeiten an externe Entwickler kommunizieren kann. In diesem Fall erwarte ich einen TextBlock mit dem Namen EulaTextBlock in meinem ControlTemplate-Element. Ich kann auch die visuellen Zustände angeben, die ich in meinem benutzerdefinierten Steuerelement erwarte:

[TemplateVisualState(
  GroupName = "Visual",
  Name = "Mouseover")]
public sealed class MyControl : Control { }

TemplatePart wird von Blend mit TemplateVisualState verwendet, um Entwickler beim Erstellen benutzerdefinierter Vorlagen gemäß den Erwartungen zu leiten. Ein ControlTemplate-Element kann anhand dieser Attributionen überprüft werden. Seit 10240 sind diese Attribute in WinRT enthalten. UWP kann sie verwenden, Blend für Visual Studio hingegen nicht. Dies bleibt eine gute, zukunftsorientierte Praxis, aber Dokumentation ist immer noch der beste Ansatz.

Barrierefreiheit. XAML-Steuerelemente von Erstanbietern werden sorgfältig entwickelt und getestet, um ansprechend, kompatibel und barrierefrei zu sein. Barrierefreiheitsanforderungen sind heute ausgesprochen wichtig und stellen Releaseanforderungen für jedes Steuerelement dar.

Wenn Sie für ein Steuerelemente eines Erstanbieters Re-Templating vornehmen, gefährden Sie die Barrierefreiheitsfunktionen, die von den Entwicklungsteams mit Bedacht hinzugefügt wurden. Es ist schwer, mit diesen richtig umzugehen, und leicht, Fehler zu machen. Wenn Sie sich für das Re-Templating eines Steuerelements entscheiden, sollten Sie sich mit den Barrierefreiheitsfunktionen des Frameworks und den Techniken für deren Implementierung bestens auskennen. Andernfalls verlieren Sie einen erheblichen Teil des Werts dieser Funktionen.

Die Hinzufügung von Barrierefreiheit als Releasevoraussetzung hilft nicht nur Menschen mit dauerhaften Beeinträchtigungen, sondern auch solchen, die vorübergehend eingeschränkt sind. Es reduziert auch Risiken, wenn Sie ein Re-Templating von Steuerelementen von Erstanbietern ausführen.

Sie haben es geschafft!

Nach dem Umstieg von einem Benutzersteuerelement auf ein Steuerelement mit Vorlagen habe ich nur sehr wenig neuen Code verwendet. Aber ich habe zahlreiche Funktionen hinzugefügt. Sehen wir uns an, was wir alles erreicht haben.

Kapselung. Das Steuerelement ist eine Sammlung von mehreren Steuerelementen, die mit benutzerdefinierten visuellen Elementen und Verhaltensweisen kombiniert sind, die von Entwicklern problemlos in einer Anwendung wiederverwendet werden können.

Geschäftslogik. Das Steuerelement enthält Geschäftsregeln, die die Akzeptanzkriterien für die User Story erfüllen. Ich habe unveränderliche Regeln in der Nähe des Steuerelements platziert und eine reichhaltige Benutzeroberfläche zur Entwurfszeit ebenfalls unterstützt.

Benutzerdefinierte Ereignisse. Das Steuerelement stellt steuerelementspezifische benutzerdefinierte Ereignisse wie „click“ bereit, die Entwicklern dabei helfen, mit dem Steuerelement zu interagieren, ohne dass sie die interne Struktur des Layouts verstehen müssen.

Benutzerdefinierte Eigenschaften. Das Steuerelement stellt Eigenschaften zur Verfügung, damit der Entwickler den Inhalt des Layouts beeinflussen kann. Dies ist auf eine Art und Weise geschehen, die die XAML-Datenbindung vollständig unterstützt.

API-Syntax. Das Steuerelement unterstützt einen intuitiven Ansatz, der es Entwicklern ermöglicht, ihre Inhalte mit literalen Zeichenfolgen und auf einfache Weise zu deklarieren. Ich habe zu diesem Zweck das ContentProperty-Attribut genutzt.

Vorlagen. Das Steuerelement enthält ein ControlTemplate-Standardelement, das eine intuitive Benutzeroberfläche bereitstellt. XAML-Re-Templating wird jedoch unterstützt, damit Entwickler die visuellen Elemente nach Bedarf anpassen können.

Es gibt noch mehr zu tun

Das Steuerelement benötigt noch einige Funktionen, aber nicht sehr viele – etwa ein wenig Aufmerksamkeit für das Layout (wie die Möglichkeit, umfangreichen Text zu scrollen) und einige Eigenschaften (wie den Inhalt des Kontrollkästchens). Ich bin fast am Ziel.

Steuerelemente können die Verwaltung des visuellen Zustands unterstützen, eine native Funktion von XAML, durch die sich Eigenschaften basierend auf Größenänderungs- oder Frameworkereignissen (wie Mouseover) ändern können. Ausgereifte Steuerelemente weisen visuelle Zustände auf.

Steuerelemente können Lokalisierung unterstützen. Die native Fähigkeit in UWP verwendet die x:Uid-Anweisung, die Steuerelemente RESW-Zeichenfolgen zuordnet, die durch das aktive Gebietsschema gefiltert werden. Ausgereifte Steuerelemente unterstützen Lokalisierung.

Steuerelemente können externe Formatvorlagendefinitionen unterstützen, um ihre visuellen Elemente zu aktualisieren, ohne eine neue Vorlage zu benötigen. Dies kann gemeinsam verwendete visuelle Elemente umfassen und Designs und BasedOn-Formatvorlagen nutzen. Ausgereifte Steuerelemente verwenden Formatvorlagen gemeinsam und unterstützen ihre Wiederverwendung.

Zusammenfassung

Das Erstellen eines Prototyps einer Benutzeroberfläche in XAML ist schnell erledigt. Benutzersteuerelemente erstellen einfache, wiederverwendbare Layouts auf einfache Weise. Steuerelemente mit Vorlagen erfordern etwas mehr Arbeit, um einfache, wiederverwendbare Layouts mit anspruchsvolleren Funktionen zu erstellen. Der Wahl des richtigen Ansatzes ist Aufgabe des Entwicklers und basiert auf ein wenig Wissen und viel Erfahrung. Experimentieren Sie. Je besser Sie die Tools kennen, desto produktiver werden Sie.

Windows Forms folgte im Jahr 2002 auf Visual Basic 6, WPF im Jahr 2006 auf Windows Forms. WPF führte XAML ein: eine deklarative Sprache für Benutzeroberflächen. Microsoft-Entwickler hatten noch nie so etwas wie XAML gesehen. Heute stellen Xamarin und UWP XAML für iOS, Android, HoloLens, Surface Hub, Xbox, IoT und den modernen Desktop bereit. Tatsächlich ist XAML heute die Technologie, auf der das Windows-Betriebssystem selbst basiert.

Entwickler auf der ganzen Welt schätzen XAML, weil es so produktiv und flexibel ist. Microsoft Engineers denken genauso. Wir entwickeln unsere eigenen Apps und sogar Windows 10 mit XAML. Die Zukunft ist vielversprechend, das Tool ist leistungsstark und die Technologie ist zugänglicher denn je.


Jerry Nixonist Senior Software Engineer und Lead Architect für Commercial Software Engineering bei Microsoft. Seit zwei Jahrzehnten entwickelt und entwirft er Software. Nixon ist Referent, Organisator, Lehrer und Autor, aber auch der Host von DevRadio. Die meiste Zeit verbringt er damit, seinen drei Töchtern Hintergrundgeschichten und Episodenhandlungen von Star Trek zu erklären.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Daniel Jacobson, Dmitry Lyalin, Daren May, Ricardo Minguez Pablos


Diesen Artikel im MSDN Magazine-Forum diskutieren