Laden...

MVVM: Im Konstruktor des VM Daten asynchron laden

Letzter Beitrag vor einem Jahr 41 Posts 980 Views
MVVM: Im Konstruktor des VM Daten asynchron laden

Hallo!

Ich habe jetzt schon viel ausprobiert aber...

Ich möchte in einer MVVM-Applikation im VM alle Daten der Applikation (XML-Dateien) asynchron laden.

public VMDVDVerwaltung()  // Konstruktor
{
    ...
    WndOpacity = 1;            // Transparenz der View initalisieren
    LoadDaten();               // Alle Programm-Daten laden
}
/// <summary>
/// Alle Programm-Daten laden.
/// </summary>
private async void LoadDaten()
{
    WndOpacity = 0.5;                                   // Fenster-Transparenz auf 0.5 setzen
    bool erfolgreich = await System.Threading.Tasks.Task.Run(() => ProgrammDatenInitialisieren(DispatcherObject));        
    ...
    WndOpacity = 1;                                     // Fenster-Transparenz auf 1 setzen
}
private bool ProgrammDatenInitialisieren(System.Windows.Threading.Dispatcher DP)
{
    bool IsErfolgreich = true;                        // Indikator: Programm-Daten erfolgreich geladen?, initalisieren

    #region Programm-Konfiguration
    PK PK2 = __PK;                                   // Standard Programm-Konfiguration immer übernehmen
    PK2 = ar_Global.Serialisieren.DeSerializeObject<PK>(Path.Combine(AppData_Pfad, PK2.KonfigDateiName)); // PK aus XML laden
    ...
    DP.Invoke(new Action(() => PK = PK2));        // Der PK im VM die eingelesene PK (aus XML) übergeben
    return IsErfolgreich;
}

Bei der Übergabe der eingelesenen PK Eigenschaft:

DP.Invoke(new Action(() => PK = PK2));        // Der PK im VM die eingelesene PK (aus XML) übergeben

wird jetzt der Fehler: (ArgumentException)

DependencySource muss in denselben Thread wie DependencyObjekt erstellt werden.

geworfen.

Wie bekomme ich denn die asynchron eingelesenen Objekte des Daten-Threads ins ViewModel???

Oder muss ich das Daten-Einlesen vollkommen anders ausführen?

NICHT im Konstruktor 😉 Ist generell eine blöde Idee.

Am besten ist es, Du lädst aus der UI gesteuert die Daten, also z.B. durch einen Button-Klick (Command).
Du kannst es aber auch in einem UI-Event das ViewModel über den DataContext suchen und dort die LoadDataAsync-Methode aufrufen, das wäre auch noch MVVM konform.
Ich hatte das früher ganz gerne durch meine selbst gebaute Navigation gelöst, das ViewModel wird informiert, wenn es angezeigt wird und lädt dann die Daten. Der Aufbau verlangt aber eine gewisse Infrastruktur.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007!

Danke für deinen Hinweis!   Ich habe es auch schon mal über einen Timer versucht ...

Du kannst es aber auch in einem UI-Event das ViewModel über den DataContext suchen und dort die LoadDataAsync-Methode aufrufen, das wäre auch noch MVVM konform.

Ich habe sowieso einen Load-Behavior implementiert. Auch an dieser Stelle könnte ich die LoadData-Async-Methode aufrufen, aber das löst ja nicht mein Problem. Der Aufruf ist aber sauberer. Danke!

public class WindowLoadedBehavior : Behavior<MainWindow>
{
    protected override void OnAttached() => AssociatedObject.Loaded += AssociatedObject_Loaded;
    protected override void OnDetaching() => AssociatedObject.Loaded -= AssociatedObject_Loaded;

    private void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {

        MainWindow view = (MainWindow)sender;
        VMDVDVerwaltung vm = (VMDVDVerwaltung)((MainWindow)sender).DataContext;
        ...
        vm.LoadDaten();     // Programm-Daten laden 
    }
}

Lies dich ein, wie man richtig mit async/await/Tasks arbeitet, durch Trial&Error wirst Du nicht glücklich.

Und deinen bisherigen hier gezeigten Code musst Du weg werfen.

// Immer einen Task zurückgeben, außer Du weißt, was du tust
public async Task LoadDataAsync() // Das "Async" ist Konvention
{
    // Hier darfst Du normal auf Properties vom ViewModel zugreifen, hier bist Du noch im UI-Thread.
    
    _data = await LoadDataFromDatabaseAsync();
    
    // Hier darfst Du normal auf Properties vom ViewModel zugreifen, das "await" kümmert sich um die korrekte Thread-Synchronisation
}

Im Behavior:

// Für Event-Handler keinen Task zurückgeben - ist eine Ausnahme.
private async void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
    // ...
    await vm.LoadDataAsync();
}

Das await synchronisiert alles selbständig, Du brauchst dafür keinen Dispatcher.
Das funktioniert aber nur, wenn das await im UI-Thread stattfindet, daher muss das auch im Event passieren, weil die sind in der Regel im UI-Thread.
Der Inhalt der asynchronen Methode wird dann auf einem anderen Thread ausgeführt und nach dem Await wechselt die Methode wieder zum UI-Thread.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Das Laden der Daten sollte nicht durch den Konstruktor angestossen werden. Es ist wesentlich geschickter dieses erst dann zu erledigen, wenn dieses erforderlich ist (z.B. wenn die View wirklich angezeigt wird).

Meine ViewModels basieren alle auf:

public abstract class ViewModelBase : ReactiveObject
{
    [Reactive] public bool IsInitialized { get; private set; }
    public async Task InitializeAsync( CancellationToken cancellationToken = default )
    {
        IsInitialized = false;
        await OnInitializeAsync( cancellationToken );
        IsInitialized = true;
    }
    
    protected virtual Task OnInitializeAsync( CancellationToken cancellationToken ) => Task.CompletedTask;
}

Die konkreten ViewModels überschreiben die OnInitializeAsync-Methode wenn es irgendwelche Daten zu laden gibt.

Per DependencyInjection bekommt jede View (Window, Page) das ViewModel im Konstruktor übergeben und beim Erscheinen der View wird die Initialisierung des ViewModels angestossen.

public class MainWindow : Window
{
    public MainWindow( MainWindowViewModel viewModel )
    {
        InitializeComponent();
        DataContext = viewModel;
        Appearing += async ( s, e ) => await viewModel.InitializeAsync();
    }
}

Das funktioniert wie gesagt bei einem Window als auch bei einer Pageganz wunderbar.

Hat die Blume einen Knick, war der Schmetterling zu dick.

Hallo Palladin07!

Die Struktur ist schon einmal sehr hilfreich! Danke!!!

Für den Abruf der Daten von einer XML-Datei funktioniert das wunderbar.

Ich kämpfe jedoch noch mit der Synchronisation von meheren Abrufen.

public async Task LoadDataAsync()
{
    data1 = await LoadDataFromXML1Async();
    data2 = await LoadDataFromXML2Async(data1);
    data3 = await LoadDataFromXML2Async(data1);
    data4 = Vorgang1_BuildView(data1,data3);
}

Im Prinziep möchte ich, dass Als erstes data1 abgerufen wird, danach data2 oder data3 und am Schluß Vorgang1 ausgeführt wird.

Ich könnte noch im asynchronen data1-Abruf data2 und data3 (synchron) mit abrufen und das Ergebnis im LoadDataAsync Task den ViewModel-Eigenschaften zuordnen, aber wie setze ich Vorgang 1 an den Schluß?

public async Task LoadDataAsync()
{
    data13 = await LoadDataFromXML1...3Async();
	data1 = data13.1;
	data2 = data13.2;
	data3 = data13.3;
    data4 = Vorgang1_BuildView(data1,data3)
}

Hallo BlonderHans!

Die Initalisierung der Daten im Load-Behavior ist für mich erst einmal eine einfache Lösung.

Ich werde mir deine Lösung aber auch noch einmal in Ruhe ansehen, eine Basisklasse ist ja auch einfach immer wieder zu benutzen.

Vielen Dank!

Was Du da zeigst, ist genau richtig so.

public async Task LoadDataAsync()
{
    data1 = await LoadDataFromXML1Async();
    data2 = await LoadDataFromXML2Async(data1);
    data3 = await LoadDataFromXML2Async(data1);
    data4 = Vorgang1_BuildView(data1,data3);
}

Genau so macht man das - das komische BuildView mal ausgenommen 😉

Das BuildView am Ende passt deshalb nicht ins Bild, weil in WPF eigentlich keine View gebaut werden muss. Du baust stattdessen ViewModels und bindest die View daran, den Rest macht WPF automatisch. Wenn Du aber genau das meinst (also das Aufbauen von ViewModels auf Basis der Daten), dann ist das auch richtig so.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Zitat von perlfred

Im Prinziep möchte ich, dass Als erstes data1 abgerufen wird, danach data2 oder data3 und am Schluß Vorgang1 ausgeführt wird.

Task hat doch sowas wie .ContinueWith(). Schau dir das mal an.

Task hat doch sowas wie .ContinueWith(). Schau dir das mal an.

Das ContinueWith() sollte man normalerweise nicht benutzen.

Mit einem await erreicht man das gleiche (Code ausführen nach Beendigung des Tasks), nur dass das await einen besser lesbaren Code ermöglicht.

Für mich sieht das eher nach einem Bedien-Fehler mit async/await aus.

PS:
Hier stand vorher eine andere Nachricht.
Die habe ich gelöscht, da ich meinen Vorredner falsch verstanden hatte.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007!

Das ist ein Missverständnis. Mit View meinte ich eine DataView (ist am Ende wieder eine Liste)! Wenn ich diese in einen Task auslagere und dann darauf "warte" wird die Reihenfolge auch korrekt eingehalten:

public async Task LoadDataAsync()
{
    data1 = await LoadDataFromXML1Async();
    data2 = await LoadDataFromXML2Async(data1);
    data3 = await LoadDataFromXML3Async(data1);
    data4 = await Vorgang1_BuildDataView();
}

Deine "Struktur" hat mir für das Verständnis (was ist wo verfügbar) sehr geholfen!!!

Vielen Dank noch einmal!

Sorry, ich muss mein Problem noch einmal "wiederbeleben" ...

Im Prinzip ist es noch genau das Gleiche wie am Anfang ... jetzt kann ich die Quelle jedoch genauer lokalisieren.

System.ArgumentException

DependencySource muss in demselben Thread wie DependencyObject erstellt werden.

Dieser Fehler tritt immer genau dann auf, wenn ich ein im Daten-Thread eingelesenes Objekt in der View aktualisieren möchte.

Beispiel (View Aktualisierung explizit):

public class MyVM : MVVM_Base
{

    private PK _data1;
    public PK data1 { get => _data1; set { _data1 = value; } } 

    ...
    
    public async Task LoadDataAsync()
    {
        data1 = await LoadDataFromXML1Async();        // Wird ohne Fehler der VM-Eigenschaft zugewiesen
        data2 = await LoadDataFromXML2Async(data1);
        ...      
    }

        ...
        
    public void ProgrammDatenViewAktualisieren()
    {
        OnChanged(nameof(data1));      // ---> Exception !!!
    }
    
}

Beispiel (View Aktualisierung implizit):

public class MyVM : MVVM_Base
{
    private PK _data1;
    public PK data1 { get => _data1; set { _data1 = value; OnChanged(nameof(data1)); } } 

    ...
    
    public async Task LoadDataAsync()
    {
        data1 = await LoadDataFromXML1Async();           // ---> Exception !!!
        data2 = await LoadDataFromXML2Async(data1);
        ...      
}

Der Sachverhalt ist so ja auch erst einmal verständlich, wenn man auch sonst den umgekehrten Fall hat, dass man aus dem Daten-Thread die View aktualisieren möchte. Ich dachte nur, dass await das eingelesene Objekt dem GUI-Thread zuordnet.

Du darfst auch keine UI-Controls außerhalb des UI-Threads anfassen. Das gilt auch für ViewModel-Eigenschaften und Events, wenn die UI daran bindet bzw. darauf horcht.

Dafür gibt's ja (unter Anderem) async/await. Wenn Du vor dem await im UI-Thread warst, bist Du es hinterher wieder, selbst wenn dazwischen ein anderer Thread am arbeiten war - einfach ausgedrückt. Das setzt aber voraus, dass Du vor dem await auch im UI-Thread warst.

Was sich nun wo und wie und aus welchem Thread aufruft, wissen wir nicht, das zeigst Du nicht.
Aber der Hintergrund-Thread darf nichts an der UI ändern.
Wenn Du aus dem Hintergrund-Thread live die UI aktualisieren willst, musst Du das über Umwege machen, z.B. indem Du aus der UI regelmäßig nach Aktualisierungen fragst. Es gibt aber auch Möglichkeiten, die Synchronisierung selber zu nutzen, entweder direkt über den SynchronizationContext (nicht nutzen, bevor Du weißt, was Du tust), oder Du guckst dir die JoinableTaskFactory-Klasse aus dem NuGet-Package "Microsoft.VisualStudio.Threading" an, dazu gibt's auch noch ein Analyzer-Package, was dich auf async/await-Fehler hinweisen kann - solltest Du auch installieren.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Th69!

LoadDataAsync rufe ich vom GUI-Thread WindowLoadedBehavior → VM → LoadDaten() aus auf.

public class WindowLoadedBehavior : Behavior<MainWindow>
{
    protected override void OnAttached() => AssociatedObject.Loaded += AssociatedObject_Loaded;
    protected override void OnDetaching() => AssociatedObject.Loaded -= AssociatedObject_Loaded;

    private async void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {

        MainWindow view = (MainWindow)sender;
        VMDVDVerwaltung vm = (VMDVDVerwaltung)((MainWindow)sender).DataContext;

        ...
        bool HasDatenLoaded = await vm.LoadDaten();                                     // Programm-Daten laden

    }
}

Im Daten-Thread ordne (möchte) ich der VM-Eigenschaft (PK) den eingelesenen Wert zuordnen:  ( if (Erg_LoadPK.IsErfolgreich) PK = (PK)Erg_LoadPK.Daten;                                        // Eingelesene Programm-Konfiguration übernehmen )

        /// <summary>
        /// Alle Programm-Daten laden.
        /// </summary>
        public async System.Threading.Tasks.Task<bool> LoadDaten()
        {
            #region Programm-Konfiguration laden

            PK = new();                                                                       // Standard Programm-Konfiguration initalisieren
            ErgAsync Erg_LoadPK = await System.Threading.Tasks.Task.Run(() => ProgrammDaten_LoadPK(PK, AppData_Pfad));
            if (Erg_LoadPK.IsErfolgreich) PK = (PK)Erg_LoadPK.Daten;                          // Eingelesene Programm-Konfiguration übernehmen            

Wenn an dieser Stelle im Setter (von PK) OnChange() ausgeführt wird, kracht (Exception) es.

Dann schau mal im Debugger, in welchem Thread sich die Methode LoadDaten() befindet (Fenster "Threads") bzw. lass dir Thread.CurrentThread (bzw. dessen ManagedThreadId) ausgeben (im Vergleich zum UI-Thread).

PS: Kannst du nicht ProgrammDaten_LoadPK() auch async implementieren?

Hallo Th69!

LoadDaten läuft im Daten-Thread.

Mal einen kleinen Moment (so einen wie in der Werbung 😉) ProgrammDaten_LoadPK asynchron vom Behavior aus aufzurufen und da das Ergebnis an die VM-Eigenschaft zu übergeben ist (vielleicht) auch eine gute Idee!

So geht es auch nicht! Die Datei lese ich ja immer in einem Arbeits-Thread ein. Wenn await das eingelesene Objekt nicht dem GUI-Thread zuordnet wird es immer krachen.

WindowLoadedBehavior
{
    async AssociatedObject_Loaded
    {
        ----- GUI-Thread
        Erg = await System.Threading.Tasks.Task.Run(() => ProgrammDaten_LoadPK2());
        vm.PK = (PK)Erg.Daten;   // Eingelesene Programm-Konfiguration übernehmen   ----> Exception <-------
    }
    
    public async System.Threading.Tasks.Task<ErgAsync> ProgrammDaten_LoadPK2(PK PK2, string AppDataVerz)
    
    {
        ---- Daten-Thread
    
        ...
        return erg;       // Ergebnis übergeben
    }
}

Etwas ausführlicher ...

public class WindowLoadedBehavior : Behavior<MainWindow>
{
    protected override void OnAttached() => AssociatedObject.Loaded += AssociatedObject_Loaded;
    protected override void OnDetaching() => AssociatedObject.Loaded -= AssociatedObject_Loaded;

    private async void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {

// im GUI-Thread ----------

        MainWindow view = (MainWindow)sender;
        VMDVDVerwaltung vm = (VMDVDVerwaltung)((MainWindow)sender).DataContext;

            #region Programm-Konfiguration laden
            
            vm.AppData_Pfad = ar_Global.ar_Path.BuildCommonAppDataPath();
            vm.PK = new();                                                             // Standard Programm-Konfiguration initalisieren
            ErgAsync Erg_LoadPK = await System.Threading.Tasks.Task.Run(() => ProgrammDaten_LoadPK2(vm.PK, vm.AppData_Pfad));
            if (Erg_LoadPK.IsErfolgreich) vm.PK = (PK)Erg_LoadPK.Daten;                // Eingelesene Programm-Konfiguration übernehmen            

            #endregion Abschluß: Programm-Konfiguration


        public async System.Threading.Tasks.Task<ErgAsync> ProgrammDaten_LoadPK2(PK PK2, string AppDataVerz)
        {
        // im Daten-Thread ----------
         
            ...
            return erg;       // Ergebnis übergeben
        }        
     }
 }  
        

Wo genau tritt die Exception auf?
Wenn er dir nur ein await anmeckert, dann geh mal in die Methode und debugge dort, um zu sehen, wo er raus springt.

Ich rate mal, Du machst in ProgrammDaten_LoadPK2 irgendetwas, was da nicht gemacht werden darf, denn genau das läuft auf einem ThreadPool-Thread und darf nicht auf den UI-Thread zugreifen. Das kann auch ein Event sein, oder eine ViewModel-Property, die Du änderst, wenn die UI irgendwie darauf reagiert, führt das zu der Exception.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007!

Die Exception tritt genau bei der Zuordnung des Abgerufenen Wertes zur ViewModel-Eigenschaft (vm.PK) und da genau (im Setter) bei der Ausführung der OnChange-Methode, auf.

if (Erg_LoadPK.IsErfolgreich) vm.PK = (PK)Erg_LoadPK.Daten;                                        // Eingelesene Programm-Konfiguration übernehmen            

Wie im Bild ersichtlich, wurde die ProgrammDaten_LoadPK2-Methode korrekt (und abschließend) ausgeführt. Alle eingelesenen Daten sind korrekt.

Ich bin mir sicher, wenn ich einen einfachen String zurückgeben würde, würde die Exception auch ausgerufen.

Wie sieht denn nun Dein Laden in Summe aus? Immer noch wie aus dem Startbeitrag?
Da sind nen paar Dinge fischig - würde gerne wissen, ob die immer noch nach dem zahlreichen Feedback fischig sind.

Hallo Abt!

Zur Zeit versuche ich die Daten im WindowLoadedBehavior zu laden. Die alte LoadDaten()-Methode habe ich erst einmal auskommentiert.

Hier der vollständige Behavior:

public class WindowLoadedBehavior : Behavior<MainWindow>
{
    protected override void OnAttached() => AssociatedObject.Loaded += AssociatedObject_Loaded;
    protected override void OnDetaching() => AssociatedObject.Loaded -= AssociatedObject_Loaded;

    private async void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {

        MainWindow view = (MainWindow)sender;
        VMDVDVerwaltung vm = (VMDVDVerwaltung)((MainWindow)sender).DataContext;

        // Nach dem Laden des Fensters die ListView-Spalten-Breite aktualisieren
        if (vm.DVDEinträgeView != null)                                                 // Sind DVD-Einträge vorhanden?
        {
            vm.DVDEinträgeView.MoveCurrentToLast();                                     // Zum letzten Eintrag gehen
            view.lv.ScrollIntoView(vm.DVDEinträgeView.CurrentItem);                     // Eintrag in sichtbaren Bereich verschieben
            arWPF.WPF_Tools.WPF_Tools.ListView_Helper.UpdateListViewColumns(view.lv);   // List-View Spalten-Breite aktualisieren
        }

        await System.Threading.Tasks.Task.Delay(1000);                                  // 2s warten, damit Splash-Screen abgeschlossen ist

        #region Programm-Konfiguration laden
        
        vm.AppData_Pfad = ar_Global.ar_Path.BuildCommonAppDataPath();                                      // Anwendungs-Unterverzeichnis im zentralem Programm-Daten-Verzeichnis (AllUser) erstellen, wenn es noch nicht vorhanden ist.
        vm.PK = new();                                                                                     // Standard Programm-Konfiguration initalisieren
        ErgAsync Erg_LoadPK = await System.Threading.Tasks.Task.Run(() => ProgrammDaten_LoadPK2(vm.PK, vm.AppData_Pfad));
        if (Erg_LoadPK.IsErfolgreich) vm.PK = (PK)Erg_LoadPK.Daten;                                        // Eingelesene Programm-Konfiguration übernehmen            

        #endregion Abschluß: Programm-Konfiguration

    }

    #region Daten-Abrufe

    /// <summary>
    /// Programm-Konfiguration (asynchron) einlesen.
    /// </summary>
    /// <param name="StdPK">Standard-Konfigurations-Daten.</param>
    /// <param name="AppDataVerz">Standard Programm-Daten-Verzeichnis.</param>
    /// <returns>Indikator: Daten: Programm-Konfiguration + Indikator: Konnten die Programm-Konfigurations-Daten ordnungsgemäß geladen werden?</returns>
    public async System.Threading.Tasks.Task<ErgAsync> ProgrammDaten_LoadPK2(PK PK2, string AppDataVerz)
    {

        await System.Threading.Tasks.Task.Delay(10); // Placeholder for asynchronous work.

        ErgAsync erg = new() { IsErfolgreich = true };                                                  // Ergebnis initalisieren

        if (string.IsNullOrEmpty(AppDataVerz))                                                        // Ist das Anwendungs-Unterverzeichnis im Programm-Daten-Verzeichnis vorhanden?
        {
            MessageBox.Show("Auf das Programmkonfigurationsverzeichnis (" + ar_Global.ar_Path.GetProgDataPath() + ") kann nicht zugegriffen werden." + Environment.NewLine + Environment.NewLine +
                            "Wenden sie sich an ihren Programm-Administrator!", "Programm-Administrator benachrichtigen!",
                            MessageBoxButton.OKCancel, MessageBoxImage.Hand);
            return new ErgAsync(PK2, false, false, "Programmkonfigurations-Verzeichnis ist nicht vorhanden!!!", Brushes.Red);
        }

        if (File.Exists(Path.Combine(AppDataVerz, PK2.KonfigDateiName)))                              // Programm-Konfigurations-Datei vorhanden? -> PK aus XML-Datei "laden"
        {
            PK2 = ar_Global.Serialisieren.DeSerializeObject<PK>(Path.Combine(AppDataVerz, PK2.KonfigDateiName));        // Programm-Konfigurations-Datei versuchen zu "laden"
            #region Gespeicherte Farben übernehmen
            PK2.FarbeHGBereiche = (SolidColorBrush)new BrushConverter().ConvertFromString(PK2.FarbeHGBereiche_Color);     // Farbe der Hintergrund-Bereiche übernehmen
            PK2.FarbeHGDVDListe = (SolidColorBrush)new BrushConverter().ConvertFromString(PK2.FarbeHGDVDListe_Color);     // Farbe des Hintergrunds der DVD-Liste übernehmen
            #endregion
        }

Nur 8000 Zeichen ... Part 2 folgt

Part 2
        if (PK2 is null)                                                                                // Programmkonfiguration konnte nicht eingelesen werden
        {
            _ = MessageBox.Show("Es konnte keine (gültige) Programm-Konfigurations-Datei gefunden werden." + Environment.NewLine + Environment.NewLine +
                        "Es wird eine Standard-Konfiguration geladen." + Environment.NewLine + Environment.NewLine +
                        "Sie können die Programm-Konfiguration nach ihren Erfordernissen anpassen und speichern (Reiter Einstellungen).",
                        "Erstaufruf des Programm's!", MessageBoxButton.OK, MessageBoxImage.Hand);
            PK2 = new();                                                                                      // Standard Programm-Konfiguration übernehmen (wurde evt. beim Deserialisieren Null gesetzt!)
        }

        if (string.IsNullOrEmpty(PK2.DatenVerzeichnis))                                                  // Es wurde (noch) kein Daten-Verzeichnis definiert oder die Programm-Konfiguration wurde noch nie gespeichert! (1. Einlesen der Programm-Konfiguration) 
        {
            string StandardDatenVerzeichnis = Path.Combine(AppDataVerz, PK2.StandardVerzeichnisNameDatenVerzeichnis);   // Standard Daten-Verzeichnis initalisieren

            if (!Directory.Exists(StandardDatenVerzeichnis))                                            // Das Standard-Daten-Verzeichnis exestiert noch nicht!
            {
                try                                                                                     // Das Standard Daten-Verzeichnis erstellen
                {
                    _ = Directory.CreateDirectory(StandardDatenVerzeichnis);                            // Daten-Verzeichnis erstellen
                    PK2.DatenVerzeichnis = StandardDatenVerzeichnis;                                    // In der Programm-Konfiguration das Daten-Verzeichnis auf das Standard-Daten-Verzeichnis setzen
                    erg.Meldung = "Das Standard Daten-Verzeichnis wurde erstellt." + Environment.NewLine;
                    erg.HasChangePK = true;                                                             // Indikator: Programm-Konfiguration wurde geändert, setzen
                    erg.MeldungsHG = Brushes.Green;                                                     // Meldung Hintergrund aktualisieren
                }
                catch (Exception ex)                                                                    // Die Erstellung des Standard Daten-Verzeichnisses konnte nicht korrekt ausgeführt werden!
                {
                    string Mldg = "Das Standard Daten-Verzeichnis: " + Environment.NewLine + Environment.NewLine;
                    Mldg += StandardDatenVerzeichnis + Environment.NewLine + Environment.NewLine;
                    Mldg += "konnte nicht erstellt werden!!!" + Environment.NewLine + Environment.NewLine;
                    Mldg += "Das Programm kann nicht fortgeführt werden!" + Environment.NewLine + Environment.NewLine;
                    Mldg += "Weitere Hinweise entnehmen sie bitte der Fehlermeldung." + Environment.NewLine + Environment.NewLine;
                    Mldg += "Fehler:" + Environment.NewLine + Environment.NewLine + ex.Message;
                    _ = MessageBox.Show(Mldg, "Ohne Daten-Verzeichnis geht's nicht!", MessageBoxButton.OK, MessageBoxImage.Error);
                    return new ErgAsync(null, false, false, "Das Daten-Verzeichnis konnte nicht erstellt werden!", Brushes.Red);
                }
            }
        }
        erg.Daten = PK2;                                                                                // Programm-Konfiguration übergeben
        erg.Meldung += "Die Programm-Konfiguration wurde geladen.";                                     // Meldung aktualisieren
        return erg;                                                                                     // Ergebnis übergeben

    }

    #endregion
}

und Part 3 ...

Part 3
Und die Hilfsklasse zur Strukturierung der Abruf-Ergebnisse:

public class ErgAsync
{
    /// <summary>
    /// Die Daten der asynchronen Abfrage.
    /// </summary>
    public object Daten { get; set; }

    /// <summary>
    /// Indikator: War der asynchrone Vorgang erfolgreich. 
    /// </summary>
    public bool IsErfolgreich { get; set; }

    /// <summary>
    /// Indikator: Wurde die Programm-Konfiguration geändert?. 
    /// </summary>
    public bool HasChangePK { get; set; }

    /// <summary>
    /// Meldungs-Text. 
    /// </summary>
    public string Meldung { get; set; } = string.Empty;

    public SolidColorBrush MeldungsHG { get; set; }
 
    public ErgAsync() { }

    public ErgAsync(object daten, bool isErfolgreich, bool hasChangePK = false, string meldung = "", SolidColorBrush meldungsHG = null)
    {
        Daten = daten; IsErfolgreich = isErfolgreich; HasChangePK = hasChangePK;
        Meldung = meldung; MeldungsHG = meldungsHG is null ? Brushes.Transparent : meldungsHG;
    }
}
Ich bin gerade dabei eine Beispiel-Applikation zu basteln, damit es leicht(er) nachzuvollziehen ist.

Entgegen meiner Vermutung, wenn ich einen String (allein) als Rückgabewert benutze, kommt keine Exception. Vielleicht hängt es irgendwie mit der Kapslung des Rückgabe-Wertes als Objekt zusammen?

Deine Benennungen sind nicht sehr intuitiv. Was ist PK?

Nutzt das irgendwelche Events, INotifyPropertyChanged?
Du übergibst das an ProgrammDaten_LoadPK2 in einen anderen Thread und änderst Dinge.

Warum es erst danach knallt, sehe ich so auf Anhieb nicht, aber die genannten Zugriffe sehen ziemlich falsch aus.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Du übergibst das an ProgrammDaten_LoadPK2 in einen anderen Thread und änderst Dinge.

So sehe ich das auch. Das gehört komplett aufgeräumt.

Warum es erst danach knallt, sehe ich so auf Anhieb nicht, aber die genannten Zugriffe sehen ziemlich falsch aus.

Ich geh davon aus, weil erst die Bindung das verletzende Element hier ist. Die Änderung wird toleriert bzw. ist Zufall, dass es nicht knallt.
Es ist nicht Thread-Safe, aber aber es findet auch kein Check statt.


Konzeptionell ist hier viel im Argen, weil Methoden Objekte verändern, Dinge übergeben sollten, die so nicht übergeben werden sollten und das von Außen nicht sichtbar ist.
Auch sehe ich paar dubiose Dinge, wie zB dass ein Event ein Task.Run erzeugt und darin irgendwelche VM Manipulationen stattfinden.

Das ist nicht die Idee von Code-Architektur oder MVVM 😃

Hallo Palladin007!

Ja, die Bezeichnung ist nicht so gut gewählt. PK ist sowohl ein Type wie auch eine Eigenschaft.

Es gibt also die  Klasse PK:

    #region Programm-Konfiguration
    [Serializable]
    /// <summary>
    /// Klasse zur Strukturierung der Programm-Konfiguration.
    /// </summary>
    public class PK
    {

        ...
    }

als auch die Eigenschaft PK im ViewModel:

public class VMDVDVerwaltung : MVVM_Base
{

    /// <summary>
    /// Programm-Konfiguration
    /// </summary>
    private PK _PK;
    public PK PK { get => _PK; set { _PK = value; OnChanged(nameof(PK)); } }

    ....
}

Ja, das hatte auch seinen Sinn (Übergabe der PK-Eigenschaft). Wenn die Programm-Konfigurations-Datei nicht aus der Datei geladen werden kann, wird eine Standard Programm-Konfiguration (= neue Instanz der PK-Klasse) verwendet. Du hast aber recht, dass kann ich jetzt jetzt komplett in die ProgrammDaten_LoadPK2 Methode verschieben und mir den Parameter sparen. Ich bin sogar noch einen Schritt weiter gegangen und habe den gesamten Inhalt (zum Laden der XML-Datei) entfernt:

public static async System.Threading.Tasks.Task<ErgAsync> ProgrammDaten_LoadPK2()
{
    PK PK2 = new();

    await System.Threading.Tasks.Task.Delay(10); // Placeholder for asynchronous work.

    ErgAsync erg = new() { IsErfolgreich = true };                                                  // Ergebnis initalisieren

    erg.Daten = PK2;                                                                                // Programm-Konfiguration übergeben
    erg.Meldung += "Die Programm-Konfiguration wurde geladen.";                                     // Meldung aktualisieren
    return erg;                                                                                     // Ergebnis übergeben
}

Und trotzdem wird immer noch die Exception geworfen! Siehe Bild.

Es wird sogar noch unerklärlicher ... in der Beispiel-Applikation funktioniert der 1:1 übernommene Code!  :0

(Load-Behavior → asynchroner Abruf der Daten mit casten der Daten in der Ergebnis-Struktur)

???

public class WindowLoadedBehavior : Behavior<MainWindow>
{
    protected override void OnAttached() => AssociatedObject.Loaded += AssociatedObject_Loaded;
    protected override void OnDetaching() => AssociatedObject.Loaded -= AssociatedObject_Loaded;

    private async void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
    {

        MainWindow view = (MainWindow)sender;
        MyVM vm = (MyVM)((MainWindow)sender).DataContext;

        vm.MyVMEigenschaft = await System.Threading.Tasks.Task.Run(() => MyDatenLoad());    // Eingelesene Daten (in's VM) übernehmen

        await Task.Delay(1000);                                                             // 1s warten (und async ermöglichen)
        ErgAsync Erg_LoadDaten2 = await System.Threading.Tasks.Task.Run(() => MyDatenLoad2("Hallo"));
        if (Erg_LoadDaten2.IsErfolgreich) vm.MyVMEigenschaft = (string)Erg_LoadDaten2.Daten;  // Eingelesene Daten übernehmen            

        await Task.Delay(1000);                                                             // 1s warten (und async ermöglichen)
        ErgAsync Erg_LoadDaten3 = await System.Threading.Tasks.Task.Run(() => MyDatenLoad3());
        if (Erg_LoadDaten3.IsErfolgreich) vm.MyVMEigenschaft = ((PK)Erg_LoadDaten3.Daten).DateiNameAblageTypenListe;  // Eingelesene Daten übernehmen            


        ErgAsync Erg_LoadPK = await System.Threading.Tasks.Task.Run(() => ProgrammDaten_LoadPK2());
        if (Erg_LoadPK.IsErfolgreich) vm.PK = (PK)Erg_LoadPK.Daten;                                        // Eingelesene Programm-Konfiguration übernehmen            

        await Task.Delay(3000);                                                             // 3s warten (und async ermöglichen)
        view.Close();                                                                       // Fenster schließen
    }

    #region Daten-Abrufe

    /// <summary>
    /// Daten laden.
    /// </summary>
    /// <returns>String zurückgeben.</returns>
    public static async Task<string> MyDatenLoad()
    {
        await Task.Delay(1000);         // 1s warten (und async ermöglichen)
        return "Meine Daten ...";       // Ergebnis übergeben
    }

    /// <summary>
    /// Daten laden.
    /// </summary>
    /// <returns>Ergebnis-Struktur zurückgeben.</returns>
    public static async Task<ErgAsync> MyDatenLoad2(string myText)
    {
        await Task.Delay(1000);                             // 1s warten (und async ermöglichen)
        return new(myText + " Meine Daten 2 ...", true);    // Ergebnis übergeben
    }

    /// <summary>
    /// Daten laden.
    /// </summary>
    /// <returns>Ergebnis-Struktur und Klasse zurückgeben.</returns>
    public static async Task<ErgAsync> MyDatenLoad3()
    {
        await Task.Delay(2000);                 // 1s warten (und async ermöglichen)
        return new(new PK(), true);             // Ergebnis übergeben
    }

    public static async System.Threading.Tasks.Task<ErgAsync> ProgrammDaten_LoadPK2()
    {
        PK PK2 = new();

        await Task.Delay(10); // Placeholder for asynchronous work.

        ErgAsync erg = new() { IsErfolgreich = true };                                                  // Ergebnis initalisieren

        erg.Daten = PK2;                                                                                // Programm-Konfiguration übergeben
        erg.Meldung += "Die Programm-Konfiguration wurde geladen.";                                     // Meldung aktualisieren
        return erg;                                                                                     // Ergebnis übergeben
    }

    #endregion
}

Schreib mal zwischen jedes await in der AssociatedObject_Loaded ein Debug.WriteLine($"{Environment.CurrentManagedThreadId} | {SynchronizationContext.Current?.GetType()?.Name}")
Das sollte eigentlich immer die gleiche ID sein, aber da es bei der PK-Zuweisung zu der Exception kommt, deutet es darauf hin, dass es dabei eben nicht die richtige ID ist.
Und der SynchronizationContext sollte nicht null sondern DispatcherSynchronizationContext sein, da der für die Synchronisation zuständig ist.

Oder das PropertyChanged-Event tut mehr, als wir wissen. In den Kommentaren von der MVVM_Base steht etwas komisches über den Dispatcher. Hast Du irgendwo eigene PropertyChanged-EventHandler geschrieben, ggf. in der MVVM_Base.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007!

Jetzt geht's ans Eingemachte 😃 an der Stelle wäre bei mir Schluß! Aber sehen wir mal wie weit wir kommen!

  1. Die drei Anweisungen habe ich so platziert:
private async void AssociatedObject_Loaded(object sender, RoutedEventArgs e)
{
    MainWindow view = (MainWindow)sender;
    VMDVDVerwaltung vm = (VMDVDVerwaltung)((MainWindow)sender).DataContext;


    System.Diagnostics.Debug.WriteLine($"{Environment.CurrentManagedThreadId} | { System.Threading.SynchronizationContext.Current?.GetType()?.Name}");

    await System.Threading.Tasks.Task.Delay(1000);                                  // 2s warten, damit Splash-Screen abgeschlossen ist

    System.Diagnostics.Debug.WriteLine($"{Environment.CurrentManagedThreadId} | { System.Threading.SynchronizationContext.Current?.GetType()?.Name}");

    #region Programm-Konfiguration laden
    vm.AppData_Pfad = ar_Global.ar_Path.BuildCommonAppDataPath();                                      // Anwendungs-Unterverzeichnis im zentralem Programm-Daten-Verzeichnis (AllUser) erstellen, wenn es noch nicht vorhanden ist.
    ErgAsync Erg_LoadPK = await System.Threading.Tasks.Task.Run(() => ProgrammDaten_LoadPK2());

    System.Diagnostics.Debug.WriteLine($"{Environment.CurrentManagedThreadId} | { System.Threading.SynchronizationContext.Current?.GetType()?.Name}");

    if (Erg_LoadPK.IsErfolgreich) vm.PK = (PK)Erg_LoadPK.Daten;            // Eingelesene Programm-Konfiguration übernehmen      ----> Exception
...

Dies erzeugt die Ausgabe:

"DVDVerwaltung.exe" (CoreCLR: clrhost): "C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\5.0.17\de\PresentationCore.resources.dll" geladen. Das Modul wurde ohne Symbole erstellt.
1 | DispatcherSynchronizationContext
1 | DispatcherSynchronizationContext
1 | DispatcherSynchronizationContext

2. "Eigene" PropertyChanged-Eventhandler habe ich:

[Serializable]
/// <summary>
/// Basisklasse, die die OnChanged-Methode auf Basis der INotifyPreopertyChanged-Schnittstelle bereitstellt.
/// </summary>
public class MVVM_Base : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    /// Dispatcher-Objekt welches den aktuellen (current) Dispatcher des ViewModels bereitstellt.
    /// (Für den Vergleich, ob sich das an die GUI gebundene Objekt in einem anderen Thread befindet.)
    /// Aufrufbeispiel:
    /// = VM ====
    /// if (DispatcherObject.Thread != System.Threading.Thread.CurrentThread)
    /// {
    ///    DispatcherObject.Invoke(() => ... Zugriff auf GUI-Objekt )));
    /// }
    /// </summary>
    public virtual System.Windows.Threading.Dispatcher DispatcherObject { get; protected set; }
    protected MVVM_Base()
    {
        DispatcherObject = System.Windows.Threading.Dispatcher.CurrentDispatcher;
    }
}

... ich denke aber, diese sind Standard!?

Das DispatcherObject ist das CurrentDispatcher-Object des aktuellen Threads.

Anmerkung: Diese Basis-Klasse verwende ich sowohl im aktuellen wie auch im Test - Projekt. (Ist Standard bei mir.) Im Test-Projekt funktioniert das (komischerweise).

Ergebnis im Test-Projekt:

1 | DispatcherSynchronizationContext
1 | DispatcherSynchronizationContext
1 | DispatcherSynchronizationContext
1 | DispatcherSynchronizationContext
1 | DispatcherSynchronizationContext

Hilft das?

Die Ausgabe ist wie erwartet korrekt - hätte mich auch gewundert, aber sicher ist sicher.

Mit EventHandler meine ich aber nicht das OnChanged, sondern ob Du irgendwo das PropertyChanged Event selber abonierst und darauf reagierst. Wäre möglich, dass der Fehler ganz woanders liegt und nur indirekt durch die PK-Zuweisung ausgelöst wird.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007!

Na ich habe da schon noch ein paar Changed-Behavior's im Angebot! Wenn du das meinst.

  1. Behavior<Xceed.Wpf.Toolkit.ColorPicker> → AssociatedObject.SelectedColorChanged
  2. DVDEintragChangeBehavior : Behavior<ListView> → AssociatedObject.SelectionChanged
  3. ScrollViewerContentBottomBehavior : Behavior<ScrollViewer> → AssociatedObject.ScrollChanged

OnChanged(nameof(PK));    löse ich an sehr vielen Stellen aus. Aber das ist doch kein Abonieren. Oder meinst du das?

Ich meine sowas:

vm.PropertyChanged += MyOnPropertyChangedHandler;

// ...

private void MyOnPropertyChangedHandler(object? sender, PropertyChangedEventArgs e)
{
    // ...
}

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Nein, so etwas brauche ich nicht- 😃  😃

Ich versuche eine Kopie des Projektes jetzt so lang "leer zu räumen", bis der Fehler nicht mehr auftritt.

Kann dauern ...  (Converter und Behaviors habe ich schon entfern, hat noch nichts gebracht ...).

Vielen Dank für deine anhaltende Unterstützung!!!

Hallo Palladin007!

Die Ursache habe ich wieder einmal gefunden, aber ...

Der Fehler tritt so lang auf, solang eine Eigenschaft in der View an die VM-Eigenschaft PK gebunden ist.

Im konkretem besteht meine View nur noch aus einem Border, dessen Background an PK.FarbeHGBereiche gebunden ist.

<Border ... Background="{Binding PK.FarbeHGBereiche}"  />

Wenn ich das PK-Binding lösche, wird die Exception nicht mehr geworfen.

... Habe weiter experimentiert ... Es liegt wirklich im Detail!!!

PK.Text kein Problem.  PK.Brush = Exception. Die Ursache liegt in der CanFreeze-Eigenschaft des Brush'es.

Habe bloß noch nicht gefunden, wie man das ändern kann.

FarbeHGBereiche.CanFreeze = false;

Eine direkte Zuweisung ist nicht möglich. Der Standard-Wert ist True, deshalb ist die Farbe auch eingefroren und lässt sich nicht ändern.

Diese Abhängigkeit lässt sich jetzt auch ohne Probleme in der Beispiel-Applikation reproduzieren.

Aber nicht mehr Heute!!!! (Lösung finden)

Nochmals vielen Dank!!! Ohne dich wäre ich nie so weit gekommen!

Das mit dem Binding hatte ich aber erwähnt 😉
Das ist aber auch nicht der Fehler, dass er da eine Exception wirft, ist korrekt so, weil er aus einem anderen Thread kommt.
Du hast also irgendwas, was dafür sorgt, dass er aus einem anderen Thread kommt und das gilt es zu finden.
Im hier gezeigten Code sehe ich davon aber nichts und der Debug.WriteLine-Test hat das auch bestätigt.

Wie genau das CanFreeze da hinein spielt, weiß ich nicht, ich vermute aber, dass das eine Optimierung ist, damit WPF die Farben nicht auch noch überwachen muss. Dann ändert sich die Farbe nicht, also muss auch im UI-Thread nichts aktualisiert werden.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Es geht also gar nicht direkt um das Binden eines PK-Objekts, sondern um SolidColorBrush. Dieses ist eine UI-Klasse und sollte daher nicht im VM verwendet werden.

Besser ist es, nur Color zu verwenden und beim Binden dann einen ColorBrushConverter zu benutzen, s. z.B. Data binding overview (WPF .NET): Data conversion.

@Th69

Tatsache, Du hast Recht, ich hab's gerade nochmal getestet.

Dass ein Brush ein UI-Objekt ist, wusste ich, aber ich dachte, das Ding tut solange nichts, bis es irgendwo benutzt wird.
Laut meinem Test ist das auch der Fall, man kann das Brush-Objekt im anderen Thread erstellen und damit arbeiten, aber in der UI nutzen darf man es danach nicht, selbst wenn das wieder im UI-Thread stattfindet. Deshalb knallt es auch erst bei der PK-Zuweisung, wo er eigentlich im richtigen Thread ist.
Und wenn man es Freezed, tritt das Problem nicht mehr auf.

Ist ein gutes Beispiel, warum man View und ViewModel nicht mischen sollte 😉


Mein kleines Test-Projekt:

MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <StackPanel>
        <Button Click="Button_Click" Content="Test" HorizontalAlignment="Left" />
    </StackPanel>
</Window>

MainWindow.xaml.cs

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
    }

    private async void Button_Click(object sender, RoutedEventArgs e)
    {
        var brush = await Task.Run(CreateBrush);
        Background = brush;
    }

    private Brush CreateBrush()
    {
        var brush = new SolidColorBrush()
        {
            Color = Colors.Red,
        };

        //brush.Freeze();

        return brush;
    }
}

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007, Hallo Th69!

Woher soll der geneigte Programmierer wissen, welche Objekte aus einer UI-Klasse stammen? Anhand meiner Programmier-Bibel (Buch: WPF von Thomas Claudius Huber) erkenne ich zwar aus der WPF-Klassenhierarchie das Brush von den Objekten: Object → DispatcherObject → DependencyObject → Freezable → Animatable erbt, aber wie ist der Zusammenhang zur Eingruppierung eines Objektes in eine UI-Klasse???

@Th69

Da ich die PK-Klasse serialisiere und aus diesem Grund die darin enthaltenen Brush'es sowieso schon als String (Color-Wert) speichere und bei der Deserialisierung (in der PK Klasse) wieder in Brush'es zurück konvertiere, spare ich mir dies jetzt (Erkenntnis UI-Klassen-Objekte gehören nicht ins VM) und verwende in der Applikation  einen String <-> SolidColorBrush Konverter.  Vielen Dank!

[Serializable]
/// <summary>
/// Klasse zur Strukturierung der Programm-Konfiguration.
/// </summary>
public class PK
{

    /// <summary>
    /// HG-Farbe der Bereiche des Programms
    /// </summary>
    [System.Xml.Serialization.XmlIgnore]
    public Brush FarbeHGBereiche { get; set; }
    public string FarbeHGBereiche_Color { get; set; } = "#FFADD8E6";    // HG-Farbe der Bereiche des Programms als String

    ....
    
    public PK()
    {
        FarbeHGBereiche = (SolidColorBrush)new BrushConverter().ConvertFromString(FarbeHGBereiche_Color);   // HG-Farbe der Bereiche
        ...
    }
}

@Palladin007

Du hast das Problem wieder einmal auf den Punkt zusammengefasst! Bewusst habe ich View-Objekt und ViewModel natürlich nicht "gemischt".

Jetzt weis ich zwar wie ich den Brush auch Freezed'en kann (und das es dann funktioniert), aber ich werde die PK-Klasse auf String-Brushes (Stringwert, der ein Color-Wert darstellt) umstellen.

Was sich doch so alles aus einem Thread-Fehler ergibt!

Ich habe viel grundlegendes über Daten-Threads im ViewModel gelernt! Danke!

Vielen Dank an Alle, die mir geholfen haben!

Woher soll der geneigte Programmierer wissen, welche Objekte aus einer UI-Klasse stammen?

DispatcherObject ist das magische Stichwort, alle die Klassen, die von DispatcherObject ableiten, dürfen nur im UI-Thread verwendet werden.

aber ich werde die PK-Klasse auf String-Brushes (Stringwert, der ein Color-Wert darstellt) umstellen

Nimm doch direkt die Color-Werte? Ist etwas intuitiver, man sieht am Typ, was es ist.

Und den BrushConverter hast Du schon, Du musst ihn nur noch so umstellen, dass es ein ValueConverter ist (IValueConverter) und dann im Binding benutzen. Du bindest dann die String/Color-Property, als Converter deinen ColorToBrushConverter und siehe da, es funktioniert 😉

... zumindest solange es kein anderes Problem gibt.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007!

Einfach ist gut!  → DispatcherObject

Kann man den Type Color direkt serialisieren? Ist eine Struktur da geht das doch bestimmt wieder nicht. Dann ist es einfacher den Converter: String <-> SolidColorBrush zu implementieren.

Na dann bis demnächst ...  😃

Probier's aus, ob das von Haus aus geht, weiß ich nicht, ich rate mal Nein.
Aber sowohl in Newtonsoft.Json, als auch System.Text.Json kann man Converter bauen.
Ich würde das auch tun, einfach um den Color-Type verwenden zu können.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.