Hallo Abt!
Danke für deine Hinweise!!!
Dieses manuelle Aktualisieren mache ich ja nur (nicht "automatisch" im Setter der Eigenschaft) weil ich die Klasse serialisieren muss. Der DVD-Eintrag ist für mich so etwas wie ein Datensatz. Deshalb (wie oben angemerkt) die Lösung über die Partial-Klasse.
Und ja, das sind Sub-Eigenschaften die gebunden werden sollen wobei ein OnChange der übergeordneten Eigenschaft (in meinem Fall Ablage) ausreicht hat, um die untergeordneten Eigenschaft(en) mit zu aktualisieren.
Ahaaa: Dein Beispiel (jetzt erst gesehen) ist für mich gut nachvollziehbar!
Vielen Dank!
Hallo Abt!
Für meine Begriffe ist die Referenz auf die Eigenschaft DVD (vom Type DVDEintrag) des ViewModels (welche an die View gebunden wird) geblieben.
Es hat sich nur der "Wert" dieser Eigenschaft geändert. Null-Werte erzeugen scheinbar die PathError-Meldung.
Das verstehe ich nicht?
Ich verwende keine Events. Manuell habe ich den Clones nur die Werte des Originals bei der Initalisierung zugeordnet.
Die Aktualisierung der Anzeige hatte nur wegen dem falschen Bezug des OnChanges nicht funktioniert. Jetzt arbeitet das Binding wieder ganz korrekt.
Hallo!
Der Fehler lag an dem Bezug des OnChanged. Es muss natürlich das OnChanged des DVDEintrages (DVD.OnChanged()) aufgerufen werden! (Und nicht das OnChanged des ViewModels!
public void RefreshDaten(MainWindow view, DVDEintrag dVDEintrag, ...)
{
_ = MessageBox.Show("tboOrdnungsNummer (Binding-Status vor Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: Active
DVD = dVDEintrag.Clone(); // Aktuellen DVD-Eintrag übernehmen
DVD.OnChanged(nameof(DVD.Ablage)); // View aktualisieren -> DVD-Ablage
_ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: Active
...
}
Sorry!
Hallo!
Manchmal gibt es doch noch ein Problem mit Bindings ...
Ich habe eine MVVM-Anwendung bei der ich im ViewModel von einer Eigenschaft (Klasseninstanz) eine Kopie (Clone) erstelle. In der View ist diese gebunden.
Eigenschaft (DVD):
private DVDEintrag _DVD;
/// <summary>
/// Aktueller DVD-Eintrag.
/// </summary>
public DVDEintrag DVD { get => _DVD; set { _DVD = value; OnChanged(nameof(DVD)); } }
Clone Methode der Eigenschaft:
/// <summary>
/// Kopiert den aktuellen DVD-Eintrag. (Neue Instanz)
/// </summary>
/// <returns>Kopie des akt. DVD-Eintrages. (Neue Instanz)</returns>
public DVDEintrag Clone()
{
DVDEintrag Neu = new(); // Neue Instanz eines DVD-Eintrages
Neu.Titel = Titel; // Wert übernehmen
if (Ablage != null) // Im Original: Ablage vorhanden?
{
Neu.Ablage = new(); // Neue Instanz der Ablage des DVD-Eintrages
Neu.Ablage.Einheit = Ablage.Einheit; // Wert übernehmen
Neu.Ablage.LageBez = Ablage.LageBez; // Wert übernehmen
Neu.Ablage.OrdnungsNr = Ablage.OrdnungsNr; // Wert übernehmen
Neu.Ablage.Position = Ablage.Position; // Wert übernehmen
...
}
...
return Neu; // Neue Instanz des aktuellen DVD-Eintrages zurückgeben
}
<TextBox x:Name="tboONr" Text="{Binding DVD.Ablage.OrdnungsNr, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Solang die Eigenschaft(en) meiner geklonten Klasse Stamm-Typen sind (string, integer...) ist das Binding nach der Zuweisung zu der geklonten Instanz korrekt. (Die Eigenschaften erhalten ja auch nur geänderte Werte)
public void RefreshDaten(DVDEintrag dVDEintrag ...)
{
DVD = dVDEintrag.Clone(); // Aktuellen DVD-Eintrag übernehmen
...
}
Wenn einer Eigenschaft der Clone-Eigenschaft aber eine neue Instanz einer Klasse zugeordnet wird (wie im obigen Beispiel bei der Eigenschaft Ablage) geht nach Zuweisung der geklonten Instanz zur gebundenen Eigenschaft des ViewModels (DVD = dVDEintrag.Clone()
)das Binding verloren.
Auch OnChanged() reaktiviert das Binding nicht.
public void RefreshDaten(MainWindow view, DVDEintrag dVDEintrag, ...)
{
_ = MessageBox.Show("tboOrdnungsNummer (Binding-Status vor Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: Active
DVD = dVDEintrag.Clone(); // Aktuellen DVD-Eintrag übernehmen
OnChanged(nameof(DVD.Ablage)); // View aktualisieren -> DVD-Ablage
OnChanged(nameof(DVD.Ablage.OrdnungsNr)); // View aktualisieren -> DVD-Ablage-OrdnungsNr
_ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: PathError
...
}
Einzig und allein ein Target-Update auf das View-Objekt, an das die ViewModel-Eigenschaft gebunden ist, stellt das Binding wieder her.
public void RefreshDaten(MainWindow view, DVDEintrag dVDEintrag, ...)
{
_ = MessageBox.Show("tboOrdnungsNummer (Binding-Status vor Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: Active
DVD = dVDEintrag.Clone(); // Aktuellen DVD-Eintrag übernehmen
OnChanged(nameof(DVD.Ablage)); // View aktualisieren -> DVD-Ablage
OnChanged(nameof(DVD.Ablage.OrdnungsNr)); // View aktualisieren -> DVD-Ablage-OrdnungsNr
_ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Clone()): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: PathError
view.tboONr.GetBindingExpression(TextBox.TextProperty).UpdateTarget();
_ = MessageBox.Show("tboOrdnungsNummer (Binding-Status nach Target-Update): " + view.tboONr.GetBindingExpression(TextBox.TextProperty).Status.ToString()); // Ergebnis: Active
...
}
Diese Arbeitsweise kann doch nicht MVVM gerecht sein? Wie kann man das Binding trotz Clone erhalten?
Anmerkung:
Da die DVDEintrag-Klasse serialisiert wird, arbeite ich mit partial class, deshalb der expliziete OnChanged(nameof(DVD.Ablage))-Aufruf, zur Aktualisierung der View.
/// <summary>
/// Klasse zur Strukturierung eines DVD-Eintrages.
/// </summary>
[Serializable]
public partial class DVDEintrag
{
/// <summary>
/// Ablage-Ort der DVD
/// </summary>
public AblageEintrag Ablage { get; set; }
...
}
public partial class DVDEintrag : INotifyPropertyChanged // Erweiterung der Datenklasse
{
/// <summary>
/// DVDEintrag -> PropertyChanged
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
internal void OnChanged([System.Runtime.CompilerServices.CallerMemberName] string propName = "") => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propName));
}
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!!!
Hallo Abt!
Ok, ich beschäftige mich erst einmal damit...
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!
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>
...
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!!!
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!
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???
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!!!
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!
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???
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!
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....
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!
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!
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?
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 ...
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.
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.
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>
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!!!
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!
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!???
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?
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!
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?
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?
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?
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!
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.
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.
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!!!
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!).
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!
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.
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.
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?
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!
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.
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();
Ob das der beste Weg ist? Ich bleib (notgedrungen) dran!
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?.
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!
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!
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?
Hallo T-Virus!
Ich sage jetzt mal Ahaaa! 😃 Die Hilfs-Klasse hätte ich nicht mit DataModel gleichgesetzt.
Trotzdem vielen Dank!
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>
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:
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!