Der ultimative INotifyPropertyChanged Guide in 2024 – für C# & VB.NET

Änderungen einfach via PropertyChanged Basisklasse kommunizieren
Änderungen einfach via PropertyChanged Basisklasse kommunizieren

Datenbindungs-Änderungen einfach ans UI weiterleiten

Kennst Du auch das Problem, die „INotifyPropertyChanged“-Schnittstelle immer wieder implementieren zu müssen? Egal ob in VB.NET oder C#, jedes „ViewModel“ benötigt in guter alter „WPF MVVM“-Manier (leider) dieses implementierte Interface, um Änderungen der Daten an die Oberfläche bekanntzugeben. Nervig wird es dann nur, wenn man das Gefühl hat, endlose Monkey-Work zu schreiben, aber zum Glück gibt es die Vererbung, gell!?

💡 Du hast es eilig?: Kein Problem, navigiere einfach mithilfe des Inhaltsverzeichnisses an für Dich wichtige Punkte. Natürlich kannst Du Dich auch durch das Video berieseln lassen. Im Beitrag gehe ich allerdings mehr auf die Details und mehr Hintergründe ein – ggf. nimmst Du Ihn daher als Ergänzung.

Mithilfe von Vererbung werden wir uns heute ein kleines Helferlein, Welches einem die tägliche Arbeit im Bereich Datenbindungen vereinfachen kann, erstellen. In einem folgenden Beitrag werde ich hier auch noch einen Schritt weiter optimieren, das darfst Du auf keinen Fall verpassen – dort reduzieren wir alles auf eine einzige Zeile 😉. Ich meine, wer hat schon Bock alles 50x zu schreiben!?

Bevor wir damit jedoch starten, machen wir erst einmal eine Reise zurück, also zur Basis von INotifyPropertyChanged.

Zu viel Text? Schaue das Video!

Wenn Du keine Lust hast, diesen Beitrag in schriftlicher Form durchzuarbeiten, kannst Du Dir natürlich auch das Video zu Gemüte führen.

Was ist INotifyPropertyChanged?

Wie oben bereits erwähnt, gehen wir erst einmal einen Schritt zurück, bevor wir weiter nach vorn gehen. Man muss ja schließlich erstmal die Basics dahinter verstehen, bevor man dann auf Diesen aufbauen kann. Wie man zumindest schonmal an dem für .NET Schnittstellen typischen „I“-Prefix erkennen kann, handelt es sich bei „INotifyPropertyChanged“ um eine Schnittstelle (engl. Interface).

Exkurs Schnittstellen

Ohne zu tief in die Thematik Schnittstellen einzutauchen, könnte man sagen: „Schnittstellen definieren grobes Verhalten, Eigenschaften und auch Ereignisse, ohne Diese konkret zu implementieren“. Wenn man einen Menschen mit Interfaces beschreiben wollen würde, könnte man vereinfacht z. B. wie folgt beginnen:

  • „ICanWalk“
  • „IHaveBladder“
  • „ICanSpeak“

Damit würden wir genau das tun, was Schnittstellen ausmacht: Beschreiben, was das jeweilige Objekt können wird (sogar muss). Wir wissen nun, dass ein Objekt, Welches diese Schnittstellen implementieren würde, laufen, sprechen und auf Toilette gehen können muss (eine Blase hat..). Im nächsten Schritt, könnte ich mir also bei der Verwendung eines dieser Objekte sicher sein, dass es sprechen kann. Ich weiß zwar nicht wie dieses „Ding“ spricht, aber ich kann mir sicher sein, dass es dies anbietet.

Reine Abstraktion – Nichts Konkretes

Wo wir beim nächsten wichtigen Punkt wären: Wir wissen nicht, wie der oder diejenige spricht! Die eine Person könnte eventuell nuscheln oder ggf. stottern. Der nächste könnte zum Beispiel in jedem zweiten Satz „Hm..“ sagen – wobei mich das an meine Videos erinnert (Spaß beiseite).

Okay, ich meine, wir könnten dies natürlich auch anders designen- wir sind ja schließlich die Programmierer. Wir könnten z. B. das „ICanSpeak“ nochmal genauer konkretisieren – aber dies ist eine Design-, Erfahrungs- und Anforderungssache! Unser „ICanSpeak“ könnte dann z. B. „ISpeakStutterly“ o. ä. heißen. Somit würde vielleicht eine andere Person, die auf stotternde Menschen spezialisiert ist, schauen: „Okay, wer von allen Menschen die ich hier vorliegen habe, stottern? Wem kann ich mit meiner speziellen Expertise helfen?“.

Quasi unendlich Möglichkeiten

Wenn man dies nun weiter spinnt, könnte man auch über eine dritte Instanz nachdenken, die „ISpeakStutterly – Stotterer“ und „IHelpStutterer“ gezielt zusammenbringt. Aber ich denke das reicht an dieser Stelle, sonst tauchen wir viel zu tief in die Interface-Thematik ab.

Wofür ist INotifyPropertyChanged?

Nachdem wir oben nun eine kleine Erinnerung an die Thematik „Schnittstellen“ ins Gedächtnis gerufen haben, nun zum „INotifyPropertyChanged“-Interface selbst. Grundsätzlich ist die „INotifyPropertyChanged“-Schnittstelle dazu da, der Oberfläche signalisieren zu können: „Hey, hier hat sich etwas geändert, bitte reagiere entsprechend darauf!“. Dies kann man grob und einfach definieren, WPF guckt bei einer Datenbindung: „Okay, immer wenn xy sich ändert, muss ich die Oberfläche neu zeichnen, bzw. die Darstellung aktualisieren“.

Das Problem – keine Kristallkugel!

Da weder Du noch ich z. B. voraussagen können, was der jeweils Andere in Zukunft realisieren wird, wird Dir auffallen, dass auch das .NET-Team vor diesem Problem stand. Sie konnten weder wissen, dass ich eine Transportsoftware baue (und vor allem nicht, wie ich Diese objekt-logisch-technisch gestalten würde), noch, dass Du z. B. eine rein hypothetische Lern-Software baust.

Doch kein Problem?

Das Gute ist allerdings, dass das .NET-Team das auch gar nicht wissen, oder gar hellsehen musste. Alles was wir zusammenfassen können, könnten wir mit einem groben „Verhalten“ festhalten: „Wenn sich was ändert, muss gesagt werden, was sich geändert hat“ – mehr nicht. Genau dort greift letztendlich das „INotifyPropertyChanged“-Interface und füllt die Lücke!

Wie verwendet man INotifyPropertyChanged dann?

INotifyPropertyChanged - wie Bescheid geben
INotifyPropertyChanged – wie Bescheid geben?

Um dies zu erklären, packe ich mal wieder meine überaus heftigen (#ironieaus) Paint-Skills aus. Schaue Dir einmal die Parteien/Teilnehmer im Bild hier drüber an, dort habe ich es vereinfacht dargestellt.

Aus dem realen Leben

Stell Dir vor, Du bietest über einen bekannten Online-Marktplatz einen Artikel zum Verkauf an. Nun findet ein Nutzer Deinen Artikel und er kauft Diesen auch. Aber was nun? Du musst den Artikel natürlich verschicken und hierzu gibst Du die Sendungsverfolgung an – Du beobachtest also anschließend (mehr oder weniger), was der Versanddienstleister Dir mitteilt.

Auf unser Beispiel bezogen, bist Du jetzt praktisch die agierende und die zuhörende Partei in Einem – in der Praxis wäre dies getrennt, aber aktuell egal. Du bekommst von den modernen Marktplätzen das verdiente Geld heutzutage nur ausgezahlt, wenn der Versanddienstleister die Zustellung des Paketes bestätigt. Ohne den signalisierenden Dienstleister würde der Marktplatz also nie Wind davon bekommen, dass das Paket angekommen ist.

Nochmal auf MVVM bezogen

Wie oben erwähnt, vermischten sich zumindest in diesem Beispiel verschiedene Rollen. Daher werde ich dies nun auf das Model-View-ViewModel-Entwurfsmuster übertragen. Also das, was man gängigerweise in der Entwicklung mit WPF und XAML verwendet.

Ein konkretes MVVM Beispiel

Hierzu steigen wir nun in den Code ein und erstellen ein reelles Beispiel. Im allerersten Schritt kannst Du dafür einfach ein WPF-Projekt (egal ob C# oder VB.NET) aufsetzen. Wenn Du dies getan hast, kannst Du nun einen Ordner namens „ViewModels“ im Projekt anlegen.

Das MainViewModel erstellen

Erstelle im angelegten Ordner eine Datei namens „MainViewModel“ und befülle Sie mit dem gleichen folgenden Code. Beachte hier, dass wir nicht ganz MVVM-konform sind, da ich hier keine Art Container namens „ShellView“ erzeuge und auch das „MainWindow“ nicht wirklich umbenenne (z. B. in MainView). Ich möchte den Fokus hier wirklich nur auf das Notwendigste halten.

Namespace ViewModels

    Public Class MainViewModel

        Public Property FirstName As String

        Sub New()
            FirstName = "Robert"
        End Sub

    End Class

End Namespace
namespace ViewModels
{
    public class MainViewModel
    {
        public string FirstName { get; set; }

        public MainViewModel()
        {
            FirstName = "Robert";
        }
    }
}

Den DataContext des Views setzen

Wir arbeiten an dieser Stelle wirklich nur mit den Bordmitteln und verzichten daher auf Zusatz-Frameworks wie „Caliburn.Micro“. Daher müssen wir uns auch selbst darum kümmern, dass das View ein passendenes ViewModel gesetzt bekommt. Dies erledigen wir in diesem Beispiel (sonst nach Präferenz) in der „Code behind“-Datei. Öffne dazu einfach das „MainWindow“, sodass Du den XAML-Code siehst und drücke F7. Dies sollte dann der (einzige) Code im View-Code sein:

' HIER DEINEN PROJEKTNAMEN/BASE-NAMESPACE EINTRAGEN!
Imports <YOURPROJECTNAME>.ViewModels

Class MainWindow

    Sub New()
        InitializeComponent()
        DataContext = New MainViewModel()
    End Sub

End Class
using <YOURPROJECTNAME>.ViewModels;

class MainWindow
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext = new MainViewModel();
    }
}

Eine erste Darstellung

Um nun die Eigenschaft namens „FirstName“ aus dem „MainViewModel“ tatsächlich im „MainWindow“ darstellen zu können, müssen wir einfach eine für WPF übliche Datenbindung verwenden. Erstelle im MainWindow also gerne einen Textblock und wende die Datenbindung wie folgt an. Beachte, dass Du in der Zeile mit dem „:local“ Deinen Projektnamen verwendest und das „YOURPROJECTNAME“ ersetzt!

<Window x:Class="MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        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:local="clr-namespace:YOURPROJECTNAME"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" WindowStartupLocation="CenterScreen">
    <Grid>

        <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
            <TextBlock Text="{Binding FirstName}" />
        </StackPanel>

    </Grid>
</Window>

Das daraus resultierende Ergebnis kannst Du hier sehen (ich weiß, noch nicht wirklich interessant, aber hier geht es ja nicht um’s Design):

WPF Datenbindung von ViewModel zu View - INotifyPropertyChanged
WPF Datenbindung von ViewModel zu View – INotifyPropertyChanged

Was, wenn sich etwas ändert?

Nachdem Du im letzten Schritt das Basic-MVVM-Beispiel erstellt hast, können wir uns nun weiter an das „INotifyPropertyChanged“-Interface selbst herantasten. Dazu zeige ich Dir eine kleine Demonstration, Welche alles noch weiter verdeutlichen wird. Außerdem ändern wir im nächsten Schritt den Inhalt der „FirstName“-Eigenschaft im ViewModel.

Lass‘ uns nun einmal folgende Methode im MainViewModel definieren, beachte jedoch, dass ich dies hier nur als Demonstration des eigentlichen Anliegens mache. Man sollte keine Async Subs/Voids verwenden, da man damit Probleme mit der sogenannten „Statemachine“, bzw. dem „Exception-Stack“ erzeugt – aber.. anderes Thema.

Natürlich müssen wir die Methode auch aufrufen, sonst passiert schließlich nicht viel – dies tue ich im Konstruktor. Ca. 2 Sekunden nach der Ausführung des Programmes sollte sich nun die „FirstName“-Eigenschaft im ViewModel ändern, tut Sie aber scheinbar nicht – doch, tut Sie wohl 😉! Setze gerne einen Haltepunkt, wenn Du mir nicht glaubst, die Zeile wird ausgeführt!

' .... MainViewModel

Sub New()
    FirstName = "Robert"
    ChangeAfterSeconds()
End Sub

Private Async Sub ChangeAfterSeconds()
    Await Task.Delay(2000)
    FirstName = "Hans"
End Sub

' ....
// .... MainViewModel

public MainViewModel()
{
    FirstName = "Robert";
    ChangeAfterSeconds();
}

private async void ChangeAfterSeconds()
{
    await Task.Delay(2000);
    FirstName = "Hans";
}

// ....

Warum wird die Änderung nicht angezeigt?

Nun – woah – nachdem wir nun einige Basics wie die groben Zwecke von Interfaces und vielen anderen Dingen wiederholt haben, kommen wir (endlich, sorry) zum Punkt! Das View kennt zwar nun das zu sich passende ViewModel – genauer gesagt, den gesetzten DataContext, Welcher eine Instanz des MainViewModels ist. Es bekommt allerdings ohne weitere Hilfe nichts von den Änderungen an einer jeweiligen Eigenschaft mit.

Dies lösen wir nun mit dem INotifyPropertyChanged Interface!

Eine INotifyPropertyChanged Implementierung

Wie Du im letzten Abschnitt gesehen hast, hat sich die „FirstName“-Eigenschaft trotz Ausführung der Zeile augenscheinlich nicht geändert. Wir haben aber durch z. B. Einzelschritt-Debugging feststellen können, dass Diese sich sehr wohl ändert, jedoch eine Benachrichtigung darüber an die UI fehlt. Diese werden wir nun mit einer „INotifyPropertyChanged“-Implementierung realisieren.

Das PropertyChanged-Ereignis

Schreibe dazu einfach unter den Namen Deiner Klasse „Implements INotifyPropertyChanged“ und bestätige die Importierung des „System.ComponentModel“-Namespaces. Dann sollte auch direkt das „PropertyChanged“-Ereignis eingebunden werden. Und ja, was soll ich sagen, dies ist es, worum es im Kern geht!

Nach Ausführung der Auto-Vervollständigung sieht unser MainViewModel nun ungefähr so aus:

Imports System.ComponentModel

Namespace ViewModels

    Public Class MainViewModel
        Implements INotifyPropertyChanged

        Public Property FirstName As String

        Sub New()
            FirstName = "Robert"
            ChangeAfterSeconds()
        End Sub

        Private Async Sub ChangeAfterSeconds()
            Await Task.Delay(2000)
            FirstName = "Hans"
        End Sub

        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    End Class

End Namespace
using System.Threading.Tasks;
using System.ComponentModel;

namespace ViewModels
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public string FirstName { get; set; }

        public DummyViewModel()
        {
            FirstName = "Robert";
            ChangeAfterSeconds();
        }

        private async void ChangeAfterSeconds()
        {
            await Task.Delay(2000);
            FirstName = "Hans";
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }
}

Aber dennoch passiert irgendwie nichts? Ich kann mir schon Deine Gedanken vorstellen: „Aber Robert, Du hast doch gesagt, dass das dann geht.. :/“. Geduld junger Padawan, es bringt natürlich nichts, das Ereignis nur zu deklarieren, wir müssen es ja schließlich auch zur passenden Zeit auslösen!

Das Ereignis auslösen

Um nun eine Änderung von „FirstName“ an die Datenbindung der grafischen Oberfläche zu kommunizieren, müssen wir das „PropertyChanged“-Ereignis mit entsprechenden Daten auslösen. Wir haben mit dem Ereignis die Möglichkeit mitzuteilen, welche Eigenschaft sich geändert hat. Um nun zu sagen, dass sich „FirstName“ geändert hat, können wir es an passender Stelle so machen:

' ... MainViewModel
FirstName = "Hans"
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(FirstName)))
' ...
// ... MainViewModel
FirstName = "Hans";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FirstName)));
// ...

💡Hinweis: Beachte, dass ich hier den NameOf-Ausdruck verwende, Dieser hilft mir mit Compiler-Unterstützung, die korrekten namen der Variablen/Eigenschaften zu verwenden. So kann ich nicht aus Versehen „FirstNamee“ schreiben, ohne das ich es merke!

Nun können wir die Änderung tatsächlich (endlich) im View sehen. Bedenke hierbei auch, dass Du diese Vorgehensweise für sämtliche Eigenschaften verwenden kann! Booleans, Strings, oder komplexe Objekte, Du sagst hier schließlich nur „xy hat sich geändert“ – wer das dann wie rendert (z. B. mit einem DataTemplate), ist eine andere Sache!

Eigenschaft geändert - INotifyPropertyChanged
Eigenschaft geändert – INotifyPropertyChanged

Verbesserung 1 – Zentralisierung

Sicherlich kannst Du Dir jetzt schon denken, dass es ziemlich anstrengend sein kann, wenn Du nun überall, wo Du die „FirstName“-Eigenschaft änderst, das Ereignis auslösen musst. Keine Sorge, musst Du nicht, Du kannst es schließlich durch eine manuell implementierte Eigenschaft (statt der Automatischen) zentralisieren. Wir können also folgendes formulieren: „Immer wenn die Eigenschaft gesetzt wird *hust, Setter, hust*, dann müssen wir PropertyChanged auslösen“.

💡Hinweis: In einem der nächsten Beiträge werde ich erklären, wie man auch dies wesentlich einfacher gestalten kann, allerdings bleibt einem ohne weitere Hilfe erstmal nichts Anderes übrig!

Dies könnte so aussehen:

' ... MainViewModel
Private _firstName As String

Public Property FirstName As String
    Get
        Return _firstName
    End Get
    Set (value as String)
        ' nur wenn sich wirklich was geändert hat..
        If _firstName = value Then
            Return
        End If
        ' .. sagen, dass sich was geändert hat!
        _firstName = value
        RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(FirstName)))
    End Set
End Property
' ...
// ... MainViewModel
    private string _firstName;

    public string FirstName
    {
        get
        {
            return _firstName;
        }
        set
        {
            // nur wenn sich wirklich was geändert hat..
            if (_firstName == value)
                return;
            // .. sagen, dass sich was geändert hat!
            _firstName = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FirstName)));
        }
    }
// ...

Nun kann die Eigenschaft ruhig aus verschiedenen Quellen gesetzt werden und trotzdem haben wir nur an einer zentralen Stelle die Auslösung des „PropertyChanged“-Ereignisses.

Verbesserung 2 – Vererbung der Funktionalität

Du kannst Dir nun sicherlich vorstellen, Welchen Aufwand wir haben, wenn wir (wie für die meisten Applikationen üblich) mehrere ViewModels haben. In jedem der ViewModels müssten wir das INotifyPropertyChanged implementieren und den Code immer und immer wieder schreiben – bäh!

PropertyChangedBase-Klasse

Deshalb kommen wir zu einer weiteren Verbesserung, wir vererben die erstellte Funktionalität einfach weiter und bauen uns sogar noch eine zentrale Methode zum Auslösen des „PropertyChanged“-Ereignisses. Erstelle also nun die folgende Klasse, in einem „Utils“ Unterordner Deines Projekts, von der Du dann anschließend in folgenden Projekten und ViewModels erben kannst. Beachte, dass Du Diese natürlich in den passenden Namespace packen solltest!

Imports System.ComponentModel
Imports System.Runtime.CompilerServices

Namespace Utils

    Public MustInherit Class PropertyChangedBase
        Implements INotifyPropertyChanged

        Protected Sub NotifyOfPropertyChange(<CallerMemberName> Optional propertyName As String = Nothing)
            RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
        End Sub

        Public Event PropertyChanged As PropertyChangedEventHandler Implements INotifyPropertyChanged.PropertyChanged

    End Class

End Namespace
using System.Runtime.CompilerServices;
using System.ComponentModel;

namespace Utils;

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    protected void NotifyOfPropertyChange([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

Die „NotifyOfPropertyChange“-Methode bietet uns hier neben dem einfachen Auslösen des Ereignisses auch noch weitere Vorteile. Wir verwenden das „CallerMemberName“-Attribut für die Erfassung des „Property“-Namens. Wir müssen dann nicht zwangsweise den Namen der Eigenschaft angeben, er erkennt die aufrufende Property automatisch. Nur wenn wir bei einer Änderung des Vornamens, z. B. die Änderung der darauf basierenden „FullName Readonly Property“ kommunizieren wollen würden, müssen wir den Namen explizit angeben.

Ein ViewModel mit der Basis-Klasse

Deine folgenden ViewModels könntest Du nun also wie folgt erstellen. Passe hierbei Dein View ggf. noch an, indem Du zwei weitere Textblöcke mit passenden Bindungen hinzufügst. Ich habe testweise 1 weitere Eigenschaften, namens „LastName“ und eine auf beiden vorherigen basierende Eigenschaft namens „FullName“ hinzugefügt.

Imports <YOURPROJECTNAME>.Utils

Namespace ViewModels

    Public Class NewMainViewModel
        Inherits PropertyChangedBase

        Private _firstName As String

        Public Property FirstName As String
            Get
                Return _firstName
            End Get
            Set(value As String)
                If _firstName = value Then
                    Return
                End If
                _firstName = value
                NotifyOfPropertyChange()
                NotifyOfPropertyChange(NameOf(FullName))
            End Set
        End Property

        Private _lastName As String

        Public Property LastName As String
            Get
                Return _lastName
            End Get
            Set(value As String)
                If _lastName = value Then
                    Return
                End If
                _lastName = value
                NotifyOfPropertyChange()
                NotifyOfPropertyChange(NameOf(FullName))
            End Set
        End Property

        Public ReadOnly Property FullName As String
            Get
                Return $"{FirstName} {LastName}"
            End Get
        End Property

        Sub New()
            FirstName = "Robert"
            LastName = "Skibbe"
            ChangeAfterSeconds()
        End Sub

        Private Async Sub ChangeAfterSeconds()
            Await Task.Delay(2000)
            FirstName = "Hans"
            LastName = "Mustermann"
        End Sub

    End Class

End Namespace
using <YOURPROJECTNAME>.Utils;

namespace ViewModels
{
    public class NewMainViewModel : PropertyChangedBase
    {
        private string _firstName;

        public string FirstName
        {
            get
            {
                return _firstName;
            }
            set
            {
                if (_firstName == value)
                    return;
                _firstName = value;
                NotifyOfPropertyChange();
                NotifyOfPropertyChange(nameof(FullName));
            }
        }

        private string _lastName;

        public string LastName
        {
            get
            {
                return _lastName;
            }
            set
            {
                if (_lastName == value)
                    return;
                _lastName = value;
                NotifyOfPropertyChange();
                NotifyOfPropertyChange(nameof(FullName));
            }
        }

        public string FullName
        {
            get
            {
                return $"{FirstName} {LastName}";
            }
        }

        public NewMainViewModel()
        {
            FirstName = "Robert";
            LastName = "Skibbe";
            ChangeAfterSeconds();
        }

        private async void ChangeAfterSeconds()
        {
            await Task.Delay(2000);
            FirstName = "Hans";
            LastName = "Mustermann";
        }
    }
}

Weiterführende Links

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert