Hallo!
Da ich einen Weg gefunden habe das DataTemplate aus einem XAML-Text zu erstellen, kann ich auch bei komplexen CellTemplates spezifische DataTemplates erstellen und diese der Auflistung des GridView Converters hinzufügen.
Als erstes definiere ich eine DataTemplate-Vorlage als XAML-Text, wobei die spezifische Data-Bindung als Platzhalter angegeben wird (Path=) und erstelle zweitens über eine Methode daraus das spezifische DataTemplate:
// 1. DataTemplate-Vorlage
string DataTemplateVorlage = @"<Border BorderBrush = ""Green"" BorderThickness = ""1"" Padding = ""2"" > ";
DataTemplateVorlage += @"<TextBox MinWidth = ""100"" Text = ""{Binding Path=, UpdateSourceTrigger=PropertyChanged}"" /> ";
DataTemplateVorlage += @"</Border >";
// 2. Spezifisches DataTemplate
DataTemplate dataTemplate = GetDynamicDataTemplate(DataTemplateVorlage, "Bewertung");
/// <summary>
/// Konvertiert einer XAML-DataTemplate Vorlage ein DataTemplate.
/// </summary>
/// <param name="DataTemplateVorlage">XAML-DataTemplate (Vorlage) das in ein DataTemplate konvertiert werden soll.</param>
/// <param name="DatenSpalte">Bezeichnung der Datenspalte die im DataTemplate verwendet werden soll.</param>
/// <param name="BindingIndikator">[Optional] Platzhalter für die Binding-Angabe des DataTemplates. Standard: Path=</param>
/// <returns></returns>
public static DataTemplate GetDynamicDataTemplate(string DataTemplateVorlage, string DatenSpalte, string BindingIndikator = "Path=")
{
DataTemplateVorlage = DataTemplateVorlage.Replace(BindingIndikator, BindingIndikator + DatenSpalte);
string zDataTemplate = @"<DataTemplate xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation""> ";
zDataTemplate += DataTemplateVorlage + "</DataTemplate>";
StringReader stringReader = new(zDataTemplate);
XmlReader xmlReader = XmlReader.Create(stringReader);
return System.Windows.Markup.XamlReader.Load(xmlReader) as DataTemplate;
}
Dieses spezifische DataTemplate übergebe ich der GridView SpaltenConfigurations-Liste welche der GridView-Converter zur dynamischen Erstellung der GridView benutzt.
SpaltenKonfiguration = new arWPF.WPF_Objekte.ColumnConfig // Spalten-Konfiguration initalisieren
{
Columns = new List<arWPF.WPF_Objekte.Column>
{
new arWPF.WPF_Objekte.Column("Spalte1 Produkt", "Bezeichnung"), // Spalte 1
new arWPF.WPF_Objekte.Column("Spalte2 Bewertung", "Bewertung", null, dataTemplate) // Spalte 2
}
};
Ziel erreicht. Dynamisches ListView mit Eingabemöglichkeit (durch CellTemplate).
Hallo!
Ich fülle ein GridView mit Daten aus einer Benutzer-SQL Abfrage. Die Anzahl der Grid-Spalten sind daher dynamisch.
Dies realisiere ich über einen GridView-Converter entsprechend dem Vorschlag von hier den ich etwas modifiziert habe:
<ListView ItemsSource="{Binding DatenListe}" View="{Binding SpaltenKonfiguration, Converter={StaticResource ConfigToDynamicGridViewConverter}}"/>
Das Grundprinzip: Der Converter erstellt aus einer Liste<>, in der die GridViewColumn-Eigenschaften definiert werden, ein (dynamisches) GridView.
Da ich für einzelne Spalten eine Eingabemöglichkeit realisieren möchte, benutze ich ein GridViewCell-Template.
Der GridView Converter bindet im Standard den DatenSpalten-Name an die DisplayMemberBinding - Eigenschaft der Grid-Spalte.
Deshalb habe ich den GridView-Converter so geändert, dass er bei Angabe eines Cell-Templates das DisplayMemberBinding übergeht und damit die Datenbindung im Cell-Template realisiert wird. (Anmerkung: Bei DisplayMemberBinding wird das Cell-Template nicht wirksam!)
[ValueConversion(typeof(ColumnConfig), typeof(GridView))]
/// <summary>
/// Value Converter zum dynamischen Binden einer Spalten-Liste an eine Grid-View.
/// </summary>
public class ConfigToDynamicGridViewConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ColumnConfig config) // Ist der Konverter an eine ColumnConfig-Instanz gebunden?
{
GridView gridView = new(); // Grid-View initalisieren
foreach (Column column in config.Columns) // Spalten-Liste iterieren
{
GridViewColumn gvc = new() // GridView-Spalte initalisieren
{
Header = column.SpaltenBezeichnung,
HeaderContainerStyle = column.HeaderContainerStyle,
CellTemplate = column.CellTemplate
};
if(column.CellTemplate == null) // Wenn kein Cell-Template angegeben wurde -> DisplayBinding implementieren, ansonsten muss das DataBinding im CellTemplate definiert werden!
{
Binding binding = new(column.NameDatenSpalte); // (DisplayMember) Binding initalisieren
gvc.DisplayMemberBinding = binding; // DisplayMemberBinding das Daten-Binding übergeben
}
gridView.Columns.Add(gvc); // GriedView-Spalte dem GridView hinzufügen
}
return gridView; // Grid-View adäquat der übergebenen ColumnConfig-SpaltenListe zurückgeben.
}
return Binding.DoNothing; // Im Fehlerfall "Leeres"-Binding als Grid-View zurückgeben
}
Beispiel für eine GridView-Spalten Generierung:
SpaltenKonfiguration = new arWPF.WPF_Objekte.ColumnConfig // Spalten-Konfiguration initalisieren
{
Columns = new List<arWPF.WPF_Objekte.Column>
{
new arWPF.WPF_Objekte.Column("Spalte1 Produkt", "Bezeichnung"), // Spalte 1
new arWPF.WPF_Objekte.Column("Spalte2 Bewertung", "Bewertung", null, GVCellTemplate) // Spalte 2
}
};
Alles schön, funktioniert (siehe Bild).
Jetzt habe ich allerdings das Problem, dass die Datenbindung im Cell-Template fest angegeben ist:
<DataTemplate x:Key="GridViewCellTemplate">
<Border BorderBrush="Green" BorderThickness="1" Padding="2">
<TextBox MinWidth="100" Text="{Binding Path=Bewertung, UpdateSourceTrigger=PropertyChanged}" />
</Border>
</DataTemplate>
(Wie) kann ich das Binding des übergebenen DataTemplates dynamisch gestalten?
Genauer gesagt, geht das bereits in XAML?
Hallo!
Ich habe es jetzt erst einmal so gelöst, dass ich über ein CellTemplate → DataTemplate einem TextBlock einen MausOver-Behavior zuordne und in diesem über die Tag-Eigenschaft die Spaltenzuordnung an die VM-Eigenschaft übertrage.
<GridViewColumn>
<GridViewColumn.Header>
<TextBlock Text="Datei-Name (Spalte 3)" />
</GridViewColumn.Header>
<GridViewColumn.CellTemplate>
<DataTemplate>
<TextBlock Text="{Binding DateiName}" Tag="3">
<i:Interaction.Behaviors>
<local:MouseOverSpaltenBehavior/>
</i:Interaction.Behaviors>
</TextBlock>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
Behavior:
/// <summary>
/// MouseOver Spalten-Behavior
/// </summary>
public class MouseOverSpaltenBehavior : Behavior<TextBlock>
{
/// <summary>
/// MouseOverBehavior -> Loaded +
/// </summary>
protected override void OnAttached() => AssociatedObject.IsMouseDirectlyOverChanged += AssociatedObject_IsMouseDirectlyOverChanged;
/// <summary>
/// MouseOverBehavior -> Loaded -
/// </summary>
protected override void OnDetaching() => AssociatedObject.IsMouseDirectlyOverChanged -= AssociatedObject_IsMouseDirectlyOverChanged;
private void AssociatedObject_IsMouseDirectlyOverChanged(object sender, DependencyPropertyChangedEventArgs e)
{
TextBlock tb = (TextBlock)sender;
VMDateiSystem vm = (VMDateiSystem)arWPF.WPF_Tools.WPF_Tools.VisualTreeHelpers.FindAncestor<Window>(tb).DataContext;
vm.AktSpalte = Convert.ToInt16(tb.Tag, App.GebietsSchemaDE);
}
}
Ist ganz schön "durch die Brust", aber vielleicht geht es ja noch einfacher?
Hallo!
Wie die Überschrift schon suggeriert möchte ich beim Überstreichen einer GridViewColumn den Wert einer VM-Eigenschaft setzen (z.B. die aktuelle Spalten-Nummer).
Das GridViewColumn hat keine Events. Über einen ListViewItem-Style könnte ich zwar beim Maus-Überstreichen eine Objekt-Eigenschaft setzen (<Setter Property=""), aber ich habe keinen Weg gefunden darüber eine VM-Eigenschaft zu setzen.
<Style TargetType="{x:Type ListViewItem}" x:Key="ListViewItemMouseOver">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Green"/>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="ListView" x:Key="ListViewMouseOverStyle">
<Setter Property="ItemContainerStyle" Value="{StaticResource ListViewItemMouseOver}"/>
</Style>
Außerdem erfasse ich durch den ListViewItemStyle nur die gesamte "Zeile" nicht die Spalte, über die der der Maus-Zeiger steht.
Kennt ihr da eine Lösung?
Hallo jbrown!
Die Zusammenhänge waren mir bis jetzt noch vollkommen unklar. Mit der Header-Breite steuert man also auch die "Daten"-Breite bei einer GridViewColumn. Gut zu wissen. Ein Thumb-Objekt habe ich bis jetzt auch noch nie benötigt, jetzt wird es klar, wo man es einsetzen kann!
Vielen Dank für deine Unterstützung!!!
So funktioniert bei mir die Breitenänderung der GridViewerColumn perfekt:
private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
if (sender is Thumb thumb && thumb.TemplatedParent is GridViewColumnHeader header)
{
if (header.Column != null)
{
double newWidth = header.Column.ActualWidth + e.HorizontalChange;
header.Column.Width = Math.Max(newWidth, 20); // Mindestbreite setzen
if (header.Column.Width is double.NaN) header.Column.Width = 20;
}
}
}
Hallo!
Leider noch ein Problem.
Ich habe eine ListView bei dem ich für die Spalten ein HeaderTemplate (ListViewHeaderLinks) definiert habe:
<Application.Resources>
<local:SplitConverter x:Key="SplitKonv" Trennzeichen="|"/>
<Style TargetType="{x:Type GridViewColumnHeader}" x:Key="ListViewHeaderLinks">
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GridViewColumnHeader}">
<Border BorderBrush="Black" Background="Transparent" BorderThickness="0,0,1,1">
<Border HorizontalAlignment="Center" Background="Transparent">
<Grid MinHeight="70">
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="1" Foreground="Gray" Margin="0,5,0,0"
Text="{TemplateBinding Content, Converter={StaticResource SplitKonv}, ConverterParameter=2}">
<TextBlock.LayoutTransform>
<RotateTransform Angle="-90"/>
</TextBlock.LayoutTransform>
</TextBlock>
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" DockPanel.Dock="Bottom" Padding="5,5,5,1" Width="{TemplateBinding Width}" TextAlignment="Left" FontWeight="Bold"
Text="{TemplateBinding Content, Converter={StaticResource SplitKonv}, ConverterParameter=1}" />
</Grid>
</Border>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
Dieses ordne ich den Spalten zu:
<ListView SelectionMode="Single" IsSynchronizedWithCurrentItem="True" Background="LightYellow"
ItemsSource="{Binding ListImportDateisystem}">
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding ZeilenNummer }" Header="Spalte: 1|Zeile" HeaderContainerStyle="{StaticResource ListViewHeaderLinks}" />
<GridViewColumn DisplayMemberBinding="{Binding DateiPfad }" Header="Spalte: 2|Datei-Pfad" HeaderContainerStyle="{StaticResource ListViewHeaderLinks}" />
<GridViewColumn DisplayMemberBinding="{Binding DateiName }" Header="Spalte: 3|Datei-Name" HeaderContainerStyle="{StaticResource ListViewHeaderLinks}"/>
<GridViewColumn DisplayMemberBinding="{Binding DateiGröße }" Header="Spalte: 4|Datei-Größe" HeaderContainerStyle="{StaticResource ListViewHeaderLinks}"/>
<GridViewColumn DisplayMemberBinding="{Binding DateiDatum }" Header="Spalte: 5|Datei-Datum" HeaderContainerStyle="{StaticResource ListViewHeaderLinks}"/>
</GridView>
</ListView.View>
</ListView>
Das HeaderTemplate wird verwendet, alles schön.
Mein Problem ist, dass zur Laufzeit jetzt die Spaltenbreite nicht mehr veränderbar ist, da der Maus-Kursor-Doppelpfeil beim überstreichen der Maus an den Spalten-Grenzen (im Standard eine senkrechte hellgraue Linie) nicht angezeigt wird.
(Im Bild rot umrandet: So müsste es sein, aber beim Überstreichen ändert sich der Maus-Zeiger nicht!)
Hallo Th69!
Na das ist ja wieder mal ein Zusammenhang! Ich hatte im Vorfeld schon gesucht, aber unter DataGrid Eingabe-Zeile kamen keine verwertbaren Hinweise.
Mal wieder vielen Dank für deine Lösung!
Hallo Blonder Hans
Danke für deine Antwort!
Diesen Beitrag hatte ich mir angesehen (daher das Binding={... Path=.} ) aber inhaltlich nicht beachtet (was auch nicht gezeigt wird)), dass man dann an eine List<T(Object)> binden muss und nicht List<T(Referenz)> benutzen kann. In diesem Beispiel hätte es ja List<StringWrapper> heißen müssen, damit man Binding={... Path=.} benutzen kann. Der Grund (string = Verweis-Typ deshalb Strings in Objekte einschließen und die Pfadeigenschaft für das Binding verwenden)) wurde ausführlich erklärt, man hätte aber auch einfach sagen können List<String> kann man nicht TwoWay binden.
Ich benutze jetzt eine OC<StringListenEintrag> und binde (wieder) an eine String-Eigenschaft über den Eigenschafts-Namen. (Standard)
public class StringListenEintrag
{
public string StringWert { get; set; }
public StringListenEintrag(string Wert)
{
StringWert = Wert;
}
}
<DataGrid ItemsSource="{Binding MediaDateiBlacklist}"
AutoGenerateColumns="False" CanUserAddRows="True" IsReadOnly="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Extension" Binding="{Binding StringWert}"/>
</DataGrid.Columns>
</DataGrid>
Was aber immer noch ist, die Leer-Zeile am Ende des DataGrid's wird mir nicht angezeigt. Ein anfügen eines Eintrages ist somit direkt im DataGrid nicht möglich.
Woran kann das den noch liegen?
Hallo!
Ich dachte das wär was ganz Einfaches!
Ich möchte eine OC an ein DataGrid binden:
<DataGrid ItemsSource="{Binding MediaDateiBlacklist, UpdateSourceTrigger=PropertyChanged}"
AutoGenerateColumns="False" CanUserAddRows="True" IsReadOnly="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Extension" Binding="{Binding Mode=TwoWay, Path=. }"/>
</DataGrid.Columns>
</DataGrid>
// VM Eigenschaft:
private ObservableCollection<string> _MediaDateiBlacklist;
/// <summary>
/// Liste der Datei-Extensionen die nicht! übernommen werden sollen.
/// </summary>
public ObservableCollection<string> MediaDateiBlacklist { get => _MediaDateiBlacklist; set { _MediaDateiBlacklist = value; OnChanged(nameof(MediaDateiBlacklist)); } }
// Initalisierung im Konstruktor des VM
public VMDateiSystem()
{
MediaDateiBlacklist = new();
MediaDateiBlacklist.Add(".exe");
MediaDateiBlacklist.Add(".zip");
MediaDateiBlacklist.Add(".doc");
MediaDateiBlacklist.Add(".inf");
}
Als erstes ist mir aufgefallen, das zur Laufzeit das DataGrid keine Leerzeile besitzt, so das ich keinen neuen Eintrag direkt (im DG) hinzufügen kann.
Als zweites musste ich feststellen, das ich zwar einen Wert direkt im DataGrid ändern kann, beim Verlassen des Eintrages wird aber der "Ursprungs-Wert" wiederhergestellt (Die Änderung unwirksam).
Wenn ich programmtechnisch (per Button) der OC einen neuen Eintrag hinzufüge wird dieser Eintrag im DataGrid zwar angezeigt, aber auch diesen Wert kann ich wieder nicht dauerhaft speichern. Beim Verlassen des Eintrags wird er (auf den Wert der programmtechnischen Zuweisung) zurückgesetzt.
Es wird werden weder ein Bindingsfehler noch eine Exception beim Ändern angezeigt/geworfen!
Wie bindet man korrekt eine OC an ein DataGrid?
Hinweis:
Wenn ich den Bindungs-Mode in der DataGridTextColumn nicht explizit angebe, wird beim Verlassen eines geänderten Eintrages die Exception:
System.InvalidOperationException: "Die bidirektionale Bindung erfordert "Path" oder "XPath"."
geworfen.
Hallo Abt!
Ich liebe deine fachgerechten Antworten!!!
Natürlich hat es so funktioniert!
TabBatchImportDefinitionen.AsEnumerable().Select(row =>
{
int eNCODING_TEXT = Convert.IsDBNull(row["ENCODING_TEXT"]) ? 0 : Convert.ToInt32(row["ENCODING_TEXT"], App.GebietsSchema); // DB-NULL Werte abfangen
return new BatchImportDefinition
{
Bezeichnung = (string)row["BEZEICHNUNG"],
...
ENCODING_TEXT = eNCODING_TEXT
};
});
Ich war zu sehr darauf fixiert, die DBNull-Abfrage bei der Initalisierung zu implementieren. Das row-Objekt habe ich ja schon vorher!!!
Vielen DANK! für deine schnelle Hilfe!
Hallo!
Ich möchte DBNull-Werte einer Spalte einer DataTable bei der Instanzierung eines Typ's abfangen.
Ich habe eine DataTable: TabBatchImportDefinitionen, dessen Zeilen ich über eine Linq-Abfrage iteriere und dabei neue Instanzen des Typs: BatchImportDefinition erstelle um eine "Liste" (IEnumerable<T>) der Werte der DataTable zu erstellen:
IEnumerable<BatchImportDefinition> ietab =
TabBatchImportDefinitionen.AsEnumerable().Select(row =>
{
return new BatchImportDefinition
{
Bezeichnung = (string)row["BEZEICHNUNG"],
Dokumententyp = (string)row["DOK_TYP"],
ENCODING_TEXT = (if (Convert.IsDBNull(row["ENCODING_TEXT"])) 0 // DB-NULL Werte abfangen
else Convert.ToInt32(row["ENCODING_TEXT"], App.GebietsSchema))
};
});
Da eine Spalte (ENCODING_TEXT) auch DBNull-Werte enthalten kann, möchte ich sie bei der Instanzierung des Typ's (BatchImportDefinition) abfangen.
Eine if-Abfrage innerhalb der Instanzierung ist aber nicht gültig.
Welche Möglichkeit gibt es denn dies zu realisieren?
So, geschafft!
Ich habe dem ViewModel noch zwei Eigenschaften (MatrixZeilen und MatrixSpalten) hinzugefügt, dem (beim Wechsel des Ablage-Typs) die Matrix-Werte des aktuellen Ablage-Typs zugeordnet werden:
public void RefreshAblageTypMatrixListe(AblageTyp aktAblageTyp)
{
MatrixZeilen = aktAblageTyp.MatrixZeilen;
MatrixSpalten = aktAblageTyp.MatrixSpalten;
...
}
und diese Werte im ItemsPanelTemplate an die zugeordneten Eigenschaften (Rows und Columns) des UniformGrid's gebunden.
<ItemsControl ItemsSource="{Binding AblageTypMatrixListe, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Height="200" Width="200"
Rows="{Binding MatrixZeilen, UpdateSourceTrigger=PropertyChanged}"
Columns="{Binding MatrixSpalten, UpdateSourceTrigger=PropertyChanged}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Jetzt wird die Matrix beim Ändern des Ablage-Typ's korrekt aktualisiert.
Hallo!
Ein Teil der Fragen hat sich geklärt.
Die Aktualisierung der Anzeige des ItemsControl wird ausgeführt, wenn ich für die zentrale AblageTypMatrixListe eine OC verwende.
private ObservableCollection<Border> _AblageTypMatrixListe;
/// <summary>
/// Liste der Ablage-Typ Matrix Einträge
/// </summary>
public ObservableCollection<Border> AblageTypMatrixListe { get => _AblageTypMatrixListe; set { _AblageTypMatrixListe = value; OnChanged(nameof(AblageTypMatrixListe)); } }
Bei einer OC wird die View ja automatisch auch beim Verändern des Inhalts (hinzufügen/löschen...) aktualisiert aber ich war davon ausgegangen, dass die manuelle Aktualisierung der View in der Befüllungs-Methode (RefreshAblageTypMatrixListe) reicht, aber ... ?
public void RefreshAblageTypMatrixListe(AblageTyp aktAblageTyp)
{
....
OnChanged(nameof(AblageTypMatrixListe)); // View der Matrix-Liste aktualisieren
}
Nun muss ich nur noch die Matrix gebunden bekommen. Dann wärs perfekt.
Hallo!
Ich möchte eine Matrix (Zeilen und Spalten) welche in einem Objekt-Typ (AblageTyp) definiert sind grafisch darstellen.
Der AblageTyp hat die (integer) Eigenschaften MatrixZeilen und MatrixSpalten.
public partial class AblageTyp
{
/// <summary>
/// ID des Ablage-Typ's.
/// </summary>
public long ID { get; set; } = DateTime.Now.Ticks;
...
/// <summary>
/// Anzahl der Spalten des Ablage-Typ's
/// </summary>
public int MatrixSpalten { get; set; } = 2;
/// <summary>
/// Anzahl der Zeilen des Ablage-Typ's
/// </summary>
public int MatrixZeilen { get; set; } = 3;
...
}
Im ViewModel habe ich eine View einer AblageTyp'en-Liste.
class VMAblageTypenVerwaltung : MVVM_Base
{
internal CollectionViewSource CVSAblageTypen { get; set; } = new CollectionViewSource(); // CVS deklarieren
/// <summary>
/// View der Ablage-Typen Liste.
/// </summary>
public ICollectionView AblageTypenListeView { get => CVSAblageTypen.View; }
...
Diese Daten-View zeige ich in einem ListView an.
<ListView ItemsSource="{Binding AblageTypenListeView}" IsSynchronizedWithCurrentItem="True">
<i:Interaction.Behaviors>
<local:AblageTypChangeBehavior/>
</i:Interaction.Behaviors>
<ListView.View>
<GridView>
<GridViewColumn DisplayMemberBinding="{Binding Path=Bezeichnung}" Header="Bezeichnung" />
<GridViewColumn DisplayMemberBinding="{Binding Path=Kapazität}" Header="Kapazität" />
<GridViewColumn DisplayMemberBinding="{Binding Path=MatrixZeilen}" Header="Matrix-Zeilen" />
<GridViewColumn DisplayMemberBinding="{Binding Path=MatrixSpalten}" Header="Matrix-Spalten" />
</GridView>
</ListView.View>
</ListView>
Beim navigieren des Benutzers durch die AblageTyp'en wird über einen Behavior eine Methode (RefreshAblageTypMatrixListe) aufgerufen die im ViewModel eine zentrale Matrix-Liste (AblageTypMatrixListe) in welcher Border entsprechend der Summe der aktuellen Matrix dynamisch gefüllt werden, aktualisiert.
public void RefreshAblageTypMatrixListe(AblageTyp aktAblageTyp)
{
AblageTypMatrixListe.Clear(); // Matrix-Liste leeren
for (int i = 0; i < aktAblageTyp.MatrixZeilen * aktAblageTyp.MatrixSpalten; i++) // Matrix iterieren
{
Border border = new();
border.Margin = new(5); border.Background = Brushes.Blue;
border.MinHeight = 20; border.MinWidth = 20;
AblageTypMatrixListe.Add(border);
}
OnChanged(nameof(AblageTypMatrixListe)); // View der Matrix-Liste aktualisieren
}
Entsprechend dem Ablage-Typ besteht die Matrix-Liste aus einer unterschiedlichen Anzahl an Bordern.
Diese (in der passenden Matrix) möchte ich grafisch darstellen. Dazu verwende ich ein ItemControl dem ich als ItemsSource die dynamisch gefüllte Matrix-Liste zuordne.
<ItemsControl ItemsSource="{Binding AblageTypMatrixListe, UpdateSourceTrigger=PropertyChanged}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Height="200" Width="200" Rows="3" Columns="3" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
Beim ersten Aufruf wird die Anzahl an Border auch angezeigt (siehe Bild). Bei einem Wechsel des Ablage-Art Eintrages wird zwar die ViewModel Matrix-Liste korrekt aktualisiert aber die grafische Anordnung ändert sich nicht.
Es ergeben sich für mich 2 Fragen:
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!).