Eingabevalidierung in Avalonia mit INotifyDataErrorInfo

In den letzten Wochen habe ich mich wieder stärker mit der GUI im Projekt MessageCommunicator [1] beschäftigt. Diese ist hier komplett in Avalonia implementiert und damit ist die Anwendung für Windows, Linux und MacOS verfügbar. Neben einigen Styling-Anpassungen (Wechsel Light/Dark-Theme) habe ich auch eine Datenvalidierung mithilfe der auch in WPF beliebten INotifyDataErrorInfo Schnittstelle integriert. Diese Schnittstelle arbeitet ähnlich wie INotifyPropertyChanged und ermöglicht, asynchron je gebundener Eigenschaft Fehler an die GUI zu melden.

Bevor es an die GUI geht, muss zunächst einmal das INotifyDataErrorInfo Interface implementiert werden. Hierzu habe ich eine eigene Basisklasse ValidatableViewModelBase erstellt. Diese erbt von PropertyChangedBase, wodurch das INotifyPropertyChanged Interface bereits implementiert ist. Die Klasse ValidatableViewModelBase erbt zusätzlich von INotifyDataErrorInfo und muss dazu folgende Member implementieren:

namespace System.ComponentModel
{
  public interface INotifyDataErrorInfo
  {
    bool HasErrors { get; }
    IEnumerable GetErrors(string propertyName);
    event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
  }
}

Soweit sind die Member der Schnittstelle sprechend gestaltet. Über die Methode GetErrors kann die GUI zu einer gebundenen Eigenschaft etwaige Fehler abrufen. Sollte die Eigenschaft HasErrors aber auf false stehen, so weiß die GUI, dass es keine Fehler im Objekt gibt. Das Ereignis ErrosChanged übermittelt schließlich Änderungen an der Fehlerliste, und zwar immer abhängig von der betroffenen Eigenschaft (vgl. INotifyPropertyChanged).

Für meine Implementierung der Schnittstelle habe ich im Hintergrund eine Dictionary, welche pro Eigenschaft eine Liste an Fehlern in Form von Strings führen kann. Fehler dürften hier aber auch durch andere Klassen als String abgebildet werden, um mehrere Informationen als nur den Fehlertext zu transportieren. Für String habe ich mich nur der Einfachheit halber entschieden und reicht für meine Applikation völlig aus. Nachfolgend der Quellcode der ValidatableViewModelBase Klasse.

public class ValidatableViewModelBase : PropertyChangedBase, INotifyDataErrorInfo
{
    private static readonly string[] NO_ERRORS = new string[0];

    private Dictionary<string, List<string>> _errorsByPropertyName = new Dictionary<string, List<string>>();

    /// <inheritdoc />
    public virtual IEnumerable GetErrors(string propertyName)
    {
        if (_errorsByPropertyName.TryGetValue(propertyName, out var errorList))
        {
            return errorList;
        }
        return NO_ERRORS;
    }

    /// <inheritdoc />
    public bool HasErrors => _errorsByPropertyName.Count > 0;

    /// <inheritdoc />
    public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;

    protected virtual void SetError(string propertyName, string error)
    {
        if (_errorsByPropertyName.TryGetValue(propertyName, out var errorList))
        {
            if (!errorList.Contains(error))
            {
                errorList.Add(error);
            }
        }
        else
        {
            _errorsByPropertyName.Add(propertyName, new List<string>{ error });
        }

        this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }

    protected virtual void RemoveErrors(string propertyName)
    {
        if (_errorsByPropertyName.ContainsKey(propertyName))
        {
            _errorsByPropertyName.Remove(propertyName);
            this.ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
        }
    }
}

Die erste Anwendung dieser Basisklasse ist auch direkt innerhalb des PropertyGrid, was ich beim Erstellen/Editieren der Profile verwende. Nachfolgender Screenshot zeigt etwa den Fehler, wenn ein Mussfeld nicht gefüllt wurde. Die Optik, also die rote Umrandung + Ausrufezeichen rechts kommt dabei bereits direkt durch Avalonia. Hier habe ich keine Anpassungen mehr gemacht.

Im nächsten Screenshot entsprechend ein anderer Fall, bei dem der Wert aus der GUI nicht ins Model übernommen werden kann. Bei der Eigenschaft Port handelt es sich um eine Zahl, eingegeben wurden hier aber auch einige Buchstaben. Exceptions bei der Konvertierung fängt Avalonia bereits selbst ab und stellt sie in der Oberfläche genauso dar, wie gemeldete Fehler aus dem INotifyDataErrorInfo Interface.

In Summe funktioniert das INotifyDataErrorInfo Interface mit Avalonia sehr gut. Die GUI reagiert, wie erwartet.

Verweise

  1. Repository von MessageCommunicator auf GitHub
    https://github.com/RolandKoenig/MessageCommunicator

Ebenfalls interessant

  1. Allgemeiner Artikel zu Avalonia als Cross-Plattform-UI-Framework
    https://www.rolandk.de/wp-posts/2020/07/cross-platform-gui-mit-c-und-avalonia/
  2. PropertyGrid mit Avalonia
    https://www.rolandk.de/wp-posts/2020/08/propertygrid-mit-avalonia/
  3. Custom Window Chrome mit Avalonia
    https://www.rolandk.de/wp-posts/2021/05/custom-window-chrome-mit-avalonia/
  4. Markdown-Dokumente mit Avalonia rendern
    https://www.rolandk.de/wp-posts/2021/08/markdown-dokumente-mit-avalonia-rendern/

Schreibe einen Kommentar

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.