Laden...

Forenbeiträge von perlfred Ingesamt 261 Beiträge

23.04.2024 - 10:27 Uhr

Hallo Abt!

Kann Microsoft nicht so ein einfaches, intuitives Beispiel wie deins, bei der Beschreibung von SelectSingleNode verwenden? Nein, da muss schon wieder gefiltert werden und eine Abfrage über untergeordnete Knoten (descendand) dargestellt werden. Als zweites Beispiel vielleicht ganz interessant, aber um das Prinzip zu verstehen nicht gerade hilfreich. (Anmerkung: Ich hatte mir die Dokumentation schon angesehen, aber nicht verstanden.)

Der Fehler bei mir war also: Das ich einen NameSpace Alias definieren muss und diesen im Pfad (bei jedem Knoten) voranstellen muss.

Jetzt habe ich deinen Hinweis:

aber der Zugriff ist definitiv so nich korrekt

verstanden.

doc = new XmlDocument();                                                    // XML-Dokument instanzieren
doc.Load(Path.Combine(EADatenVerzeichnis, dEintrag.EADatei_Name));          // XML-Daten laden

XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);         // NameSpace Manager initalisieren
nsmgr.AddNamespace("dibt", "https://energieausweis.dibt.de/schema/Kontrollsystem-GEG-2024_V1_0.xsd");   // Spezifischen Namespace als/mit Alias dem NameSpace Manager hinzufügen
               
XmlElement root = doc.DocumentElement;                                      // Root-Knoten initalisieren

string RegNr = doc.SelectSingleNode("//dibt:GEG-Energieausweis/dibt:Energieausweis-Daten/dibt:Registriernummer", nsmgr).InnerText;  // Über voll qualifizierten Pfad (NS-Alias:Knoten-Name) auf den gewünschten Knoten zugreifen

Wieder vielen Dank für deine Hilfe!!!

22.04.2024 - 15:03 Uhr

Hallo Abt!

Ok, ich beschäftige mich erst einmal damit...

22.04.2024 - 14:49 Uhr

Hallo Abt!

Danke für die schnelle Antwort (mit der ich noch nicht zurecht komme).

Die im NameSpace angegebene Schema-Datei (xsd) habe ich nicht.

In dem MS-Beispiel wird doch für das root-Element auch die Klasse XmlElement benutzt. Was sollte ich denn alternativ verwenden?

Ich sehe mir den Teil mit dem NameSpace-Manager noch einmal an, ich dachte ohne die Angabe eines NameSpaces geht es auch.

Danke für die Hinweise!

22.04.2024 - 14:19 Uhr

Hallo!

Ich habe etwas ganz triviales ... , ich möchte den Wert eines Knoten in einer XML-Datei abfragen.

Die XML-Struktur ist gut Strukturiert aber umfangreich. Ich möchte die relevanten Knoten deshalb mit einem absoluten Pfad ansprechen und nicht durch Iterieren mich durch die Baumstruktur kämpfen.

Die Abfrage des Knotenpunktes Registriernummer:

doc = new XmlDocument();                                                    // XML-Dokument instanzieren
doc.Load(Path.Combine(EADatenVerzeichnis, dEintrag.EADatei_Name));          // XML-Daten laden
XmlElement root = doc.DocumentElement;                                      // Root-Knoten initalisieren
                
string RegNr = root.SelectSingleNode("/Energieausweis-Daten/Registriernummer").Value;

ergibt immer null!

Der Root-Knoten (root) enthält alle Eigenschaften wie man der Schnellansicht des Debuggers (Anhang) entnehmen kann, aber die Pfad-Angabe scheint noch falsch zu sein, was ich nicht nachvollziehen kann.

Die XML-Datei:

<?xml version="1.0"?>
<GEG-Energieausweis xmlns="https://energieausweis.dibt.de/schema/Kontrollsystem-GEG-2024_V1_0.xsd">
  <Gebaeudebezogene-Daten>
    <Gebaeudeadresse-Strasse-Nr>Rosenweg 18-44</Gebaeudeadresse-Strasse-Nr>
         ...
    </Gebaeudebezogene-Daten>
  <Energieausweis-Daten Gesetzesgrundlage="GEG-2024" Rechtsstand="2024-04-04" Rechtsstand-Grund="Ausweisausstellung (bei Verbrauchsausweisen und alle anderen Fälle)">
    <Registriernummer>SN-2024-005026180</Registriernummer>
    ...
13.04.2024 - 11:27 Uhr

Hallo Abt!

Wegen einer Eigenschaft die ich im Konstruktor der Klasse nicht implementiert hatte, wollte ich den Konstruktor nicht ändern, deshalb hatte ich es diesmal so gelöst und prompt ... so ein Faselfehler.

Mit records habe ich noch nicht gearbeitet, dass muss ich mir erst einmal ansehen. Vielen Dank für deinen Hinweis!!!

13.04.2024 - 10:28 Uhr

Hallo Th69!

Na da verschwinde ich ja gleich unter dem Boden ... ich dachte es wär etwas spezifisches was mit den anonymen Typen zu tun hat und dann so ein Faselfehler bei einer einfachen Initialisierung. Ich habe es einfach nicht gesehen, auch beim zweiten prüfen 😦

Vielen Dank für dein aufmerksames Lesen!

12.04.2024 - 12:58 Uhr

Hallo!

Eins zieht oft das Andere nach sich ...

Meine Linq-Relation liefert mir eine IEnumerable<anonymerTyp>. Diese möchte ich in eine List<T> konvertieren.

Folgender Ansatz:

GesamtListe = View.Select(x => new EA_Eintrag() { EADatei_Name = x.DateiName, x.WI, x.OBJ.Value }).ToList();

Es wird aber der Fehler:

CS0747	Ungültiger Deklarator des Initialisierermembers.

Bei der Initalisierung des EA_Eintrages angezeigt: (x.WI und x.OBJ.Value).  Bei String scheint alles in Ordnung zu sein.

Wodurch denn???

12.04.2024 - 11:25 Uhr

Hallo Abt!

Der Grund warum der Fehler CS1941 geworfen wird ist, dass die Bezeichnungen der einzelnen Felder der zu vergleichenden anonymen Typen gleich lauten müssen. Am einfachsten gibt man ihnen die gleichen Namen {FeldName1 = , Feldname2 =} equals {Feldname1 =, Feldname2 =} oder man benennt im zweiten anonymen Typ die Feldnamen entsprechend den Feldnamen des ersten anonymen Typ's.

Der Deutlichkeit halber im Beispiel mit A und B gelöst:

var View =
    from ausweiseListe in EAListe                                                                   // Alle Elemente der Ausweis-Liste
    join zuordnungsListe in EAZuordnungsListe                                                       // Left Join -> Zuordnungs-Liste
        on new { A = ausweiseListe.Kontierung_WI, B = ausweiseListe.Kontierung_Obj } equals         // Relation über zwei anonyme Typ(en)
            new { A = zuordnungsListe.WodisWI, B = zuordnungsListe.EAObj } into gj                  // Ergebnis der Relation in GroupJoin speichern
    from subgroup in gj.DefaultIfEmpty()                                                            // Einträge die keine Entsprechung in der Zuordnungs-Liste haben Null setzen
    select new                                                                                      // Ergebnis abfragen
    {
        WI = ausweiseListe.Kontierung_WI,                                                           // Kontierung WI-Teil
        OBJ = subgroup?.WodisObj,                                                                   // Kontierung Objekt-Teil
        DateiName = ausweiseListe.EADatei_Name,                                                     // Ausweis Datei-Name
    };

Wie du schon gesagt hast, hat für den Left-Join hat noch das .DefaultIfEmpty() gefehlt. Jetzt kommt für mich genau das gewünschte Ergebnis.

Vielen Dank für deine Hinweise!!!

11.04.2024 - 17:48 Uhr

Hallo Abt!

Danke für deine schnelle Antwort.

Ja, du hast recht. Inhaltlich brauche ich wahrscheinlich nur einen inner join auf List2.

Werde mir das morgen noch einmal in Ruhe ansehen.

Feld1 + Feld2 stellen eine Kontierung dar, auf die ich in List2 "filtern" muss.

Prinziep: Gib mir alle Einträge von List2, wenn die (gleiche) Kontierung in List1 enhalten ist.

Wünsche noch einen entspannten Abend!

11.04.2024 - 16:26 Uhr

Hallo!

Ich möchte einen Left Outer Join über eine Relation mit 2 Feldern erzeugen.

Liste1 → Feld1 → Integer

Liste1 → Feld2 → Integer

Liste2 → Feld1 → Integer

Liste2 → Feld2 → Integer

Es ist eine 1:N - Beziehung. In Liste2 können im Feld2 mehere Einträge zur Paarung Feld1, Feld2 stehen.

Ich habe folgenden Ansatz:

// Prinziep:

object View = from liste1 in Liste1
                join liste2 in Liste2
                        on new { liste1.Feld1, liste1.Feld2 } equals
                           new { liste2.Feld1, liste2.Feld2 }
                select new { liste2.Feld1, liste2.Feld2, liste2.Feld3 };

// Genaue Implementierung

object View = from ausweiseListe in EAListe
                join zuordnungsListe in EAZuordnungsListe
                        on new { ausweiseListe.Kontierung_WI, ausweiseListe.Kontierung_Obj } equals
                            new { zuordnungsListe.WodisWI, zuordnungsListe.WodisObj }
                select new { zuordnungsListe.WodisWI, zuordnungsListe.WodisObj, zuordnungsListe.EAObj };

Die Relation erstelle ich durch die zwei Tuple {Feld1,Feld2} die ich mit equals vergleiche.

Es wird aber die Exception:

Fehler	CS1941	Der Typ eines Ausdrucks in der join-Klausel ist falsch. Fehler beim Typrückschluss im Aufruf von "Join".

Was ist daran verkehrt???

10.11.2023 - 19:36 Uhr

Hallo!

Wie das manchmal so ist ... im praktischen macht die unmittelbare Aktualisierung einer Eigenschaft in der ICollectionView nur Sinn (egal ob über ein UserControl oder direkt)), wenn diese Eigenschaft nicht in der Sortierung und erst recht nicht in der Gruppierung enthalten ist! In beiden Fällen zieht man ansonsten dem Benutzer das Element, wo er gerade einen Wert ändert, unter der Maus weg! 😃 Mehrfachänderungen führen in den Wahnsinn 😃 😃

In meinem Fall sind es sogar 2 Eigenschaften (CheckBox = Soll das Element übernommen werden und Integer = Position innerhalb der Gruppe).

Ich habe es jetzt so gelöst, dass der Benutzer erst alle Werte ändern kann und danach eine Aktualisierungs-Schaltfläche anklickt, um die Aktualisierung der  ICollectionView (refresh()) auszulösen. Bei diesem Vorgang ist ein wenig Logik implementiert, so dass alle Elemente (UserControls) an dem gewünschten Ort erscheinen (Sortiert und Gruppiert). Der Benutzer kann sich die Auswirkungen seiner Änderung(en) erst noch einmal ansehen und ggf. ändern. (Eine Aktualisierung nehme ich intern immer vor...)


Wenn die Änderungs-Eigenschaft nicht in der Sortierung oder Gruppierung enthalten ist, muss man erst recht nichts machen, da die Eigenschaften ja durch das UserControl-Binding geändert werden und so für die weitere Verarbeitung aktuell zur Verfügung stehen.

Vielen Dank für das (zahlreiche) Interesse an meinem Problem!

08.11.2023 - 12:29 Uhr

Hallo!

Ich habe eine ICollectionView mit einer Gruppierung und einer Sortierung (nach MediaPosition).

Einer ListView ordne ich diese (ICollection)View als SourceCollection zu.

In einer ListView-Spalte setze ich für das CellTemplate ein DataTemplate das ein UserControl beinhaltet:

<ListView ItemsSource="{Binding ImportMedienListeView}" IsSynchronizedWithCurrentItem="True"
>
    <ListView.ItemsPanel>
    ...
    </ListView.ItemsPanel>
    <ListView.View>
        <GridView>
            <!-- Bild -->
            <GridViewColumn>
                <GridViewColumn.CellTemplate>
                    <DataTemplate>
                        <local:UCMediaElement 
                            MediaSource2="{Binding MediaBild}"
                            MediaBereich="{Binding MediaBereich}"
                            MediaDateiName="{Binding MediaDateiName}"
                            IsÜbernahmeRelevant="{Binding IsÜbernahmeRelevant}"
                            MediaPosition="{Binding MediaPosition, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
>
                                                                        
                        </local:UCMediaElement>
                    </DataTemplate>
                </GridViewColumn.CellTemplate>
            </GridViewColumn>
        </GridView>
    </ListView.View>

    <ListView.GroupStyle>
     ...
    </ListView.GroupStyle>
</ListView>

Bis hierher alles Standard... (siehe auch Bild)

In dem UserControl kann man die MediaPosition ändern.

Die Änderung wirkt sich erst auf die View aus, wenn man die View Refresh(t):

ImportMedienListeView.Refresh();                                    // Import-Medien View aktualisieren

Meine Frage ist jetzt, wie kann ich auf eine Änderung einer (gebundenen) Eigenschaft in der (Icollection)View  reagieren?

Die View selbst hat nur CollectionChanged und CurrentChanged - Events (trifft Beides nicht zu). Dem UserControl habe ich noch ein PositionChanged-Event spendiert. Dieses könnte ich im DataTemplate durch einen Behavior abfangen und die View refreshen.

Aber es muss (?) doch auch im ViewModel gehen! Oder?

Das UserControl hat DP's zur Übergabe/Übernahme der Werte.

Die gebundene Eigenschaft der Source-Liste benutzt INotifyPropertyChanged:

/// <summary>
/// Anzeige-Position des importierten Media-Objekt's.
/// </summary>
public int MediaPosition { get => _MediaPosition; 
    set { _MediaPosition = value; OnChanged(nameof(MediaPosition)); } }

Durch das Binding des UserControls wird der Media-Wert geändert, aber ein "einklinken" in den Setter ist bestimmt auch nicht der richtige Weg....

07.11.2023 - 08:20 Uhr

Hallo Abt!

Vor lauter Neuem (Copilot) komme ich ja gar nicht mehr zum Programmieren! 😃

Für Bibliotheken werde ich die XML-Dokumentationsdatei schon erstellen, aber für App's wird es schnell zu aufwendig! Hat aber trotzdem Etwas.

Danke für deinen Hinweis!

06.11.2023 - 23:59 Uhr

Hallo Yankyy02!

Ja, genau das war es! Items!!! Ich hatte nach einer CollectionViewGroupInternal-Klasse gesucht. Da ist Internal einfach nur drangehangen?

Das muss unbedingt in den Wissensspeicher.

Vielen, vielen Dank!

06.11.2023 - 19:26 Uhr

Hallo!

Ich hatte schon einmal eine Lösung, kann sie aber Partus nicht wieder finden...

Ich gruppiere eine ICollectionView nach einer Eigenschaft der CollectionViewSource:

CVSImportMedienListe.GroupDescriptions.Add(new PropertyGroupDescription("MediaBereich"));   // Feste Gruppierung nach dem Media-Bereich setzen

In der View definiere ich einen GroupStyle und zeige im ContainerStyle die Eigenschaft, nach der gruppiert wurde, an.

<ListView ItemsSource="{Binding ImportMedienListeView}" SelectionMode="Single" 
            Background="Transparent">
    <ListView.View>
        <GridView>
            <GridViewColumn Header="Datei-Name" Width="120" DisplayMemberBinding="{Binding MediaDateiName}" />
        </GridView>
    </ListView.View>

    <ListView.GroupStyle>
        <GroupStyle>
            <GroupStyle.ContainerStyle>
                <Style TargetType="{x:Type GroupItem}">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <Expander IsExpanded="True">
                                    <Expander.Header>
                                        <StackPanel Orientation="Horizontal">
                                            <TextBlock Text="{Binding Name}" FontWeight="Bold" VerticalAlignment="Bottom" />
                                            <TextBlock Text="{Binding ItemCount}" FontSize="22" FontWeight="Bold" ... />
                                            <TextBlock Text=" Einträge" FontSize="22" Foreground="Silver" ... />
                                        </StackPanel>
                                    </Expander.Header>
                                    <ItemsPresenter />
                                </Expander>
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Style>
            </GroupStyle.ContainerStyle>
        </GroupStyle>
    </ListView.GroupStyle>
</ListView>

Innerhalb des Styls zeigt der DataContext auf die CollectionViewGroupInternal. Deshalb kann ich nur auf die Name-Eigenschaft binden. Ich möchte jedoch auf eine andere Eigenschaft binden. Wenn ich mich richtig erinnere ging das über das (erste) Item der Items der Gruppe.

Konkret habe ich nach einer Aufzählung gruppiert. Innerhalb der (ListView-ItemsSource) Datenklasse habe ich aber auch eine adäquate Aufzählungs-Bezeichnungs Eigenschaft. Diese ist für die Anzeige natürlich Aussagekräftiger.

Wie komme ich von der CollectionViewGroupInternal zu den ItemsSource-Einträgen?

06.11.2023 - 08:40 Uhr

Guten Morgen Th69!

Um mit Sokrates zu sprechen: Ich weiß, dass ich nichts weiß.

Diese Option (XML-Dokumentationsdatei) in den Projekt Build-Optionen ist mir noch nie aufgefallen.

Jetzt habe ich 498 Warnungen ... denn das wird ja auch konsequent durchgezogen (jede öffentliche Eigenschaft oder Type muss dann eine Beschreibung haben) aber es funktioniert! (Das ich diese auch unterdrücken könnte ist mir schon bewusst.)

Interessant ist allerdings, dass dadurch auch vergessene Beschreibungen von Parametern bei Methoden offensichtlich werden.

Warnung	CS1573	Der x/y-Parameter hat (im Gegensatz zu anderen Parametern) kein entsprechendes param-Tag im ...

Vielen Dank für deine Hilfe!

PS:  Hier gibt es dann gleich noch viel mehr ...

05.11.2023 - 11:29 Uhr

Hallo!

Ich muss mich noch einmal korrigieren.

Die Beschreibung einer DP wird mir bei der Instanzierung des UC's (in XAML) nicht angezeigt. Die DP's erscheinen (im Eigenschafts-Fenster) bei Sonstiges. Aber auch da, ohne die/eine Beschreibung.

05.11.2023 - 11:12 Uhr

Sorry hat sich glaube erledigt.

Bei der public-Variablen zur DP scheint es zu funktionieren.

        private static readonly DependencyProperty RahmenAbstandProperty =
            DependencyProperty.Register("RahmenAbstand", typeof(Thickness), typeof(UCMediaElement),
                                        new PropertyMetadata(new Thickness(2)));
        /// <summary>
        /// (Innerer) Abstand zwischen dem Rahmen und dem Inhalt.
        /// </summary>
        public Thickness RahmenAbstand
        {
            get => (Thickness)GetValue(RahmenAbstandProperty);
            set => SetValue(RahmenAbstandProperty, value);
        }

Muss nur noch kontrollieren, ob es auch bei der Weitergabe des UserControls noch angezeigt wird.

05.11.2023 - 11:03 Uhr

Hallo!

Wie kann ich denn für eine DP eine Beschreibung für den Benutzer "definieren"?

Also adäquat der Summary-Angabe bei VM-Eigenschaften.

private static readonly DependencyProperty RahmenAbstandProperty =
DependencyProperty.Register("RahmenAbstand", typeof(Thickness), typeof(UCMediaElement),
                      new PropertyMetadata(new Thickness(2)));
/// <summary>
/// Der Rahmenabstand ist der Abstand zwischen Rahmen und Inhalt
/// </summary>
18.10.2023 - 12:16 Uhr

Hallo!

Mit einem binären Einlesen bin ich jetzt ans Ziel gekommen, aber ich werde die BOM-Kennung separat behandeln, da ich die Kodierung der gesamten CSV-Datei dann doch lieber dem ReadLines mit Angabe der Kennung überlasse.

byte[] fileBytes = File.ReadAllBytes(PfadDateiname);
StringBuilder sb = new();
foreach (byte b in fileBytes) { sb.Append(Convert.ToChar(b)); }

Ergibt:  ï»¿BOM;ID;Anlass;PLZ;Or...

|

Die BOM-Kennung als String erhalte ich über:

private string BOMKennung { get; } = new string(new char[] { Convert.ToChar(0xEF), Convert.ToChar(0xBB), Convert.ToChar(0xBF) });

Danke für die vielen Hinweise!!!

18.10.2023 - 11:26 Uhr

Hallo Alf Ator!

Du hast schon recht... Zur ersten Zeile gehört bei mir zur Anzeige die BOM-Kennung aber mit dazu.

Ich werde es jetzt mal mit einer binären Einlesung versuchen.

Vielen Dank für deine Hinweise!

18.10.2023 - 11:03 Uhr

Hallo T-Virus!

Ja, ich möchte zur Anzeige (TextBlock) der ersten 10 Zeilen der CSV-Datei, die BOM-Kennung mit anzeigen.

Eine Instanzierung der Kodierung ändert leider nichts. Es werden nur die Spalten-Namen (ohne BOM-Kennung) angezeigt.

Kodierung = new UTF8Encoding(false);

foreach (string Zeile in File.ReadLines(PfadDateiname, Kodierung))

Ergibt Zeile: BOM;ID;Anlass;P...

Da hat Alf Artor bestimmt recht, dass der BOM bereits von ReadLines nicht zurückgegeben wird.

Was die Frage aufwirft, wie bekomme ich die Zeilen als vollständigen String zurück.

Was ich auch nicht verstehe, die reine BOM-Kennung bekomme ich nicht als String:

private string BOMKennung { get; } = Encoding.UTF8.GetString(new byte[] { 0xEF, 0xBB, 0xBF });

Ergibt einen Leer-String!???

18.10.2023 - 10:05 Uhr

Hallo!

Ich möchte die Zeilen einer CSV-Datei einlesen und die Kodierung der Zeilen "erhalten".

Zum Problem...

Die erste Zeile meiner CSV-Datei hat eine (UTF-8) BOM-Kennung (EFBB BF) gefolgt von Semikolon separierten Spalten-Namen.

Die erste Spalte heißt BOM, danach ID, Anlass usw. .  Siehe Bild.

Die Zeilen lese ich über File.ReadLines mit Angabe der Kodierung (UTF-8) ein:

foreach (string Zeile in File.ReadLines(PfadDateiname, Kodierung))
{
    ...
}

Das Problem ist, dass die BOM-Kennung selbst, im 1. Zeile-String nicht enthalten ist!

Zeile: BOM;ID;Anlass;P...   (Nur die Spalten-Namen)

Umlaute in den Spalten-Namen (und an anderen Stellen) werden korrekt kodiert (ä,ü,ö...).

Ich möchte, wie im Bild, für die erste Zeile den String:  i>>?BOM;ID;Anlass... (BOM-Zeichen kann ich hier nicht korrekt darstellen)

Wie kann ich das erreichen?

12.10.2023 - 21:09 Uhr

Hallo!

Ich habe jetzt erst einmal eine Lösung gefunden, die ist aber relativ unflexibel, da sie von einer definierten Anzahl von Einträgen in der Liste ausgeht:

var Eintrag1 = await  WebViewer.CoreWebView2.ExecuteScriptAsync("document.querySelector('.sc-dffc6c81-0').querySelectorAll('li.ipc-inline-list__item')[0].innerText");

Vielleicht gibt es ja noch bessere!

12.10.2023 - 20:43 Uhr

Hallo!

Eine Überprüfung der Anzahl der selektierten Einträge:

string Anzahl = await WebViewer.CoreWebView2.ExecuteScriptAsync("document.querySelector('.sc-dffc6c81-0').querySelectorAll('li.ipc-inline-list__item').length");

gibt 3, also die korrekte Anzahl der Einträge wieder!

Das Problem ist, dass die Methode CoreWebView2.ExecuteScriptAsync() als (Awaitable) Task<string> typisiert ist, und

var Einträge = await  WebViewer.CoreWebView2.ExecuteScriptAsync("document.querySelector('.sc-dffc6c81-0').querySelectorAll('li.ipc-inline-list__item')");

demzufolge "null" zurück gibt.

Wie könnte man die Daten (innerhalb einer Liste) in der Internetseite denn noch abfragen?

Könnte der Script das Iterieren gleich ausführen und einen String zurückgeben?

12.10.2023 - 18:18 Uhr

Hallo!

Im Bereich Web-Entwicklung habe ich noch nicht so viel gemacht ... und hoffe, dass ich hier im richtigen Forum bin.

Ich habe von einer (fremden) Web-Seite folgenden HTML Ausschnitt:

<div class="sc-dffc6c81-0 iwmAVw">
    <h1 textlength="14" data-testid="hero__pageTitle" class="sc-afe43def-0 hnYaOZ">
        <span class="sc-afe43def-1 fDTGTb">Plötzlich Papa</span>
    </h1>
    <div class="sc-afe43def-3 EpHJp">Originaltitel: Demain tout commence</div>

    <ul class="ipc-inline-list ipc-inline-list--show-dividers sc-afe43def-4 kdXikI baseAlt">
        <li class="ipc-inline-list__item">
            <a class="ipc-link ipc-link--baseAlt ipc-link--inherit-color" role="button" tabindex="0" aria-disabled="false" href="/title/tt5078204/releaseinfo?ref_=tt_ov_rdat">2016</a>
        </li>
        <li class="ipc-inline-list__item">
            <a class="ipc-link ipc-link--baseAlt ipc-link--inherit-color" role="button" tabindex="0" aria-disabled="false" href="/title/tt5078204/parentalguide/certificates?ref_=tt_ov_pg">0</a>
        </li>
        <li class="ipc-inline-list__item">1 Std. 58 Min.</li>
    </ul>
</div>

In meinem C# (WPF) Projekt setze ich die WebView2 Komponente von Microsoft zur Anzeige einer Internet-Seite ein und möchte definierte Informationen "abgreifen".

Microsoft.Web.WebView2.Wpf.WebView2 WebViewer = (Microsoft.Web.WebView2.Wpf.WebView2)sender;    // WebViewer "holen"

Der direkte Zugriff auf den Text eines Objekt, dass einer Klasse zugeordnet ist funktioniert:

string htmlTitel = await WebViewer.CoreWebView2.ExecuteScriptAsync("document.querySelector('.sc-afe43def-1').innerText");

Nun möchte ich aber eine Liste mit querySelectorAll iterieren und bekomme einfach keine (gefüllte) Liste zurück:

var html3 = await WebViewer.CoreWebView2.ExecuteScriptAsync("document.querySelector('.sc-dffc6c81-0').querySelectorAll('.li.ipc-inline-list__item')");

oder so

var html3 = await WebViewer.CoreWebView2.ExecuteScriptAsync("document.querySelector('.sc-dffc6c81-0').querySelector('.sc-afe43def-4').querySelectorAll('.li.ipc-inline-list__item')");

Was mache ich denn verkehrt?

28.09.2023 - 23:36 Uhr

Hallo Abt!

Was ich alles ausprobiert habe! (Ich hatte immer das Problem der zusätzlichen Ebene bei den Darsteller-Einträgen)

Und du umgehst das Problem so einfach!

Jetzt wo ich meinen LINQ Left Outer Join verstanden habe machst du alles wieder zunichte! 😉

Das Prinzip hat mich aber schon überzeugt! So kann ich in jeder Ebene prüfen, ob ein (Listen) Wert auch vorhanden ist!

Struktur:

Liste DVD-Einträge
° DVD-Eintrag1
° Liste Land (Eigenschaft Land)
° Land1 ID → Land
° Land2 ID → Land
° Liste Genre (Eigenschaft Genre)
° Genre1 ID → Genre
° Genre2 ID → Genre
° Liste Darsteller-Einträge (Eigenschaft DarstellerEinträge)
° Darsteller-Eintrag1 
° Darsteller-Position
° Darsteller ID → Darsteller
° Darsteller-Eintrag2
° Darsteller-Position
° Darsteller ID → Darsteller

Zugegebener maßen habe ich den Eindruck, dass diese Lösung etwas langsamer ist (Darsteller sind es zur Zeit ca. 78.000 die jedes mal von den 8.000 DVD-Einträgen "gefiltert" werden), aber 10s beim Start sind noch ok.

Bleibt mir nur noch wieder vielen Dank!!!! zu sagen!

PS: Kann man hier auch Leerzeichen erzeugen die bleiben?

28.09.2023 - 20:34 Uhr

Hallo Abt!

Die Quelle müsste ich erst wieder suchen. Habe schon einiges recherchiert bevor ich hier frage ...

Den join um den es geht habe ich hier abgesetzt dargestellt:

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

        join Dar in DarstellerListe on DVD.DarstellerEinträge.FirstOrDefault().DarstellerID equals Dar.ID  // Inner Join Darsteller (1. Wert)
        into Gruppe4 from Dar2 in Gruppe4.DefaultIfEmpty()                    // Performing Left Outer Join

        select new DVDEintragSort(DVD,
        Reg2 != null ? Reg2.Name : string.Empty,
        Gen2 != null ? Gen2.Bezeichnung : string.Empty,
        Land2 != null ? Land2.Bezeichnung : string.Empty,
        Dar2 != null ? Dar2.Name : string.Empty)
    ).ToList();

Bei dieser Variante wird eine NullReferenceException geworfen: (siehe Bild)   Der Debugger zeigt auch genau die Stelle.

System.NullReferenceException
  HResult=0x80004003
  Nachricht = Object reference not set to an instance of an object.

Danke! für deine unermüdliche Unterstützung!

28.09.2023 - 19:24 Uhr

Hallo!

Ich habe eine Linq-Relation die mir den ersten Darsteller aus einer Darsteller-Liste zurückgeben soll:

join Dar in DarstellerListe on DVD.DarstellerEinträge.FirstOrDefault().DarstellerID equals Dar.ID

Zum Verständnis: DVD ist der "Datensatz" der die Eigenschaft DarstellerEinträge hat, die eine Liste der Darsteller darstellt. Dessen 1. Eintrag ich über die Relation der DarstellerID zu einer Darsteller-Liste setzen möchte.

Das funktioniert so lang, wie die Darsteller-Liste auch Einträge hat. (Mir wird korrekt der 1. Darsteller zurückgegeben)

Wenn die Liste keine Einträge hat, wird durch FirstOrDefault() der Wert null (Defaultwert eines Objektes) und null hat natürlich keine Eigenschaft DarstellerID. Es wird eine Exception geworfen.

Ich habe im Internet noch einen Ansatz gefunden, der darauf basiert, dass FirstOrDefault() dadurch ersetzt wird, dass man die Darsteller-Liste sortiert (OrderBy), danach sich den ersten Eintrag zurückgeben lässt (Take(1)) und wenn dies nicht möglich ist (DefaultIfEmpty) als Ersatz-Wert einen neuen Darsteller-Eintrag erzeugt:

 join Dar in DarstellerListe on DVD.DarstellerEinträge.OrderBy(x => x.DarstellerPosition).Take(1).DefaultIfEmpty(new DarstellerEintrag()) equals Dar
                  

Klingt plausibel! (Bei leerer Liste:) Ein neuer Darsteller-Eintrag stimmt mit keinem Eintrag in der Darsteller-Liste überein die Relation ist nicht "vorhanden". Aber es wird auch keine Exception geworfen.

Der join scheint aber den gesamten Darsteller-Eintrag (1. oder Neuen) mit den Darsteller-Eintrag aus der Darsteller-Liste nicht vergleichen zu können. Fehlermeldung:

CS1941	Der Typ eines Ausdrucks in der join-Klausel ist falsch. Fehler beim Typrückschluss im Aufruf von "GroupJoin".

Ich habe aber keinen Weg gefunden die ID (wie im obigen join) anzugeben.

27.09.2023 - 21:56 Uhr

Hallo Abt, Hallo chilic!

Ihr sprecht beide das Gleiche an, aber wie soll es denn richtig realisiert werden?

Ich habe eine Schaltfläche die eine Rücksicherung einleiten soll. Sprich Rücksicherungs-Command.

Der Benutzer soll mit dem Auslösen des Commands eine optische Bestätigung, dass er einen Vorgang initiiert hat, bekommen.

Entspricht meiner Command-Routine:

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
}

Die Logik in der RunRücksicherung -Methode habe ich nun auch noch in die Command-Methode übernommen. Bleibt für den Daten-Thread nur noch das (Parameter gesteuerte) De-Komprimieren der Zip-Datei.

An dieser Stelle müsste ich auch noch einmal "nachschärfen", da ich in meiner Bibliothek in einem Fall die Abfrage des Zielverzeichnisses integriert habe. Da stellt sich die Frage, ob meine Bibliothek dann so viel bringt.

Aber dann ... wäre es korrekt? Oder?

Jetzt heißt es Kopf abkühlen Danke! und gute Nacht.

27.09.2023 - 20:55 Uhr

Hallo Abt, Hallo Th69!

Die Ursache lag (natürlich) ganz, ganz wo Anders. Das Binding zur View-Opacity war Schlicht und Einfach korrumpiert (gibt bestimmt auch noch einen knallige Fachbezeichnung!). Ich hatte die View-Opacity einmal an eine Viewmodel Eigenschaft und einmal über eine (in XAML definierte) Animation "gebunden". Wenn ich dann die Animation einmal ausgeführt hatte war es um beide "Bindings" geschehen! Und das nicht reagieren der View-Opacity war ja der Ausgangspunkt meiner ganzen Überlegungen.

Danke Th69 deine Erklärung zur MessageBox:

diese läuft in einer von Hauptfenster unabhängigen eigenen Nachrichtenschleife

brachte mich zum Suchen in andere Richtungen!

Was bleibt ist, dass die RunRückSicherung-Methode nach wie vor im Hauptthread läuft. Da der Vorgang aber sowieso erst fortgeführt werden kann, wenn die Datenrücksicherung abgeschlossen ist, stört mich die Hauptthread-Blockade erst einmal nicht.

Bei der Lösung wo das MessageBox-Ergebnis im Abeits-Thread abgefragt werden kann:

await Task.Run(() =>
{
    _ = AnimiereViewHelligkeit(((MainWindow)obj).Resources, "Dunkel");    // View Helligkeit Dunkel (Animation ausführen)
    MessageBoxResult dialogResult = MessageBox.Show("Ja -> Opacity = 1, Nein -> Opacity bleibt bei 0,7", "Titel",
                                                    MessageBoxButton.YesNo);
    if (dialogResult == MessageBoxResult.Yes)
    {
        _ = MessageBox.Show("Ja Clicked");
        _ = AnimiereViewHelligkeit(((MainWindow)obj).Resources);    // View Helligkeit Hell (Animation ausführen)
    }
    else
        MessageBox.Show("Nein Clicked");
});

muss man noch mit Invoke arbeiten, um den Zugriff zu den Resourcen zu ermöglichen.

Vielen Dank für die vielen Hinweise!!!

27.09.2023 - 18:37 Uhr

Hallo Th69!

Bin gerade ein wenig in der Krise und muss auch noch etwas testen ...

Erst einmal grundsätzlich, muss die MessageBox eigentlich nicht asynchron aufgerufen werden.

Ich wollte dem Benutzer nur signalisieren, dass er den Rücksicherungsvorgang eingeleitet hat und über die MessageBox soll er auswählen, welchen Umfang die Rücksicherung haben soll.

Mein Ausgangsproblem war, dass sich die View-Opacity bei der Auslösung des Commands nicht geändert hat!

Dabei ist mir dann aufgefallen, dass sich die RunRücksicherungs-Methode im Hauptthread befindet. Deshalb wollte ich die MessageBox dann auch asynchron abfragen.

Ich habe für die asynchrone MessageBox jetzt noch eine "Lösung" im Internet gefunden, die die Problematik noch mehr verwirrt.

await Task.Run(() =>
{
    WndOpacity = 0.7;                                                   // Transparenz der View absenken
    MessageBoxResult dialogResult = MessageBox.Show("Ja -> Opacity = 1, Nein -> Opacity bleibt bei 0,7", "Titel", 
                                                    MessageBoxButton.YesNo);
    if (dialogResult == MessageBoxResult.Yes)
    {
        _ = MessageBox.Show("Ja Clicked");
        WndOpacity = 1;
    }
    else
        MessageBox.Show("Nein Clicked");
});

Diese rufe ich innerhalb meiner Command-Methode (die ich fast vollkommen leer gemacht habe) auf.

Wenn ich diese Debugge sehe ich, dass die Task.Run-Methode in einem ArbeitsThread läuft! (siehe Bild)

Und trotzdem wird die Opacity der View nicht geändert!!!!!!

Das muss ich jetzt erst einmal untersuchen.

Zu deiner Frage noch, das (gleiche, da dies in der Basisklasse definiert ist) ActionCommand verwende ich in der gleichen Applikation auch an anderen Stellen asynchron und da ist alles korrekt (Allerdings ohne MessageBox!).

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?