Laden...

Forenbeiträge von perlfred Ingesamt 279 Beiträge

27.09.2023 - 17:27 Uhr

Das Problem muss irgendwie mit der MessageBox zusammenhängen.

Die Übrige Konstellation: Command → Async/Await → Methode die await Task.Delay() als Dummy benutzt, habe ich schon sehr oft ohne Fehler benutzt.

Trotzdem Danke! für deine Hinweise!

27.09.2023 - 17:17 Uhr

Hallo Abt!

Das

private async void PKDataRestoreExecuted(object obj)

ist die Command-Routine des Commands das ich an die Rücksichern-Schaltfläche binde.

public ICommand PKDataRestoreCommand { get; private set; }              // Daten zurücksichern Command

PKDataRestoreCommand = new ActionCommand(PKDataRestoreExecuted, PKDataRestoreCanExecute);  // Programm-Konfiguration: // Daten zurücksichern Command, initalisieren

Ist für mich erst einmal Standard, von einem Command ein async/await aufzurufen.

27.09.2023 - 17:01 Uhr

Hallo Abt!!!

Mit den Umlauten bessere ich mich ...

Das async void stellt für mich ein Dummy für asynchrone Methoden dar, die eigentlich keine await Methode haben. Wie in diesem Fall die De-Kompressesions-Methoden.

Apropos hier die Methode aus meiner Bibliothek:

/// <summary>
/// Extrahiert den Inhalt der Archiv-Datei in das (übergebene) Ziel-Verzeichnis.
/// </summary>
/// <param name="ZielVerzeichnis">Das Verzeichnis, in das der Inhalt der Archiv-Datei entpackt werden soll.</param>
/// <param name="ArchivDateiName">Der Name (+Verzeichnis) der Archiv-Datei, dessen Inhalt in das Ziel-Verzeichnis entpackt werden soll. Standard: Leer -> Benutzer-Auswahl</param>
/// <param name="StartArchivVerzeichnis">Das Verzeichnis, von dem aus der Benutzer die Archiv-Datei auswählen kann. Standard: Das Programm-Daten-Verzeichnis des Benutzers.</param>
/// <param name="DateienÜberschreiben">Indikator: Sollen bestehende Dateien im Ziel-Verzeichnis überschrieben werden? Standard: Nein</param>
/// <returns>Indikator: Der Inhalt der Archiv-Datei wurde erfolgreich in das Ziel-Verzeichnis extrahiert (oder nicht).</returns>
public static ZIPErgebnis ArchivToVerzeichnis(string ZielVerzeichnis, string ArchivDateiName = "", string StartArchivVerzeichnis = "" , bool DateienÜberschreiben = false)
{
    if (string.IsNullOrEmpty(StartArchivVerzeichnis))                                    // Wurde das Start Archiv-Verzeichnis nicht angegeben, Standard Archiv-Verzeichnis benutzen
        StartArchivVerzeichnis = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);

    ZIPErgebnis ergebnis = new(ZielVerzeichnis, false, StartArchivVerzeichnis, ArchivDateiName);                        // Zip-Ergebnis initalisieren

    if (string.IsNullOrEmpty(ZielVerzeichnis))                                                  // Wurde ein Ziel-Verzeichnis angegeben? Nein -> abbrechen
    {
        ergebnis.Fehler = new("Es wurde kein Ziel-Verzeichnis (in das der Inhalt der Archiv-Datei entpackt werden soll) angegeben!");
        return ergebnis;
    }
    else                                                                                        // Das Ziel-Verzeichnis wurde angegeben
    {
        if (!DateienÜberschreiben)                                                          // Wurde angegeben, dass die Dateien im Ziel-Verzeichnis nicht überschrieben werden sollen?
            if (Directory.Exists(ZielVerzeichnis))                                              // Ist das Ziel-Verzeichnis schon vorhanden?
                if (Directory.GetFiles(ZielVerzeichnis).Length > 0)                             // Enthält das Ziel-Verzeichnis Datei(en)?
                {
                    ergebnis.Fehler = new("Das Ziel-Verzeichnis: " + ZielVerzeichnis + " exestiert und enthält Dateien!" + 
                                            Environment.NewLine + Environment.NewLine +
                                            "Die Option: Dateien nicht überschreiben verhindert eine Fortführung des Vorganges!");
                    return ergebnis;
                }
    }                                                                                           // Das Ziel-Verzeichnis kann genutzt werden!

    if (string.IsNullOrEmpty(ArchivDateiName))                                                  // Wenn der Archiv (Pfad +) Dateiname nicht übergeben wurde, Archiv-Datei vom Benutzer auswählen lassen
    {
        Microsoft.Win32.OpenFileDialog dlgDateiAuswahl = new()                                  // Datei Auswahl-Dialog deklarieren
        {
            Title = "Archiv-Datei auswählen...",                                                // Titel des Datei-Auswahlfensters
            InitialDirectory = StartArchivVerzeichnis,                                          // Ausgangsverzeichnis (Eigene Dokumente)
            DefaultExt = "zip",                                                                 // Standard Erweiterung
            Filter = "Zip Datei(en)|*.zip",                                                     // Filter der Datei-Erweiterungen
            ValidateNames = false,                                                              // Nicht prüfen, ob die Datei bereits geöffnet oder schreibgeschützt ist
            AddExtension = true,                                                                // Datei-Erweiterung autom. ergänzen
            Multiselect = false                                                                 // Nur eine Datei auswählbar
        };
        if (dlgDateiAuswahl.ShowDialog() == true)
        {
            ArchivDateiName = dlgDateiAuswahl.FileName;                                         // Datei-Auswahl-Dialog aufrufen -> Archiv-Namen Auswahl übernehmen
            ergebnis.ArchivDateiName = Path.GetFileName(ArchivDateiName);                       // Ergebnis Aktualisieren
            ergebnis.ArchivVerzeichnis = Path.GetDirectoryName(ArchivDateiName);                // Ergebnis Aktualisieren
        }
        else                                                                                    // Abbruch der Auswahl ...
        {
            ergebnis.ArchivDateiName = ArchivDateiName;                                         // Ergebnis Aktualisieren
            ergebnis.Abbruch = true;                                                            // Ergebnis Aktualisieren
            return ergebnis;                                                                    // Bei Abbruch der Archiv-Dateinamen-Auswahl, Vorgang abbrechen
        }                     
    }                                                                                           // Archiv-Dateiname ist definiert

    try { ZipFile.ExtractToDirectory(ArchivDateiName, ZielVerzeichnis, DateienÜberschreiben); } // Archiv-Datei in das Ziel-Verzeichnis entpacken
    catch (Exception ex)
    {
        ergebnis.Fehler = new("Bei der Extraktion der Archiv-Datei in das Ziel-Verzeichnisses: " + 
                                ZielVerzeichnis + " ist ein Fehler aufgetreten!" +
                                Environment.NewLine + Environment.NewLine + "Fehler:" +
                                Environment.NewLine + Environment.NewLine + ex.Message);                  // Ergebnis Aktualisieren
        return ergebnis;
    }
    ergebnis.Erfolgreich = true;                                                                        // Ergebnis Aktualisieren
    return ergebnis;
}

Das Ergebnis-Feld ist nur eine Hilfs-Klasse, damit ich die Informationen des Vorgangs als ein Objekt zurückgeben kann:

public class ZIPErgebnis
{
    /// <summary>
    /// Indikator: War der Vorgang erfolgreich?
    /// </summary>
    public bool Erfolgreich { get; set; } = false;

    /// <summary>
    /// Das Quell- oder Ziel-Verzeichnis
    /// </summary>
    public string Verzeichnis { get; set; } = string.Empty;

    /// <summary>
    /// Das (gewählte) Archiv-Verzeichnis?
    /// </summary>
    public string ArchivVerzeichnis { get; set; } = string.Empty;

    /// <summary>
    /// Der (gewählte) Archiv-Datei-Name?
    /// </summary>
    public string ArchivDateiName { get; set; } = string.Empty;

    /// <summary>
    /// Fehler der gegebenfalls bei dem Vorgang aufgetreten ist?
    /// </summary>
    public Exception Fehler { get; set; } = new();

    /// <summary>
    /// Indikator: Wurde der Vorgang durch den Benutzer abgebrochen?
    /// </summary>
    public bool Abbruch { get; set; } = false;


    public ZIPErgebnis() { }
    public ZIPErgebnis(string verzeichnis, bool erfolgreich, string archivVerzeichnis, string archivDateiName, Exception fehler = null, bool abbruch = false)
    {
        Erfolgreich = erfolgreich; ArchivVerzeichnis = archivVerzeichnis;
        ArchivDateiName = archivDateiName; Fehler = fehler; Abbruch = abbruch;
        Verzeichnis = verzeichnis;
    }
}

Mehr gibt es meines Erachtens nicht.

27.09.2023 - 16:23 Uhr

Hallo!

Ich möchte innerhalb einer asynchronen Abfrage auf eine MessageBox-Result warten, meine Ansätze waren leider nicht erfolgreich.

Ich habe die asynchrone Methode die auf die Antwort der MessageBox warten soll:

private async void PKDataRestoreExecuted(object obj)
{
    WndOpacity = 0.7;                                                   // Transparenz der View absenken

    arWPF.Compression.ZIP.ZIPErgebnis Erg = await RunRückSicherung();   // Rücksicherung ausführen

    if (Erg.Abbruch) ...                                                // Benutzer-Abbruch

    WndOpacity = 1;                                                             // Fenster-Transparenz zurücksetzen
}

Es soll also auf die Methode RunRücksicherung gewartet werden. In ihr habe ich eine MessageBox in der der Benutzer etwas auswählen soll:

private async Task<arWPF.Compression.ZIP.ZIPErgebnis> RunRückSicherung()
{
    arWPF.Compression.ZIP.ZIPErgebnis Erg;                              // Ergebnis-Indikator deklarieren

    switch (MessageBox.Show(Mldg, "Bitte wählen ...", MessageBoxButton.YesNoCancel))
    {
        case MessageBoxResult.No:                                       // Benutzer-Auswahl der Sicherungs-Datei
            Erg = Methode1(); break;                                    // Methode1 ausführen
        case MessageBoxResult.Yes:                                      // Letzte Datensicherung zurücksichern
            Erg = Methode2(); break;                                    // Methode2 ausführen
       default: { Erg = new(); Erg.Abbruch = true; } break;            // Vorgang abbrechen
    }
    await Task.Delay(100);
    return Erg;
}

Zur Laufzeit wird die Methode RunRücksicherung schon aufgerufen, aber ich befinde mich immer noch im Hauptthread und auch die Absenkung der View-Opacity wird nicht ausgeführt.

Woran liegt das und wie kann ich es ändern?

22.09.2023 - 01:32 Uhr

Hallo Th69!

Nein, dass LINQPad kannte ich nicht. Wirkt sehr "aufgeräumt" und für die grafische Anzeige der Ergebnisse ist es sehr gut.

Anmerken möchte ich, dass bereits für das Debuggen der LINQ-Anweisungen die Premium-Freischaltung benötigt wird.

Wenn man es öfter einsetzt, sind 125$ dann auch ok, denke ich. Danke für den Hinweis!

Zu meiner Frage:

Die LeftOuter Relation(en) auf den ersten Eintrag einer Liste sind korrekt!!! (Wer hätte das gedacht 😃 )

join Gen in GenreListe on DVD.Genre.FirstOrDefault() equals Gen.ID     // Inner Join Genre (1. Wert)
into Gruppe2 from Gen2 in Gruppe2.DefaultIfEmpty()                     // Performing Left Outer Join

Der Grund, warum bei mir die DVD-Sort-Einträge mehrfach erzeugt wurden, lag in einem doppelten Eintrag in der (Gesamt) Genre-Liste. Auch wenn ich nur einen Einzigen Genre-Eintrag in der DVD-Genre-Liste eingetragen hatte, wurden 3 DVD-Sort-Einträge erstellt. Der Verweis in der DVD-Genre-Liste war noch nicht einmal auf diesen doppelten Genre-Eintrag (der Gesamtliste).

Auch deinen zweiten Hinweis werde ich mir merken. Mit Oracle-SQL oder MS-SQL bin ich "aufgewachsen". Wenn's komplizierter wird ist das gut zu wissen!

Vielen Dank!!!!

PS.:  Ein RegEx-Evaluator ist übringens im LINQPad auch enthalten!

21.09.2023 - 16:18 Uhr

Hallo!

Ich wusste das mich LINQ noch beschäftigt ...

Ich habe das Problem, dass ich in einer LeftOuter Join Relation, die Relation nur zum ersten Eintrag herstellen möchte:

DVDEinträgeSort =                                                    // DVD-Einträge Sortierungs-Liste
    (from DVD in DVDEinträge                                                // Left Data Source
        join Reg in RegisseurListe on DVD.Regisseur equals Reg.ID              // Inner Join Regisseure
        into Gruppe from Reg2 in Gruppe.DefaultIfEmpty()                       // Performing Left Outer Join
        join Gen in GenreListe on DVD.Genre.FirstOrDefault() equals Gen.ID     // Inner Join Genre (1. Wert)
        into Gruppe2 from Gen2 in Gruppe2.DefaultIfEmpty()                     // Performing Left Outer Join
        join Land in LänderListe on DVD.Land.FirstOrDefault() equals Land.ID   // Inner Join Land (1. Wert)
        into Gruppe3 from Land2 in Gruppe3.DefaultIfEmpty()                    // Performing Left Outer Join

        select new DVDEintragSort(DVD,  
        Reg2 != null ? (Reg2.Nachname + ", " + Reg2.Vorname).Trim() : string.Empty,
        Gen2 != null ? Gen2.Bezeichnung : string.Empty,
        Land2 != null ? Land2.Bezeichnung : string.Empty)
    ).ToList();

Am Beispiel:

In meiner DVD-Klasse sind die Eigenschaften Genre und Land Listen vom Typ List<long>. (Einem DVD-Eintrag können mehere Genre und Länder zugewiesen werden.)

Für das Sortieren der Einträge der DVD-Liste möchte ich immer den ersten Eintrag bei diesen Eigenschaften als Sortier-Wert benutzen. (Pro DVD-Eintrag soll 1 DVD-Sortierungs-Eintrag entstehen!)

Die DVD Sortier-Liste (DVDEinträgeSort)  erzeuge ich durch oben dargestellte LINQ-Abfrage.

Ich dachte, dass ich die korrekte Relation so hergestellt hätte:

     join Gen in GenreListe on DVD.Genre.FirstOrDefault() equals Gen.ID     // Inner Join Genre (1. Wert)

Den 1. Eintrag (der Liste) der Genre-Eigenschaft soll mit einem Eintrag der Genre-Liste übereinstimmen.

Die Relation bezieht sich doch nur auf den 1. Eintrag!

Im Ergebnis der Abfrage werden mir aber für alle Genre-Einträge des DVD-Eintrages separate Sortierungs-Einträge zurückgegeben. Siehe Bild (Das es sich um den gleichen DVD-Eintrag handelt, sieht man an der Regisseur-Bezeichnung)

Dieser(der Eine) DVD-Eintrag hat eine Liste mit 2 Genre-Einträgen.

Wenn es wenigstens 2 Sortierungs-Einträge mit dem 1. Genre wären, würde ich es ja noch verstehen, aber es ist die DVD-Genre-Liste voll aufgelöst nach den Genre-Einträgen auf die sie verweisen.

In SQL würde ich das mit einem SUBSELECT realisieren.

19.09.2023 - 23:01 Uhr

Hallo!

Ich habe es jetzt so nach dem Ausschluss-Verfahren zwar hinbekommen, aber ... wie schon eingangs gesagt, muss ich mich damit beschäftigen.

DVDEinträgeSort =                                                                       // DVD-Einträge Sortierungs-Liste
    (from DVD in DVDEinträge                                        // Left Data Source
     join Reg in RegisseurListe on DVD.Regisseur equals Reg.ID      // Inner Join Regisseure
     into Gruppe from Reg2 in Gruppe.DefaultIfEmpty()               // Performing Left Outer Join
     join Gen in GenreListe on DVD.Genre.First() equals Gen.ID      // Inner Join Genre (1. Wert)
     into Gruppe2 from Gen2 in Gruppe2.DefaultIfEmpty()             // Performing Left Outer Join

      select new DVDEintragSort(DVD,  
            Reg2 != null ? (Reg2.Nachname + ", " + Reg2.Vorname).Trim() : string.Empty,
            Gen2 != null ? Gen2.Bezeichnung : string.Empty)
      ).ToList();
  1. Join mit Gruppe, dann 2. Join mit Gruppe, dann select.

Ob das der beste Weg ist? Ich bleib (notgedrungen) dran!

19.09.2023 - 21:53 Uhr

Hallo Abt!

Ich merke schon, dass ist wieder ein Feld, mit dem ich mich noch (viel) mehr beschäftigen muss! <schnauf> 😃

Aber steigern kann ich mich schon mit der nächsten Frage:  😃  😃

Wie setze ich denn mehrere LeftOuter Join's in einer Abfrage ???

DVDEinträgeSort =                                                                       // DVD-Einträge Sortierungs-Liste 
  (from DVD in DVDEinträge                                        // Left Data Source
   join Reg in RegisseurListe on DVD.Regisseur equals Reg.ID      // Inner Join Regisseure
   join Gen in GenreListe on DVD.Genre.First() equals Gen.ID      // Inner Join Genre
   into Gruppe from Reg2 in Gruppe.DefaultIfEmpty()               // Performing Left Outer Join

   select new DVDEintragSort(DVD,  Reg2 != null ? (Reg2.Nachname + ", " + Reg2.Vorname).Trim() : string.Empty), Gen.Bezeichnung).ToList();

Im Beispiel möchte ich aus der Regisseur und der Genre-Liste Daten (per LeftOuter Join) abfragen.

In Reg2 kommt jetzt der 2. Inner Join (Genre) an und Reg fehlt?.

18.09.2023 - 18:35 Uhr

Hallo Th69!

Das Ergebnis der Relation wird also erst noch einmal in einer Variablen gespeichert, die durch DefaultIfEmpty() auch null werden kann und dann wird das Select (auf Grundlage der Variable) ausgeführt:

DVDEinträgeSort =                                                                 // DVD-Einträge Sortierungs-Liste aktualisieren
                  (from DVD in DVDEinträge                                        // Left Data Source
                   join Reg in RegisseurListe on DVD.Regisseur equals Reg.ID      // Inner Join
                   into Group from Reg2 in Group.DefaultIfEmpty()                 // Performing Left Outer Join
                   select new DVDEintragSort(DVD,  Reg2 != null ? (Reg2.Nachname + ", " + Reg2.Vorname).Trim() : string.Empty)).ToList();

Das ist schon gewöhnungsbedürftig! 😉   Warum gibt es (in LINQ) nicht gleich einen LeftOuterJoin??? (Die Performing Left Outer Join - Zeile ist ja immer die Gleiche)

Vielen Dank! für deine Lösung, da war ich zu ungeduldig!

18.09.2023 - 17:34 Uhr

Hallo Abt!

Das ist ja wieder mal so ein Denkfehler!!! Ja, ich muss natürlich den gesamten Eintrag sehen und nicht die Relationsverknüpfung und der Default eines Objektes ist dann natürlich null.

Auch das Debuggen ist ganz interessant! Allerdings gestaltet sich das Vorhaben bei rund 2000 Regisseur und 5000 DVD-Einträgen nicht so einfach. Mit Take() kommt man dann etwas weiter und sieht, dass erst alle Relations-Einträge komplett eingelesen werden und danach die DVD-Einträge iteriert werden und dieses Ergebnis dann im Select zurückgegeben wird.

Ich hatte dann folgenden Ansatz:

DVDEinträge = (List<DVDEintrag>)Erg_LoadDVDEinträgeListe.Daten;                   // Eingelesene DVD-Einträge-Liste übernehmen
DVDEinträgeSort = (from DVD in DVDEinträge
                   join Reg in RegisseurListe.DefaultIfEmpty() on DVD.Regisseur equals Reg.ID
                   select new DVDEintragSort( DVD, Reg != null ? (Reg.Nachname + ", " + Reg.Vorname).Trim() : "?")).ToList(); 

habe aber nie die 5.180 DVDEinträge in der DVDEinträgeSort-Liste erhalten (es waren immer 5.156 die 24 ohne Regisseur fehlen).

Ist also immer noch nicht ein LeftOuterJoin!

Ich werde mir mal den von Th69 vorgeschlagenen Link ansehen...

Vielen Dank für deine Hinweise!

18.09.2023 - 13:36 Uhr

Hallo!

Ich dachte das wäre eine LeftJoin-Relation:

DVDEinträgeSort = (from DVD in DVDEinträge
                   join Reg in RegisseurListe.DefaultIfEmpty() on DVD.Regisseur equals Reg.ID
                   select new DVDEintragSort( DVD, new string((Reg.Nachname + ", " + Reg.Vorname).Trim()))).ToList(); 

Reg.ID und DVD.Regisseur sind vom Type long. Der Default-Wert müsste doch 0 sein.

Es werden jedoch alle Einträge der DVDEinträge-Liste die bei Regisseur den Wert 0 haben nicht übernommen.

Die RegisseurListe besitzt keinen Eintrag mit der ID 0.

Warum wirkt dann DefaultIfEmpty() nicht?

18.09.2023 - 13:24 Uhr

Hallo T-Virus!

Ich sage jetzt mal Ahaaa! 😃 Die Hilfs-Klasse hätte ich nicht mit DataModel gleichgesetzt.

Trotzdem vielen Dank!

18.09.2023 - 12:46 Uhr

Hallo T-Virus!

Irgendwie reden wir aneinander vorbei.

Ich habe doch ein Model für die View. Meine Eigenschaft die ich in der View binde ist die DVDEinträgeView eine ICollectionView Darüber steure ich die Sortierung!  Mir ist noch ein bisschen unklar, wie du mit der separaten Eigenschaft eine Sortierung erzeugen willst?

Die ICollectionView hat als Source eine Liste (Enumerable) und diese Liste muss ein Feld enthalten, nach dem sortiert werden kann. Ich habe als Ausgangspunkt immer zwei Listen die über eine Relation verbunden sind. Also muss ich als erstes eine View erzeugen, die  mir das RelationsFeld (in der gewünschten Form aufbereitet) zur Verfügung stellt.

Ich habe das jetzt so gelöst, dass ich 1.  einen separaten Datentype erstelle, der mir den ursprünglichen Datentype durch die Relations-Feld(er) erweitert:

/// <summary>
/// Hilfs-Klasse zur Strukturierung von DVD-Einträgen für die Sortierung.
/// </summary>
public class DVDEintragSort
{
    /// <summary>
    /// DVD-Eintrag.
    /// </summary>
    public DVDEintrag DVDEintrag { get; set; }

    /// <summary>
    /// Der Name des Regisseurs des DVD-Eintrages (aufbereitet für eine Sortierung).
    /// </summary>
    public string RegisseurBezeichnung { get; set; }

    public DVDEintragSort(DVDEintrag dVDEintrag, string regisseurBezeichnung = "")
    {
        DVDEintrag = dVDEintrag; RegisseurBezeichnung = regisseurBezeichnung;
    }
}

2. Eine Eigenschaft die ich als Quelle für die ICollectionView benutze deklariere:

/// <summary>
/// Liste der DVD-Einträge mit aufgelösten Relationen
/// </summary>
private List<DVDEintragSort> DVDEinträgeSort { get; set; }

und 3. in diese die ursprünglichen Daten (DVD-Liste) und die Daten der aufgelösten Relationen übernehme:

DVDEinträgeSort = 
(from DVD in DVDEinträge
                   join Reg in RegisseurListe on DVD.Regisseur equals Reg.ID
                   select new DVDEintragSort( DVD, new string((Reg.Nachname + ", " + Reg.Vorname).Trim()))).ToList(); // DVD-Eintrag + Relation(en) 

und diese als Quelle der ICollectionView zur Verfügung stelle:

CVSDVDEinträge.Source = DVDEinträgeSort;    // (Neue) Liste der DVD-Einträge der CVS zuordnen
DVDEinträgeView.Refresh();                  // Daten-View aktualisieren

Jetzt kann ich in der Sortierung, sowohl Eigenschaften die in der originalen DVD-Liste als auch Eigenschaften die durch die Relation erstellt worden sind, ansprechen/benutzen.

    string FeldName;                                                                                  // Feld-Name deklarieren
    switch (SortierungsFeldDVDÜbersicht)
    {
        case EnuSortierungsFelderDVDÜbersicht.Rating:  FeldName = "DVDEintrag.ERating"; break;
        case EnuSortierungsFelderDVDÜbersicht.Genre: FeldName = "DVDEintrag.Genre"; break;
        case EnuSortierungsFelderDVDÜbersicht.Land: FeldName = "DVDEintrag.Land"; break;
        case EnuSortierungsFelderDVDÜbersicht.Jahr: FeldName = "DVDEintrag.Jahr"; break;
        case EnuSortierungsFelderDVDÜbersicht.Dauer: FeldName = "DVDEintrag.Dauer"; break;
        case EnuSortierungsFelderDVDÜbersicht.Regisseur: FeldName = "RegisseurBezeichnung"; break;
        default: FeldName = "DVDEintrag.Titel"; break;
    }

    ListSortDirection FeldSortierung = SortierungsRichtungDVDÜbersicht == EnuSortierungRichtung.Aufsteigend // Sortierung aus Aufzählungs-Wert ableiten
                                        ? ListSortDirection.Ascending : ListSortDirection.Descending;
    SortDescription sdDatenView = new(FeldName, FeldSortierung);                                            // Sortierung (aus den Sortier-Eintrag Werten) erstellen
            
    CVSDVDEinträge.SortDescriptions.Add(sdDatenView);                                                       // Sortierung auf die CollectionViewSource übertragen (Die Sortierung selbst wird erst mit der Refresh-Methoder der DatenView ausgeführt!)                

Durch die Hilfsklasse ist dann auch eine eindeutige Zuordnung in XAML wieder gegeben:

<ListView.View>
    <GridView>
        <!-- DVD-Nr.  = über den DVD-Eintrag -->
        <GridViewColumn Header="DVD-Nr." >
            <GridViewColumn.CellTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding DVDEintrag.Nummer}" Foreground="Blue"
/>
                </DataTemplate>
            </GridViewColumn.CellTemplate>
        </GridViewColumn>
    ...
        <!-- Regisseur  = über die Relation -->
        <GridViewColumn>
            <GridViewColumn.CellTemplate>
                 <DataTemplate>
                    <TextBlock Text="{Binding RegisseurBezeichnung}" 
/>
                </DataTemplate>
            </GridViewColumn.CellTemplate>
        </GridViewColumn>
18.09.2023 - 01:48 Uhr

Hallo T-Virus!

Danke für deine Überlegung(en)!

Mit dem fehlerhaften Datenmodell könntest du vielleicht recht haben, aber ganz so einfach ist es dann doch nicht ...

Mal zur Ausgangssituation:

Ich verwende in dieser Applikation keine relationale Datenbank sondern deserialisiere (mehere) XML-Dateien in Listen (unterschiedlichem Daten-Typs).

Als Beispiel eine Liste vom Datentyp DVDEintrag List<DVDEintrag> und eine Liste vom Datentyp Regisseur List<Regisseur>.

Der Datentype DVDEintrag hat (50 andere Eigenschaften) und eine Eigenschaft Regisseur (vom Type long).

Der Datentype Regisseur hat (10 andere Eigenschaften) und eine Eigenschaft ID (vom Type long).

In der Eigenschaft Regisseur vom DVDEintrag wird der Wert der ID-Eigenschaft eines Regisseur-Eintrages gespeichert.

Ganz klassische N:1 Relation.

Die DVD-Liste dient als Source einer ICollectionView, die ich in der View der Applikation, als Source an eine ListView binde.

/// <summary>
/// Liste der DVD-Einträge.
/// </summary>
private List<DVDEintrag> DVDEinträge { get; set; }

internal CollectionViewSource CVSDVDEinträge { get; set; } = new CollectionViewSource();    // CVS deklarieren

/// <summary>
/// View der DVD-Einträge Liste.
/// </summary>
public ICollectionView DVDEinträgeView { get => CVSDVDEinträge.View; }

...

CVSDVDEinträge.Source = DVDEinträge;    // (Neue) Liste der DVD-Einträge der CVS zuordnen
DVDEinträgeView.Refresh();              // Daten-View aktualisieren

Die ICollectionView benutze ich zum Filtern und Sortieren der DVD-Einträge. Und genau an dieser Stelle kann ich beim Sortieren die Relation (zu den Regisseur-Einträgen) nicht abbilden.

Jetzt zu deinen Überlegungen:

  • Die richtige Property → Habe ich eigentlich. Regisseur-Eigenschaft der DVD-Liste.
  • Leg doch eine (Neue) an → Was soll da drin stehen? Der Name des Regisseurs? Dann hätte ich Datenredundanz und müsste die Aktualisierung steuern
  • Warum brauchst du eine Methode? → Diese soll die Relation abbilden und den Namen des Regisseurs für die Sortierung "holen"

Den einzigen Ausweg den ich zur Zeit sehe ist, der Source für die ICollectionView eine View zu übergeben (in der die Regisseur-Name Relation bereits aufgelöst ist).

object MyDVDEintragView = from myDVDListe in DVDListe
                          join myRegisseurListe in MyRegisseurListe on myDVDListe.Regisseur equals myRegisseurListe.ID
                          select new { myDVDListe.*, myRegisseurListe.Name };

CVSDVDEinträge.Source = MyDVDEintragView ;    // (Neue) Liste der DVD-Einträge der CVS zuordnen

Ich habe aber das Gefühl, dass das nicht der richtige Weg ist. Ich habe ja noch viele Relationen ...

Und ich habe auch keinen richtigen Daten-Type mehr, die View ist ja vom Type: object!

17.09.2023 - 14:48 Uhr

Hallo!

Wie sortiert man denn das Feld einer Relation?

Ich habe eine (Basis) Liste die ein Feld (Regisseur) enthält, dass eine ID auf einen Listen-Eintrag in einer anderen Liste (Regisseur-Liste), enthält.

Jetzt möchte ich die Basis-Liste nach den Regisseur-Feld sortieren. Natürlich möchte ich nicht die Regisseur-IDs sortieren, sondern die den ID's entsprechenden Regisseur-Einträge nach den Regisseur-Namen.

cvsDVDEinträge.SortDescriptions.Clear();                             // Alle/Alte Sortierung (immer) löschen

SortDescription sdRegisseur = new("Regisseur-ID", FeldSortierung);		// Sortierung nach Regisseur

cvsDVDEinträge.SortDescriptions.Add(sdRegisseur);                  // Sortierung auf die CollectionViewSource übertragen          

Wie gebe ich denn eine Funktion (die in meinem Beispiel aus der Regisseur-ID den Regisseur-Namen ermittelt) in der SortDescription an?

SortDescription sdRegisseur = new( GetRegisseurName(Regisseur-ID, Regisseur-Liste), FeldSortierung);		// Sortierung nach Regisseur
18.08.2023 - 14:39 Uhr

Hallo Th69!

Danke! für deinen Hinweis!

Das hatte ich schon am Anfang ... Ergibt den gleichen Fehler, deshalb habe ich es mit einem "festem" Namen probiert, dass erschien mir sicherer (um andere Fehler auszuschließen).

Ich habe jetzt erst einmal eine Lösung, die ist aber bestimmt tuning fähig!  😃

Innerhalb des CommandParameter-Bindings kann man auch ein PlacementTarget setzen!!!

<ContextMenu>
    <MenuItem Header="Nachrichten-Bereich anzeigen" 
                Command="{Binding SBNachrichtenBereichAnzeigenCommand}"
                CommandParameter="{Binding PlacementTarget, RelativeSource={
                RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"/>
</ContextMenu>

Dadurch erreicht man, dass der Command-Parameter einen Verweis auf die Status-Bar (welche sich wiederum ganz normal im VisualTree und LogicalTree befindet) zurückgibt.

Dadurch kann man mit Visual Tree Helpers die View und dadurch wieder die View-Resource(n) bestimmen.  Ziel erreicht.

private async void SBNachrichtenBereichAnzeigenExecuted(object obj)
{
    if(obj is System.Windows.Controls.Primitives.StatusBar sBar)                                    // StatusBar "holen"
    {                
        Window view = arWPF.WPF_Tools.WPF_Tools.VisualTreeHelpers.FindAncestor<Window>(sBar);       // View "holen"
        if (view != null && view.Resources is ResourceDictionary resourcen)                         // Resourcen "holen"
        {
            bool fertig = await VMDVDVerwaltung.SetDatenLadenBorder(resourcen, "Auf", 1000);        // Daten-Laden Border Animation ausführen
        }
    }
}

Nochmals vielen Dank  Th69 für deine Unterstützung!

18.08.2023 - 13:33 Uhr

Hallo Th69!

Ich weiß jetzt zwar nicht, ob das alles so korrekt ist ... Ich habe einen MultiBinding Converter implementiert:

<StatusBar.ContextMenu>
    <ContextMenu>
        <MenuItem Header="Nachrichten-Bereich anzeigen" Command="{Binding SBNachrichtenBereichAnzeigenCommand}">
            <MenuItem.CommandParameter>
                <MultiBinding Converter="{StaticResource ResourcenKonverter}">
                    <Binding Path="Resources" ElementName="wndMainWindow" />
                    <Binding Path="." />
                </MultiBinding>
            </MenuItem.CommandParameter>
        </MenuItem>
    </ContextMenu>
</StatusBar.ContextMenu>

Der im Debugger auch "aufgerufen" wird, aber das Problem wird im Prinzip nur an die Stelle, wo ich das Einzelbindig auf die Resource binde verschoben.

MenuItem.CommandParameter: Object	Quelle nicht gefunden: ElementName=wndMainWindow	

Interessant: Das Einzelbinding (Path=".") verweist im Converter auf das View-Model (siehe Bild)

Binde ich den MultiConverter falsch?

18.08.2023 - 11:06 Uhr

Hallo!

Ich übergebe einem Command als Parameter eine Resource (die MainWindows-Resource).

Solang ich dieses Command außerhalb eines Context-Menüs aufrufe ist alles in Ordnung. Der Command-Parameter wird übergeben und das Command ausgeführt.

Sobald ich das Command aber innerhalb eines Context-Menüs ausführe:

<StatusBar.ContextMenu>
    <ContextMenu>
        <MenuItem Header="Nachrichten-Bereich anzeigen" Command="{Binding SBNachrichtenBereichAnzeigenCommand}" 
                    CommandParameter="{Binding Path=Resources, ElementName=wndMainWindow}"/>
    </ContextMenu>
</StatusBar.ContextMenu>

wird mir ein Bindungsfehler angezeigt:

MenuItem.CommandParameter:  Quelle nicht gefunden: ElementName=wndMainWindow

Dies hat seine Ursache in der Tatsache, dass das ContextMenu separate Fenster mit eigenem VisualTree und LogicalTree hat.

Eine vorgeschlagene Lösung ist, das CommandTarget manuell an das übergeordnete ContextMenu zu binden:

<StatusBar.ContextMenu>
    <ContextMenu>
        <MenuItem Header="Nachrichten-Bereich anzeigen" Command="{Binding SBNachrichtenBereichAnzeigenCommand}" 
                    CommandParameter="{Binding Path=Resources, ElementName=wndMainWindow}"
                    CommandTarget="{Binding Path=PlacementTarget, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type ContextMenu}}}"/>
    </ContextMenu>
</StatusBar.ContextMenu>

scheinbar hat das aber keine Auswirkung(en) auf den Command-Parameter. Der Bindungs-Fehler bleibt bestehen.

Wie könnte ich denn meinen Command-Parameter an die Resource(n) des MainWindow binden?

11.08.2023 - 11:43 Uhr

Sorry, Sorry, Sorry ...

Die Methode die den Daten-Thread aufruft, darf nicht als static deklariert werden:

private async System.Threading.Tasks.Task<List<Hausblatt>> RefreshHausBlätterListe(string Pfad, int Umfang, bool ShowVerarbeitung)

dann kann die VM_Aktualisierungs-Methode auch als Instanz-Methode deklariert werden und damit stehen in dieser wieder die VM-Eigenschaften  zur Verfügung.

private void VM_Aktualisierung(double verarbeitungsStand)
11.08.2023 - 11:23 Uhr

Hallo!

Ich starte im VM über async/await einen Daten-Thread:

private async void ExtraktExecuted(object obj)
{
    ...   
    ListHausBlätter = await System.Threading.Tasks.Task.Run(() => RefreshHausBlätterListe(App.PfadHausblätter, MaxDateien, ShowVerarbeitung));

im Daten-Thread möchte ich eine VM-Eigenschaft aktualisieren:

--- Daten-Thread ----------
private static async System.Threading.Tasks.Task<List<Hausblatt>> RefreshHausBlätterListe(string Pfad, int Umfang, bool ShowVerarbeitung)
{
    ...
    Application.Current.Dispatcher.Invoke(new Action(() => VM_Aktualisierung(VerarbeitungsZähler * 100 / HausBlätterListe.Count)));
}

--- GUI-Thread ------------
private static void VM_Aktualisierung(double verarbeitungsStand)
{
            
    // VerarbeitungStand = 50;
}

In der Methode VM_Aktualisierung befinde ich mich wieder im GUI-Thread. Dadurch das ich die Methode als static deklariert habe, fehlen mir allerdings alle Eigenschaften (es werden nur die Methoden im VS angezeigt), siehe Bild.

Wenn ich die VM_Aktualisierung-Methode als Instanz-Methode deklariere wird wiederum der Fehler, das ein Objekt-Verweis erforderlich ist, geworfen:

error CS0120: Für das nicht statische Feld, die Methode oder die Eigenschaft "VMHausblätter.VM_Aktualisierung(double)" ist ein Objektverweis erforderlich.

Wie kann ich denn nun eine VM-Eigenschaft, aus dem Daten-Thread heraus, aktualisieren?

10.08.2023 - 11:30 Uhr

Hallo!

Weil es unheimlich nervt ...

Seit geraumer Zeit (evt. durch ein VS-Update?) werden Compiler-Fehler nicht mehr sofort in der Fehlerliste angezeigt, sondern erst beim explizitem Projekt-Mappe neu erstellen. Die Fehler werden dann nur noch als Maker (ausgefüllter Pfeil) im linken Bereich markiert und in der Ausgabe beschrieben. Der Effekt tritt erst nach einer gewissen Zeit, nach dem Neustart des PC's auf.

Die elektronische Glaskugel sagt , dass dieses Verhalten durch ein deaktivieren des Schreib-Caches des Datenträgers behoben werden könne, aber mein VS ist scheinbar resistent gegenüber dieser Lösung (Schreib-Cache ist deaktiviert und das Verhalten ändert sich nicht!).

Den Standard: Cache ist aktiv, habe ich geändert (und den PC wieder neu gestartet) siehe Bild.

Habe auch das .vs-Verzeichnis mal gelöscht, weil dies in der Lösung mit angegeben war, keine Besserung.

Habt ihr noch einen Hinweis?

Anmerkung:

An der Hardware des PC wurden keine Änderungen vorgenommen.

Die Energie-Optionen des PC's wurden nicht verändert und stehen auf höchste Leistung.

06.08.2023 - 14:30 Uhr

Hallo dannoe

Das es da einige gibt, ist mir bewußt, habe lieber etwas lokales benutzt. Deine Seite sieht aber wirklich sehr gut aus!

Nochmals vielen Dank!!!

06.08.2023 - 14:23 Uhr

Das ist in RegExLab aber sehr versteckt! Es könnte sich ja wenigstens die Hintergrundfarbe ändern, wenn es mehere Treffer gibt.

Durch "scrollen" der Pattern-Zeile werden die weiteren Ergebnisse angezeigt und diese erhalten dann auch eine andere Hintergrundfarbe (siehe Bild).

Sorry.

06.08.2023 - 14:13 Uhr

Hallo Wilfried!

Danke für deine Links.

06.08.2023 - 14:08 Uhr

Hallo dannoe!

Das ist ja der Hammer! Ich habe das "nur" mit dem hier im Forum entwickelten regexlab getestet und da wird nur ein Treffer angezeigt (Siehe Bild).

Da ich im Moment nicht an ein VS komme kann ich es ad hoc nicht nachvollziehen, aber ich bin mir sicher, dass deine Prüfung richtig ist!

Vielen Dank!!!

06.08.2023 - 11:23 Uhr

Hallo!

Man hätte es viel schneller ausprogrogrammiert, aber ... irgendwann verstehe ich auch noch RegEx.

Ich möchte aus einnem Text alle Jahreszahlen (im Bereich von 18xx ... 20xx) extrahieren.

Wenn der Text z.B. "ab1998ist 2000gg1898" lautet , dann müsste ich 3 Treffer für 1998, 2000 und 1898 erhalten.

Einen Treffer erhalte ich durch:

(?:(?:18|19|20)[0-9]{2})

Wie sage ich aber "RegEx" das er alle Treffer findet?

Erster Treffer:
(?:(?:18|19|20)[0-9]{2})


Letzter Treffer:
(?:(?:18|19|20)[0-9]{2})*$

(Soll mit 18, 19, oder 20 beginnen und dann 2 Ziffern folgen) *   funktioniert so nicht.

Die Jahreszahlen können auch unmittelbar innerhalb eines Wortes eingeschlossen sein!

In dem Text stehen auch andere Zahlen mit mehr oder weniger Stellen, die nicht erfasst werden sollen.

30.07.2023 - 15:15 Uhr

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 ...  😃

30.07.2023 - 13:04 Uhr

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!

29.07.2023 - 23:17 Uhr

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!

29.07.2023 - 22:01 Uhr

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!!!

29.07.2023 - 21:33 Uhr

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?

29.07.2023 - 20:45 Uhr

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?

29.07.2023 - 16:40 Uhr

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
}
29.07.2023 - 14:50 Uhr
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?
29.07.2023 - 14:47 Uhr
        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 ...

29.07.2023 - 14:45 Uhr

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

29.07.2023 - 13:59 Uhr

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.

29.07.2023 - 13:10 Uhr

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
        }        
     }
 }  
        
29.07.2023 - 12:27 Uhr

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!

29.07.2023 - 11:44 Uhr

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.

29.07.2023 - 10:27 Uhr

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.

28.07.2023 - 11:38 Uhr

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!

27.07.2023 - 17:41 Uhr

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!

27.07.2023 - 17:33 Uhr

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)
}
27.07.2023 - 11:51 Uhr

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 
    }
}
27.07.2023 - 11:01 Uhr

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?

26.07.2023 - 08:58 Uhr

Hallo Palladin007, Hallo Th69!

Sorry, mit weiteren Antworten hatte ich garnicht mehr gerechnet!

[Flags]
enum MyEnum : long
{
    None = 0,
    A = 1 << 0, // 1
    B = 1 << 1, // 2
    C = 1 << 2, // 4
    D = 1 << 3, // 8
}

Das ist ja wirklich sehr übersichtlich, besonders bei "höheren" Bits!!! (Da saß ich dann mit dem Taschenrechner da ...)

Vielen Dank für eure weiteren Hinweise!

16.07.2023 - 11:06 Uhr

Einen zweiten Nachtrag muss ich auch noch machen. Th69 hat in seiner Antwort die Lösung (komprimiert) genau so angegeben.  Leider kann man hier eine Antwort nicht als Lösung deklarieren.

Also auch vielen Dank an dich!!!

15.07.2023 - 21:40 Uhr

Verzeichnis Auswahl-Dialog (Assembly)

Der Kern der Verzeichnis-Anzeige basiert auf dem API32 SHBrowseForFolder – Methoden-Aufruf.

Ihm wird als Parameter eine (parametrisierte) BROWSEINFO-Struktur übergeben. Diese steuert alle Optionen der Anzeige des Windows Verzeichnis-Dialoges:

API32.BFFCALLBACK _callbackKeepAlive = BrowseCallbackProc;      // CallBack-Methode (für die BrowseInfo-Struktur) initalisieren

IntPtr buffer = Marshal.AllocHGlobal(MAX_PATH);                 // Speicherplatz für das Rückgabe-Verzeichnis reservieren

API32.BROWSEINFO bi = new();                                    // Browse-Info Struktur initalisieren
bi.pidlRoot = rootVerzeichnisPIDL;                              // Der PIDCList-Eintrag auf das Root-Verzeichnis übergeben
bi.hwndOwner = hwndDialogFenster;                               // Zeiger auf das Dialog-Fenster übergeben
bi.pszDisplayName = buffer;                                     // Dem Zeiger auf die DisplayName-Eigenschaft den Zeiger auf den reservierten Speicher übergeben
bi.lpszTitle = Title;                                           // Den Titel des Verzeichnis-Dialoges übergeben
bi.ulFlags = publicOptions | (int)API32.BFFStyles.NewDialogStyle; // Den Flags alle eingestellten Dialog-Optionen und das neue Dialog-Style-Flag übergeben
bi.lpfn = _callbackKeepAlive;                                   // CallBack Methode zum selektieren des Select-Verzeichnis-Eintrages im Dialog-Fenster (TreeView)

pidlRet = API32.SHBrowseForFolder(ref bi);                      // Verzeichnis-Dialog anzeigen. Das Ergebnis der Auswahl ist ein (Zeiger auf einen) PIDCList-Eintrag auf das ausgewählte Verzeichnis.
 

An dieser Stelle gibt es eigentlich nur einen Knackpunkt. Das ist das selektieren des Eintrages das dem Select-Verzeichnis zugeordnet werden soll. Ein iterieren durch das TreeView des Auswahl Verzeichnis-Dialog-Fensters scheitert an der Möglichkeit sich im TreeView immer weiter (rekursiv) fortzubewegen. Die Lösung stellt die Definition einer CallBack-Methode (hier BrowseCallbackProc), die man ebenfalls in der BROWSEINFO-Struktur zuordnen kann (bi.lpfn=), dar.

In ihr kann man abfragen, ob die Initalisierung des Dialog-Fensters abgeschlossen wurde und dann eine BFFM_SETSELECTIONW Nachricht an das Dialog-Fenster senden in dessen Parameter das Select-Verzeichnis (sogar als String!) angegeben ist, um den passenden TreeView-Eintrag zu selektieren. Gleichzeitig verschiebt diese Nachricht den selektierten Eintrag noch in den sichtbaren Bereich, aber das funktioniert nicht ganz so zuverlässig.

private int BrowseCallbackProc(IntPtr hwnd, uint msg, IntPtr lParam, IntPtr lpData)
{
    if (msg == API32.BFFM_INITIALIZED)  // Wenn die Initalisierung des Dialog-Fensters abgeschlossen wurde ...
    {
        _ = API32.SendMessage(hwnd, API32.BFFM_SETSELECTIONW, 1, _selectedPath) // Das TreeView-Item mit der Bezeichnung des Select-Verzeichnisses selektieren
    }
    return 0;                                       // Immer das Flag: Erfolgreich zurückgeben
}

Am Ende muss man nur noch den von der SHBrowseForFolder Methode zurückgegebenen PDCList-Eintrag  auswerten, dessen Wert auf das ausgewählte Verzeichnis, verweist.

if (pidlRet == IntPtr.Zero) return MessageBoxResult.Cancel;     // Wenn der Benutzer den Dialog abgebrochen hat, dann ein Abbruch-Ergebnis zurückgeben

directoryPath = GetVerzeichnisString(pidlRet);                    // Ausgewähltes Verzeichnis aus dem Rückgabe PIDCList-Eintrag ermitteln

Wie die Parametrisierung der BROWSEINFO-Struktur im Detail erfolgt oder sonstige Details könnt ihr ebenfalls dem veröffentlichten Projekt: Projekt VerzeichnisDialog Assembly.zip, entnehmen.

Beide Projekte sind ausführlichst dokumentiert.

Für die Praktiker: Hier die GUI zum Testen als Programm (GUI.zip) und den Verzeichnis Auswahl-Dialog als Assembly-DLL (Assembly.zip).

Damit hier nicht der falsche Eindruck entsteht! Das habe ich nicht alles selbst "entwickelt" sondern nur zusammengetragen und für mich so lang aufbereitet, bis es (für mich) passt!!!

Besonderen Dank an charlieface von stackoverflow

15.07.2023 - 21:26 Uhr

Hallo!

Viel später als gedacht, aber jetzt ist es doch erledigt. Mein Verzeichnis Auswahl-Dialog (Framework: .NETCore Assembly).

Der Verzeichnis Auswahl-Dialog hat 4 Überladungen für den Aufruf.

Sie unterscheiden sich in der Art wie die Root und Select-Verzeichnis - Eigenschaft übergeben wird (Auswahl-Art). Diese Eigenschaften können als String (spezifiziert einen Verzeichnispfad) oder als Aufzählungswert der Aufzählung: System.Environment.SpecialFolder übergeben werden (Spezial-Verzeichnis). Über die Spezial-Verzeichnisse können Einträge im Verzeichnis-Dialog selektiert werden, die keine direkte Entsprechung im Dateisystem haben. (Desktop als oberste Ebene des Verzeichnis-Dialoges oder Arbeitsplatz als Sammelbehälter für weitere Spezial-Verzeichnisse und Datenträger).

MessageBoxResult ShowDialog(string, string)	// Root + Select-Verzeichnis als Verzeichnis

MessageBoxResult ShowDialog(Environment.SpecialFolder, string)	// Root als Spezial-Verzeichnis / Select als Verzeichnis

MessageBoxResult ShowDialog(Environment.SpecialFolder, Environment.SpecialFolder) // Root + Special – Verzeichnis als Spezial-Verzeichnis

MessageBoxResult ShowDialog(string, Environment.SpecialFolder)	// Root als Verzeichnis und Select-Verzeichnis als Spezial-Verzeichnis

Zur leichteren Handhabung der Spezialverzeichnisse habe ich in der VerzeichnisDialog-Klasse eine Liste der Spezialverzeichnisse implementiert, durch die eine einfache Auswahl (auch mehrerer Spezialverzeichnisse = Gruppe) über Flags und die direkte Zuordnung einer Beschreibung des Spezial-Verzeichnisses zu der Spezialverzeichnis-Aufzählung ermöglicht wird.

Die Liste kann über die Methode:

GetSpezialVerzeichnisse (VerzListenFlag Flags) 

Type: ObservableCollection<SpezialVerzeichnisEintrag>

abgerufen werden. Weitere Details zur Handhabung der Spezial-Verzeichnis Liste könnt ihr dem hier veröffentlichten Projekt: Projekt Verzeichnis Dialog GUI.zip, entnehmen.

An dieser Stelle ein paar Hinweise:

- Der Rückgabe-Type der ShowDialog-Methode (der VerzeichnisDialog-Klasse) ist eine, der  System.Windows.MessageBoxResult – Aufzählung adäquate Aufzählung, in der VerzeichnisDialog-Klasse. (Dies ist notwendig um konsequent nur im Microsoft.NETCore.App Framework  zu bleiben.)

- Das selektierte Verzeichnis wird nur dann (zuverlässig) in den sichtbaren Bereich verschoben, wenn  die Auswahl-Art des Root- und Select – Verzeichnisses gleich sind und sich das Select-Verzeichnis unterhalb des Root-Verzeichnisses befindet.

- Wenn die Option OnlyFilesystem (Standard: Ja) auf Nein gestellt wird und ein Eintrag, der keine Entsprechung im Datei-System hat ausgewählt wird, wird bei der Bestätigung [OK-Schaltfläche] automatisch der Status: Abbruch (MessageBoxResult.Cancel) zurückgegeben.

- Wenn das Root-Verzeichnis durch die Auswahl-Art: Dateisystem spezifiziert wird, dann können die übergeordneten Spezial-Verzeichnisse: Desktop und Arbeitsplatz nicht als Select-Verzeichnis  angegeben werden. Wenn sie doch ausgewählt werden, wird für diese automatisch das Benutzer-Dokumenten-Verzeichnis  selektiert.

Die Bilder zu den einzelnen Varianten der Root- und Select- Verzeichnis-Auswahl stehen ebenfalls noch einmal hier im Verzeichnis Bilder zur Verfügung.