Mit AvaloniaUI Enums in XAML zeigen

Es geht um das GUI-Framework AvaloniaUI. Und es geht darum, wie man in der darin üblichen XAML-Beschreibung Enum-Werte in z.B. eine Combobox hineinbekommt – ohne extra Code.

Es gibt gewisse Kritik an diesem Vorgehen vor allem aus dem Kreise der AvaloniaUI-Kernprogrammierer. Man möchte lieber für alles und immer View-Modelle (MVVM) verwenden. Zweifellos, das ist gut. Aber es gibt auch Gründe, es anders herum zu machen. Zum Beispiel, wenn man einfach nur schnell etwas zusammenstecken möchte. Oder gerade eben kein Viewmodell will.

Zur Lösung: Man möchte z.B. diese GUI darstellen, wobei in der Combobox die Items aus einem eigenen oder bekannten Enum stammen sollen und selektierbar sein sollen. Hier: Dock aus dem DockPanel.

In dieser kleinen Demo wird die Combobox durch den Enum Dock gefüllt und im Weiteren dieser Selektionswert an die Dock-Eigenschaft des kastanienfarbenen Rechtecks gebunden. De XAML oder aXAML-Code dazu sieht so aus:

<UserControl xmlns="https://github.com/avaloniaui"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:adb="https://flinkebits.de/avadevbox"
             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
             x:Class="AvaloniaControls.Demo.RangeSliderDemo">
  <StackPanel>
    <TextBlock>Aufzählungswerte von Dock:</TextBlock>
    <ComboBox Name="cbdock" Width="240"
              HorizontalAlignment="Left"
              SelectedIndex="2" Margin="10,5,0,30"
              Items="{Binding Source={adb:EnumBindingSource {x:Type Dock}}}"/>

    <Border BorderThickness="3" BorderBrush="AliceBlue" Margin="10" Height="300">
      <DockPanel HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Rectangle DockPanel.Dock="{Binding #cbdock.SelectedItem}"
                   Width="55"
                   Height="44"
                   Fill="Maroon" />
        <Button IsEnabled="False" >Rest</Button>
      </DockPanel>
    </Border>
  </StackPanel>
</UserControl>

Früher in WPF hat man dazu nette Verrenkungen mit ObjectValueProvider gemacht. Doch den gibt es unter Avalonia nicht mehr. Stattdessen greife ich hier auf eine Markupextension zurück: EnumBindingSource. Allerdings gibt es Markupextensions in AvaloniaUI auch nicht so wirklich. Auf jeden Fall gibt es keine Ableitung von MarkupExtension. Das Problem für AvaloniaUI ist, dass man eigentlich gleichzeitig von AvaloniaObject und MarkupExtension erben wöllte, aber natrülichin C# nur eine Klasse beerbt werden kann!

Die Lösung ist, dass die XAML-Komponente von AvaloniaUI die Klasse MarkupExtension vollständig ignoriert und bei Verwendungen wie Markupextensions einfach nach Klassen sucht, die lediglich von AvaloniaObject abgeleitet sind und eine von mehreren möglichen public ProvideValue()-Signaturen hat. Avalonia lässt hier verschiedene Rückgabewerte zu. Konkrete und object, sowie verschiedene Parameter. Somit lässt sich diese Extension so schreiben:

public class EnumBindingSource : AvaloniaObject /*: MarkupExtension*/
{
    private Type _enumType;
    public Type EnumType
    {
        get { return this._enumType; }
        set
        {
            if (value != this._enumType)
            {
                if (null != value)
                {
                    Type enumType = Nullable.GetUnderlyingType(value) ?? value;

                    if (!enumType.IsEnum)
                        throw new ArgumentException("Type must be for an Enum.");
                }

                this._enumType = value;
            }
        }
    }

    public EnumBindingSource() { }

    public EnumBindingSource(Type enumType)
    {
        this.EnumType = enumType;
    }

    public Array ProvideValue(IServiceProvider serviceProvider)
    {
        if (null == this._enumType)
            throw new InvalidOperationException("The EnumType must be specified.");

        Type actualEnumType = Nullable.GetUnderlyingType(this._enumType) ?? this._enumType;
        Array enumValues = Enum.GetValues(actualEnumType);

        if (actualEnumType == this._enumType)
            return enumValues;

        Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
        enumValues.CopyTo(tempArray, 1);
        return tempArray;
    }
}

Das Ganze geht noch besser, denn man könnte auch noch die Attribute DescriptionAttribute auf den Aufzählungswerten auswerten und damit eine Lokalisierung anbieten. Das geht natürlich genau so, dass der SelectedValue vom Enum-Typ ist und die Anzeige in der Combobox der Description-Text ist.

Aber das überlasse ich einer Übung des Lesers. Es gibt genug WPF-Beispiele, die genau das tun.

WPF Dependency Properties von innen Setzen

Entwicklung eines WPF-Composite-Controls mit Dependency Properties (aka. Abhängkgeitseigenschaften)

Ab und zu muss man bei der Etnwicklung von WPF-Oberflächen neue Controls erstellen. Nun gibt es verschiedene Arten von Controls. Man unterscheided sog. Lookless Controls und Composite Controls. Erstere sind quasi rein nur eine von UIElement abgeleitete Datenstruktur. Das gesamte Verhalten, das Aussehen und die modifikation der Datenfelder (die Dependency Properties) geschieht über Styles und dort wiederum mit Triggern und Binungen. Darum soll es hier aber nicht gehen. Hier geht es um die andere Art von Control: Um Kompositum-Kontrollelemente bzw. User-Controls. Also in Etwa ein Panel oder ein Window. Mehrere Controls sind in einem neuen Control zusammengepfärcht und interagieren intern miteinander, während sie nach außen hin wie ein einziges auftreten. Dies lässt sich mit und ohne View-Model machen. Man hat also die Wahl zwischen MVVM und code behind. Je nach komplexität des Kontrollements ist es entweder sinnvoll oder einfach Overhead, ein View-Model dazu zu bauen. Hier soll es jetzt um ein einfaches Control gehen und daher greifen wir auf code behind zurück.

Das Control

Entwickelt wird ein Datei-Auswahl-Control. Es besteht aus einem Label und einem Button. Klickt man den Button, so erscheint ein Datei-Öffnen-Dialog und die fürderhin ausgewählte Datei wird angezeigt. Gleichzeitig hat auch die extern sichtbare Eigenschaft „FileName“ ihren Wert geändert und alle Bindungen  darauf ändern sich mit. Die Wahl fällt auf ein Control mit Code-Behind. Somit können wir einfach auf das Click-Ereignis des Buttons reagieren. Dort muss dan aktiv der Wert der Eigenschaft geändert werden. Tatsächlich ist hier par Bindung das Control sein eigenes View-Model.
Bestandteile:

Vorgehen

Zunächst benötigen wir ein Control.
Daher legen wir eine XAML-Datei an und passend dazu eine Code-Behind-Datei. Wir erben von System.Windows.Control.

 

und

 

Controlaufbau

Das Control besteht aus zwei weiteren Controls: Button und Label. Wir fügen beide ein und Binden das Label an die noch zu erstellende Eigenschaft FileName. Damit das nacher funktioniert, muss die Source noch korrekt sein. Wir erreichen das recht einfach, indem wir dem Conttol(!) den DataContext setzen und auf sich selbs verweisen lassen. Das Control ist
so gesehen sein eigenes View-Model.

 

Abhängigkeitseigenschaften

Zur erfolgreichen Bindung benötigt das Control noch eine Dependency Property FileName:

mah beachte, wie per FrameworPropertyMetadata ein Standardwert mitgegeben wurde und die Bindungsoptionen standardmäßgig auf TwoWay definiert wurden. Dies hat im Folgenden den Vorteil, dass man von Extern (bei Verwendung) nicht bei Binungen Mode=TwoWay angeben muss.

Events

Wir wollten es einfacher mit dem Button. Nun fehlt noch der Eventhandler:

Man beachte hier die beiden Aufrufe der von DependencyObject stammenden Methodena: SetCurrenValue und ClearValue. Damit wird der Wert bzw. die Bindung hinter einem Dependency-Property geänder bzw. auf den Std.-Wert (aus den Metadaten) zurückgesetztgesetzt. Verwendet man vergleichsweise dazu GetValue/SetValue wie in der Implementierung des CLR-Properties, zerstört man die Bindung. Das wäre fatal, da dann die Funktionalität zusammenbricht. An dieser Stelle sein noch kurz auf die Doku verwiesen… Demnach seien CLR-Getter/Setter nur so zu implementieren, wie hier gezeigt. Nur Aufrufe von GetValue/SetValue und keine weiteren Aktionen nebenher. Denn WPF ruft gerne selbst GetValue/SetValue mit passenden Parametern auf und umgeht dabei die CLR-Properties. Zusatzaktionen
muss man daher in passenden PropertyChanged– bzw. CoerceValue– oder ValidateValue-Callbacks machen. Auf selbige wurde hier verzichtet.

Eingebaut

Nun sehen wir uns noch an, wie dieses Control zu verwenden ist. Dazu wurde ein WPF-Fenster gestaltet, dass dieses Control verwendet und gleichzeitig einen Textblock an unsere neue Dependency-Property bindet:

 

… es ist kein Code-Behind nötig….
Man beachte, wie zunächst der text „aus window“ im Control steht, und später der Wert aus dem Eventhandler des Controls (siehe im Code: cancel oder OK-Zweig).