Laden...

MVVM: Tabelle mit dynamischen Daten dynamisch erstellen

Letzter Beitrag vor einem Jahr 9 Posts 652 Views
MVVM: Tabelle mit dynamischen Daten dynamisch erstellen

Moin,
ich bin gerade dabei ein PacketViewer zu erstellen, der die Packets von diesem Client-Server-Protocol auslesen und anzeigen soll. Das Auslesen der Packets und deren IDs in einer Liste darstellen, klappt schon soweit. Nun möchte ich, daß wenn man auf einen der ListItems klickt, die Details zu dem jeweiligen Packet in einem neuen Fesnter angezeigt werden. Klick-Funktion und neues Fenster funktionieren auch schon.

Beim Anzeigen der Daten komme ich aber nicht weiter. Ich habe es mir nach dem folgenden Schema vorgestellt:
`
Beschreibung des Packets als Text.

Überschrift:
key1: value1
key2: value2

Überschrift:
key3: value3
key4: value4
key5: value5
key6: value6

Überschrift:
...`

Einige Packets haben kaum Daten andere wiederum sind sehr datenreich. Es sind insgesamt 198, größtenteils sehr unterschiedliche Packets und ich möchte nicht für jedes Packet eine View erstellen müssen. Eine Idee wäre es mit einer Dictionary zu versuchen und dann alles irgendwie zu parsen:


dict.add("*headline*", "Überschrift");
dict.add("key1", "value1");
dict.add("key2", "value2");
dict.add("*headline*", "Überschrift");
...

Ich habe nur keinen blassen Schimmer, wie man so etwas mit MVVM generiert. Bei jeder Headline muss es irgendwie ein Column-Span geben.

Ist das überhaupt eine gute Idee oder gibt es bessere Lösungsansätze? Und wenn ja, wie realisiert man das?

Wäre es da nicht besser, dem Kinde gleich die richtigen Namen zu verpassen?

Du hast es doch mit dem Schema quasi schon fertig:


class Section
{
    public string Header { get; set; }
    public Dictionary<string,string> Values { get; set; }
}

class Packet
{
    public string Description { get; set; }
    public List<Section> Sections { get; set; }
}

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

Hallo PierreDole!

Die Beschreibung deines Problems klingt im Ansatz nicht so, als hättest du dein (bisheriges) Projekt bereits MVVM konform aufgebaut. Da ist es schwer, ein Detail jetzt MVVM konform umzusetzen.

Kannst du denn aus deinen eingelesenen Daten eine Datenklasse entsprechend deiner vorgeschlagenen Struktur erstellen / befüllen?

Wenn ich das richtig sehe hast du eine 3stufige hierarchische Struktur.

Ebene 0 -> Liste der Packete, die die Packet-Details enthält.

Ebene 1 -> Die Beschreibung zu einem Packet und eine Liste die die Details zu den Überschriften enthält.

Ebene 2 -> Die die Bezeichnung einer Überschrift und eine Liste der Werte zu dieser Überschrift.

Vorschlag zum ViewModel:


public class VMPacketDetails : MVVM_Base
{
    private List<PacketDetail> _PacketListe;
    /// <summary>
    /// Beschreibung des Packete's
    /// </summary>
    public List<PacketDetail> PacketListe { get => _PacketListe; set { _PacketListe = value; OnChanged(nameof(PacketListe)); } }

    public VMPacketDetails()
    {
        PacketListe = GetPacketDetails();
    }

    /// <summary>
    /// Methode zur Abfrage der Packet-Details
    /// </summary>
    /// <returns></returns>
    private static List<PacketDetail> GetPacketDetails()
    {
        List<PacketDetail> PacketDetails = new();

        #region Beispiel-Werte
        string beschreibung; List<Überschrift> überschriften; Überschrift überschrift;

        überschriften = new List<Überschrift>();
        überschrift = new Überschrift("Packet: 0x0C", new List<string> { "Sent By: Both", "Size: variable" });
        überschriften.Add(überschrift); 

        überschrift = new Überschrift("Packet Build", new List<string> { "BYTE[1] Cmd", "BYTE[2] Length", "BYTE[2] Tile Number to edit. If number is 0x8000+, then it is a map tile.",
                                                                            "BYTE[4] Tile data Flags for the item.", "BYTE[1] Weight of item.", "BYTE[1] Quality of item." });
        überschriften.Add(überschrift);

        beschreibung = "Packet Name: Edit Tile Data (God Client)" + Environment.NewLine + "Last Modified: 2010 - 01 - 13 10:24:46" + Environment.NewLine + "Modified By: Tomi";

        PacketDetails.Add(new PacketDetail(beschreibung, überschriften));
        #endregion

        return PacketDetails;
    }
}


Und zu den entsprechenden Daten-Klassen:


#region Daten-Klassen

public class PacketDetail
{
    /// <summary>
    /// Beschreibung des Packete's
    /// </summary>
    public string Beschreibung { get; set;}

    /// <summary>
    /// Liste der Überschrift(en) eines Packetes
    /// </summary>
    public List<Überschrift> Überschriften { get; set; }

    public PacketDetail(string beschreibung, List<Überschrift> überschriften)
    {
        Beschreibung = beschreibung; Überschriften = überschriften;
    }
}

public class Überschrift
{
    /// <summary>
    /// Bezeichnung der Überschrift
    /// </summary>
    public string Bezeichnung { get; set; }

    /// <summary>
    /// Werte zu einer Überschrift
    /// </summary>
    public List<string> Werte { get; set; } = new();

    public Überschrift(string bezeichnung, List<string> werte)
    {
        Bezeichnung = bezeichnung; Werte = werte;
    }
}
#endregion


Dann kannst du in deiner View ein PacketDetail (pro Fenster) entweder mit einem hierarchische TreeView oder mit einer ListView und DataTemplates, nach deinen Wünschen anzeigen / gestalten.

  • statics haben in ViewModels nich wirklich was zu suchen
  • In Konstruktoren sollte man keine Daten generieren
  • In ViewModels verwendet man keine List sondern ObservableCollection
  • Code mit Umlauten ist Körperverletzung

Hallo Abt!

Vielen Dank! für deine Hinweise! und das meine ich auch so!!!

Zu drei Hinweisen habe ich allerdings noch ein paar Anmerkungen.

statics haben in ViewModels nich wirklich was zu suchen

Ich habe die GetPacketDetails-Methode static deklariert, weil VS sonst die Mitteilung setzt:

Fehlermeldung:
Nachricht CA1822 Der Member "GetPacketDetails" greift nicht auf Instanzdaten zu und kann als "static" markiert werden.

und ich anstrebe in der Fehlerliste keine Eintragungen zu belassen (0/0/0). Aber ich verstehe den prinzipiellen Ansatz.

In Konstruktoren sollte man keine Daten generieren

Auch dies ist prinziepell richtig. Die Daten würden in einem Praxis-Projekt ja auch aus einem Daten-Abruf resultieren. In diesem Fall werden aber Beispieldaten erzeugt.
Sollte man trotzdem die Erstellung (zeitversetzt) in einen Daten-Thread auslagern???

In ViewModels verwendet man kein List sondern ObservableCollection

Da mir von vornherein klar war, dass ich für die View ein CurrentItem benutze und mit einer CollectionViewSource, für die als Source schon eine List<T> "genügt", arbeite, hatte ich diese (bewußt) gewählt. Dies ist dann aus dem Gesamtkonzept, welche ich gleich noch poste, ersichtlich.

Dein letzter Hinweis ist aber vollkommen richtig. Was soll ich sagen? Deutsche Bezeichnungen sind mir einfach lieber. (Bis jetzt hat es auch immer 100ig funktioniert.)

Aber noch einmal, ich finde es gut das du auf so elementares hinweist!!!

Ich habe die GetPacketDetails-Methode static deklariert, weil VS sonst die Mitteilung setzt:

Das ist ein reiner Hinweis. Visual Studio peitscht Dich nicht aus. Als Entwickler ist man durchaus in Verantwortung zu überlegen: macht dieser Hinweis sinn?
Normalerweise haben aber ViewModel gar keine Verantwortung Daten selbst zu erzeugen, sondern laden zB Daten über ein Repository. Dadurch erübrigt sich das Problem.

Ein gesunder Hinweis wäre hier, dass der Compiler Dir sagt: "Ey, raus mit der Daten Generierung aus dem ViewModel!"

und ich anstrebe in der Fehlerliste keine Eintragungen zu belasse

Es ist kein Fehler. Es taucht auch niemals unter den Fehlern auf, sondern unter den Hinweisen.
Wirf vielleicht mal ein Blick in das Thema Compiler Hints und überleg Dir, ob jeder Hint in der Form für Dich auch Sinn macht.

Vorgehen wie "Ich will eine leere Liste haben" ist nicht unbedingt ein gesundes Vorgehen für Deine Software, sondern füttert nur Deinen inneren Monk.

List<T> "genügt", arbeite, hatte ich diese (bewußt) gewählt. Dies ist dann aus dem Gesamtkonzept, welche ich gleich noch poste, ersichtlich.

Das mag für Deinen Fall okay sein, aber das hier ist ja ein Forum um Wissen zu teilen. Hier gehts ja vor allem um Konzepte.
Wenn man also jemanden versucht ein Konzept zu erklären, dann sollte man das Konzept hier beachten.

Kopiert jemand Dein Code blind, was leider oft passiert, funktionierts einfach nicht.

Hallo PierreDole!

Ich wollte auch noch einen Vorschlag für die View nachreichen. Ich habe diesen mit Umschaltung (ComboBox) der Packete gestaltet. Für dein Vorhaben würde es reichen wenn du dem Fenster ein (das ausgewählte) PacketDetail-Objekt übergibst und den XAML-Code von der GroupBox Details übernimmst. Du würdest auch nicht an CurrentItem der DataView binden sondern im ViewModel des Anzeigefensters auf die übergebene Eigenschaft vom Typ PacketDetail zugreifen.

View-Vorschlag:


<Window x:Class="PacketDetails.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PacketDetails"
        mc:Ignorable="d" WindowStartupLocation="CenterScreen"
        Title="Packet Übersicht" Height="450" Width="800">
    <Window.DataContext>
        <local:VMPacketDetails/>
    </Window.DataContext>
    <Grid Margin="10">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="10"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="10"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="10"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <ComboBox Grid.Row="0" ItemsSource="{Binding MyDatenView}"  IsSynchronizedWithCurrentItem="True" HorizontalAlignment="Left">
            <ComboBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel>
                        <TextBlock Text="{Binding PacketName}" FontWeight="Bold"/>
                    </StackPanel>
                </DataTemplate>

            </ComboBox.ItemTemplate>
        </ComboBox>
        <GroupBox Grid.Row="2" Header="Packet" Padding="10" Background="AliceBlue">
            <StackPanel>
                <TextBlock Text="{Binding MyDatenView.CurrentItem.PacketName}" FontWeight="Bold"/>
                <TextBlock Text="{Binding MyDatenView.CurrentItem.Beschreibung}"/>
            </StackPanel>
        </GroupBox>
        <GroupBox Grid.Row="4" Header="Details" Padding="10" Background="AliceBlue">
            <ListBox ItemsSource="{Binding MyDatenView.CurrentItem.Überschriften}" Background="AntiqueWhite">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <StackPanel>
                            <TextBlock Text="{Binding Bezeichnung}" FontWeight="Bold"/>
                            <ListBox ItemsSource="{Binding Werte}" Margin="10,0,0,0" Background="Transparent" BorderBrush="Transparent"/>
                        </StackPanel>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
        </GroupBox>
    </Grid>
</Window>

Aktualisiertes ViewModel:


/// <summary>
/// ViewModel Packet-Details
/// </summary>
public class VMPacketDetails : MVVM_Base
{
    #region Zentrale Eigenschaften
    public CollectionViewSource CvsDatenListe { get; set; } = new();        // CVS der Daten-Liste deklarieren
    public ICollectionView MyDatenView => CvsDatenListe.View;               // View der Daten-Liste deklarieren

    /// <summary>
    /// Liste der (eingelesenen) Packete
    /// </summary>
    public List<PacketDetail> PacketListe { get; set; }
    #endregion

    public VMPacketDetails()
    {
        PacketListe = GetPacketDetails();
        CvsDatenListe.Source = PacketListe;
    }

    /// <summary>
    /// Methode zur Abfrage der Packet-Details
    /// </summary>
    /// <returns></returns>
    private static List<PacketDetail> GetPacketDetails()
    {
        List<PacketDetail> PacketDetails = new();

        #region Beispiel-Werte
        string beschreibung; List<Überschrift> überschriften; Überschrift überschrift;
        #region Packet 1
        überschriften = new List<Überschrift>();
        überschrift = new Überschrift("Packet: 0x0C", new List<string> { "Sent By: Both", "Size: variable" });
        überschriften.Add(überschrift); 

        überschrift = new Überschrift("Packet Build", new List<string> { "BYTE[1] Cmd", "BYTE[2] Length", "BYTE[2] Tile Number to edit. If number is 0x8000+, then it is a map tile.",
                                                                            "BYTE[4] Tile data Flags for the item.", "BYTE[1] Weight of item.", "BYTE[1] Quality of item." });
        überschriften.Add(überschrift);

        beschreibung = "Last Modified: 2010 - 01 - 13 10:24:46" + Environment.NewLine + "Modified By: Tomi";

        PacketDetails.Add(new PacketDetail("Edit Tile Data (God Client)", beschreibung, überschriften));
        #endregion

        #region Packet 2
        überschriften = new List<Überschrift>();
        überschrift = new Überschrift("Packet: 0x22", new List<string> { "Sent By: Both", "Size: 3 bytes" });
        überschriften.Add(überschrift);

        überschrift = new Überschrift("Notes", new List<string> { "0 = invalid/across server line", "1 = innocent (blue)", "2 = guilded/ally (green)",
                                                                    "3 = attackable but not criminal (gray)", "4 = criminal (gray)" });
        überschriften.Add(überschrift);

        beschreibung = "Last Modified: 2009-08-13 11:59:28" + Environment.NewLine + "Modified By: MuadDib";

        PacketDetails.Add(new PacketDetail("Character Move ACK/ Resync Request", beschreibung, überschriften));
        #endregion

        #endregion

        return PacketDetails;
    }
}

#region Daten-Klassen

public class PacketDetail
{
    /// <summary>
    /// Name des Packete's
    /// </summary>
    public string PacketName { get; set; }

    /// <summary>
    /// Beschreibung des Packete's
    /// </summary>
    public string Beschreibung { get; set;}

    /// <summary>
    /// Liste der Überschrift(en) eines Packetes
    /// </summary>
    public List<Überschrift> Überschriften { get; set; }

    public PacketDetail(string packetName, string beschreibung, List<Überschrift> überschriften)
    {
        PacketName = packetName; Beschreibung = beschreibung; Überschriften = überschriften;
    }
}

public class Überschrift
{
    /// <summary>
    /// Bezeichnung der Überschrift
    /// </summary>
    public string Bezeichnung { get; set; }

    /// <summary>
    /// Werte zu einer Überschrift
    /// </summary>
    public List<string> Werte { get; set; } = new();

    public Überschrift(string bezeichnung, List<string> werte)
    {
        Bezeichnung = bezeichnung; Werte = werte;
    }
}
#endregion

Vielen Dank für eure Antworten. Ich stand wohl gewaltig auf dem Schlauch, daß ich nicht drauf kam eigene Klassen dafür zu nehmen. Irgendwie bin ich gedanklich am Key-Value hängen geblieben.

Ich hätte noch zwei Fragen.

Wie man auf dem Screenshot sehen kann, haben die beiden ListBoxen ein Hover-Effekt, den ich da nicht haben möchte. Ich habe schon viele Code-Schnippsel ausprobiert und nicht gehts. Es kommen keine Fehler, aber auch keine Wirkung.
Eigentlich brauche ich auch keine Liste in dem Sinne, sondern eher eine Tabelle. Kann man nicht einfach einen Grid dynamisch befüllen?

Dann wäre noch die TextBox, die sich nicht warpen lassen will. Die "Command"-Zeile ist ewig lang und egal was ich nicht probiere, da kommt kein Zeilenumbruch. Komischerweise geht das aber mit der TextBox, die Raw Data anzeigt, obwohl es die gleiche ist. Habe sie copy-pastet. Raw Data ist auch nur eine lange Zeile. Ich möchte keine fixen Breiten angeben, da sich das Layout dem Fenster anpassen soll.


<Window x:Class="PacketViewer.UI.View.PacketDetailsWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:PacketViewer.UI.View"
        mc:Ignorable="d"
        Title="Packet Details" Height="400" Width="570">
    <ScrollViewer
        HorizontalScrollBarVisibility="Disabled"
        VerticalScrollBarVisibility="Auto">
        <Grid 
            Grid.Column="0" Grid.Row="0"
            Margin="10">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <GroupBox
                Grid.Row="0"
                Header="Packet"
                Padding="10"
                Background="AliceBlue">
                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="Auto" />
                        <ColumnDefinition Width="*" />
                    </Grid.ColumnDefinitions>
                    <StackPanel Grid.Column="0">
                        <TextBlock Text="ID: " FontWeight="Bold"/>
                        <TextBlock Text="Name: " />
                        <TextBlock Text="Length: " />
                        <TextBlock Text="Sent by: " />
                        <TextBlock Text="Description: "/>
                    </StackPanel>
                    <StackPanel Grid.Column="1" Margin="7, 0, 0, 0">
                        <TextBlock Text="{Binding ID}" FontWeight="Bold"/>
                        <TextBlock Text="{Binding Name}" />
                        <TextBlock Text="{Binding Length}" />
                        <TextBlock Text="{Binding SentBy}" />
                        <TextBlock Text="{Binding Description}"/>
                    </StackPanel>
                </Grid>
            </GroupBox>
            <GroupBox
                Grid.Row="1"
                Header="Details"
                Padding="10"
                Background="AliceBlue">
                <ListBox
                    ItemsSource="{Binding Details}"
                    Width="Auto"
                    Background="AntiqueWhite"
                    ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                    <ListBox.Resources>
                        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
                        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Transparent" />
                        <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
                        <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="Transparent" />
                    </ListBox.Resources>
                    <ListBox.ItemTemplate>
                        <DataTemplate>
                            <Grid>
                                <Grid.RowDefinitions>
                                    <RowDefinition Height="Auto" />
                                    <RowDefinition Height="Auto" />
                                </Grid.RowDefinitions>
                                <TextBlock Grid.Row="0" Text="{Binding Header}" FontWeight="Bold"/>
                                <ListBox
                                    Grid.Row="1"
                                    ItemsSource="{Binding Data}"
                                    Margin="10,0,0,0"
                                    Background="Transparent"
                                    BorderBrush="Transparent"
                                    ScrollViewer.HorizontalScrollBarVisibility="Disabled">
                                    <ListBox.Resources>
                                        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="Transparent" />
                                        <SolidColorBrush x:Key="{x:Static SystemColors.HighlightTextBrushKey}" Color="Transparent" />
                                        <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="Transparent" />
                                        <SolidColorBrush x:Key="{x:Static SystemColors.InactiveSelectionHighlightBrushKey}" Color="Transparent" />
                                    </ListBox.Resources>
                                    <ListBox.ItemTemplate>
                                        <DataTemplate>
                                            <Grid>
                                                <Grid.ColumnDefinitions>
                                                    <ColumnDefinition Width="Auto" />
                                                    <ColumnDefinition Width="Auto" />
                                                </Grid.ColumnDefinitions>
                                                <TextBlock 
                                                    Grid.Column="0"
                                                    Text="{Binding Path=DataName}"/>
                                                <TextBox
                                                    Width="Auto"
                                                    Margin="7,0,0,0"
                                                    Grid.Column="1" 
                                                    Text="{Binding Path=DataValue}"
                                                    TextWrapping="Wrap"
                                                    Background="Transparent"
                                                    BorderThickness="0"
                                                    IsReadOnly="False"
                                                    ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>
                                            </Grid>
                                        </DataTemplate>
                                    </ListBox.ItemTemplate>
                                </ListBox>
                            </Grid>
                        </DataTemplate>
                    </ListBox.ItemTemplate>
                </ListBox>
            </GroupBox>
            <GroupBox
                Grid.Row="2"
                Header="Raw Data"
                Padding="10"
                Background="AliceBlue">
                <TextBox
                    Width="Auto"
                    Grid.Column="1" 
                    Text="{Binding Path=RawData}"
                    TextWrapping="Wrap"
                    Background="Transparent"
                    BorderThickness="0"
                    IsReadOnly="False"
                    ScrollViewer.HorizontalScrollBarVisibility="Disabled"/>
            </GroupBox>
        </Grid>
    </ScrollViewer>
</Window>