Laden...

Wie bindet man korrekt ein DataGrid?

Erstellt von TheLion092 vor 4 Jahren Letzter Beitrag vor 4 Jahren 2.291 Views
T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren
Wie bindet man korrekt ein DataGrid?

Ich probiere derzeit viel mit Bindings rum und versuche ein DataGrid an eine List zu binden. Ich habe jetzt viel darüber gelesen, z.B. dass man bei einem einfachen Binding das Problem hat, dass das DataGrid sich bei Wertänderung in der Quelle nicht automatisch aktualisiert. Außerdem habe ich gelesen, dass BindingList<T> und ObservableCollection<T> nicht mehr verwendet werden sollen. Am besten soll wohl "ICollectionView" sein. Auch wenn ich jetzt viel dazu gelesen habe, vor allem hier: https://www.codingfreaks.de/2017/08/14/wpf-und-mvvm-richtig-einsetzen-teil-5/ und auch hier [Artikel] MVVM und DataBinding , benötige ich wohl doch nochmal einen kleinen Lösungsvorschlag, das ganze mit der besten Methode (dies scheint wohl "ICollectionView" zu sein) umzusetzten.

Seid nicht zu grob zu mir, ich bringe mir das alles grade selber bei 😃

XAML:


<Window x:Class="Test.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:Test"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" WindowStartupLocation="CenterScreen">

    <Grid>
        <Button x:Name="button" Content="User erzeugen" HorizontalAlignment="Left" Margin="10,10,0,0" Width="100" VerticalAlignment="Top" Click="ClickEvent"/>
        <DataGrid x:Name="datagrid" Margin="10,50,10,10" AutoGenerateColumns="True" IsReadOnly="True" Loaded="datagridLoaded"/>
    </Grid>
</Window>

MainWindow:


using System.Collections.Generic;
using System.Windows;

namespace Test
{
    public partial class MainWindow : Window
    {
        //Private Felder (Variablen)
        private bool loaded;
        private List<User> userlist;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void datagridLoaded(object sender, RoutedEventArgs e)
        {
             userlist = new List<User>();
             datagrid.ItemsSource = userlist;
        }

        private void ClickEvent(object sender, RoutedEventArgs e)
        {
            userlist.Add(new User() { vorname= "Hans", nachname= "Peter", alter = "42" });    
        }
    }

    class User
    {
        public string vorname { get; set; }
        public string nachname { get; set; }
        public int alter { get; set; }
    }
}

Liebe Grüße

16.827 Beiträge seit 2008
vor 4 Jahren

Außerdem habe ich gelesen, dass BindingList<T> und ObservableCollection<T> nicht mehr verwendet werden sollen.

Wo hast Du das gelesen?

ObservableCollection ist ein absolutes Muss bei WPF.
WPF ist auf Datenbindung mit MVVM konzipiert - und hier spielt die ObservableCollection eine maßgebliche Rolle.
Steht auch in [Artikel] MVVM und DataBinding

Dir fehlt in Deinem Code das entsprechende ViewModel, das Du bindest, komplett.
Du verwendest überhaupt kein Binding in Deinem Code - das solltest Du unbedingt tun.
Events wiederum verwendet man in WPF und MVVM prinzipiell gar nicht bzw. versucht das eben durch Bindings und Commands umzusetzen.

Verwende also ein ViewModel, das Deine User kennt und binde das ViewModel an ein Grid.
Aber programmiere nicht das DataGrid über das Window an.

public class User 
{
   // Dies stellt Dein Modell dar
}

public class MainWindowViewModel
{
   // Dies stellt Dein ViewModel für das MainWindow dar
   // Hier werden Deine User gelistet
   public ObservableCollection<User> Users {get;set;}

   // Hier wäre zB auch der Datenbank-Context, der die User Liste füllt
}


public class MainWindow
{ 
   // Dies ist ein WPF Window.
   // Hier ist die Instanz des ViewModels bekannt - Zugriff jedoch via XAML
   // Code Behind vermeiden!
}
5.658 Beiträge seit 2006
vor 4 Jahren

Ergänzend dazu sei gesagt, daß auch User ein ViewModel sein sollte und INotifyPropertyChanged implementieren muß, um die Änderungen anzuzeigen:

public ObservableCollection<UserViewModel> Users  { get; set; }

Steht aber alles auch in dem verlinkten Artikel zu MVVM.

Weeks of programming can save you hours of planning

T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

Wo hast Du das gelesen?

Im Artikel https://www.codingfreaks.de/2017/08/14/wpf-und-mvvm-richtig-einsetzen-teil-5/ steht:

Der hier gezeigte Ansatz ist scheinbar sehr charmant. Man hat etwas extrem einfaches im ViewModel (ObservableCollection<T>), bindet dagegen und alles scheint sauber zu funktionieren. Das Problem bei dem Beispiel ist die Performance. Um das zu verstehen, muss man sich vergegenwärtigen, dass das UI-Element DataGrid in unserem Beispiel direkt auf den Daten operiert. Außerdem beeinflussen die Daten direkt das Erscheinungsbild des UI. Bei 10 oder 100 Elementen spielt das noch keine Rolle. Haben wir aber z.B. 10.000 Elemente in der Datenquelle, wird das Bein recht schnell dick.

Ein weiterer wichtiger Nachteil unserer Lösung ist, dass wir auf das Multi-Threading acht geben müssen. Würde aktuelle ein anderer Thread als der UI-Thread Elemente auf der ObservableCollection<T> ändern, würden wir recht schnell Exceptions im UI bekommen

Gute Alternative:
Gerade aus Sicht von MVVM bietet sich ein anderer Typ perfekt für die gewünschte Implementierung an: ICollectionView. Dieser Typ ist schon von seinem Grundaufbau nahezu perfekt für MVVM geeignet, da er die Daten von der Repräsentation trennt. Der Einsatz von ICollectionView führt also quasi zu MVVM in MVVM. Sehen wir uns das genauer an.

Nachdem ich in meinem Code die List<> gegen eine ObservableCollection<> getauscht hatte, funktionierte es auf einmal und verhielt sich so, wie ich es wollte. Ich vermute aber, dass das trotzdem nicht richtig ist, wie ich es gemacht habe, da ich immer noch nix mit ViewModel gemacht habe.

Ich habe jetzt mal ein bisschen weiter gegoogelt und versucht, das zu implementieren. Jedoch leider ohne Erfolg 😦


using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows;

namespace Test
{
    public partial class MainWindow : Window
    {
        //Private Felder (Variablen)
        private ObservableCollection<User> userlist;

        public MainWindow()
        {
            InitializeComponent();
            ViewModel = new MainViewModel();
        }
        public MainViewModel ViewModel
        {
            get { return DataContext as MainViewModel; }
            set { DataContext = value; }
        }


        private void ClickEvent(object sender, RoutedEventArgs e)
        {
            userlist.Add(new User() { vorname = "Max", nachname = "Mustermann", alter = 15 });
        }

        private void dgLoaded(object sender, RoutedEventArgs e)
        {
            // ObservableCollection<> Instanziieren
            userlist = new ObservableCollection<User>();               
        }
    }

    public class MainViewModel : INotifyPropertyChanged
    {

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string info)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(info));
            }
        }

        public MainViewModel()
        {

        }

        private ObservableCollection<User> _userlist;

        public ObservableCollection<User> Userlist
        { 
            get { return _userlist; }
            set { Userlist = value; OnPropertyChanged("Userlist"); }
        }
    }

    class User
    {
        public string vorname { get; set; }
        public string nachname { get; set; }
        public int alter { get; set; }
    }
}

16.827 Beiträge seit 2008
vor 4 Jahren

Halte ehrlich gesagt den Artikel dazu für nicht 100% passend. ((Davon abgesehen sehe ich da nirgends die Aussage, dass die OC veraltet ist).

Die Nachteile, die er nennt, würden nicht auftreten, wenn er auf Reactive Extensions setzen würde bzw. dies nennen würde.

Reactive Extensions ist ein sehr weit verbreiteter und sehr beliebter Ansatz in allen Desktop-Technologien, um den State einer Anwendung zu halten.
Ursprünglich aus dem Cloud-Bereich kam es durch Angular und Co auch in viele andere Sprachen.
http://reactivex.io/

Durch die Architektur von RX kann es daher zu vielen Problemen die er nennt, zB das Threadverhalten oder die Datenmenge für das Chunking von Ansichten, gar nicht mehr kommen.

Wenn man neu in WPF ist, ist das aber meiner Meinung ein zu großer Schritt sofort auch RX zu integrieren.
Das überfordert die meisten.

Hier mal die im Web-Editor korrigierte Fassung mit den Hinweisen, die wir Dir hier in dem Thema schon gegeben haben.

namespace Test
{
    public partial class MainWindow : Window
    {
         private MainViewModel _viewModel = new MainViewModel
         {
             Users = new ObservableCollection<UserViewModel>();
         }


        public MainWindow()
        {
            InitializeComponent();
            DataContext = _viewModel;
        }

         // Unbedingt Events vermeiden!!
        private void ClickEvent(object sender, RoutedEventArgs e)
        {
            _viewModel.Users.Add(new UserViewModel() { Vorname = "Max", Nachname = "Mustermann", Alter = 15 });
        }
    }

    public class MainViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        private ObservableCollection<UserViewModel> _users;

        public ObservableCollection<UserViewModel> Users
        {
            get { return _users; }
            set { _users = value; 
		  NotifyPropertyChanged(); }
        }
    }

    public class UserViewModel : INotifyPropertyChanged // siehe Hinweis von MrSparke, den Du hier nicht beachtet hast
    {
	private string _vorname;
	private string _nachname;
	private int _alter;

        public string Vorname
        {
            get { return _vorname;}
            set { _vorname = value; NotifyPropertyChanged(); }
        }
        public string Nachname
        {
            get { return _nachname;}
            set { _nachname = value; NotifyPropertyChanged(); }
        }
        public int Alter
        {
            get { return _alter;}
            set { _alter = value; NotifyPropertyChanged(); }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

Man könnte jetzt noch C#6 / C#8 Syntaxzucker hinzufügen, um den Code noch schlanker zu gestalten; aber das kannste ja dann bei Bedarf selbst machen.

Wenn Du nur sagst "es funktioniert nicht" können wir trotzdem ohne Glaskugel jedoch nicht wissen, was laut Deinem Blickwinkel nicht funktioniert 😉

PS:

set { Userlist = value; OnPropertyChanged("Userlist"); }

Hier setzt Du die Property und nicht das Backing Field.

T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

Ok ich glaube langsam, es zu verstehen bzw. nachvollziehen zu können. Der C# Code hat mir jedenfalls schonmal lerntechnisch weitergeholfen.

Verstehe ich es richtig, dass wenn man in eine Klasse diese "NotifyPropertyChanged"-Methode einbaut, man es dann ViewModel nennt ?

Was ich noch nicht ganz nachvollziehen kann ist, warum man hier das UserViewModel in das MainViewModel stecken muss. Das UserViewModel gibt doch schon Meldung darüber, wenn sich ein Wert ändert !?

Und wieso hat es funktioniert, die "List<User> userlist" gegen ein "ObservableCollection<User> userlist" zu tauschen und dann einfach zu sagen "datagrid.ItemSource = userlist" und was ist falsch daran, bzw. welche Probleme bingt es mit sich. Oder ist das einfach nur die Regel, dass man das nicht machen soll ?

Dass die OC veraltet ist, habe ich wohl doch falsch verstanden, sorry.

Ich habe jetzt jedenfalls mal versucht, das DataGrid in XAML zu binden. Scheint aber wohl auch noch nicht so ganz richtig zu sein:


<DataGrid x:Name="datagrid" Margin="10,50,10,25" ItemsSource="{Binding Source=User, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Vorname" Binding="{Binding Vorname}"/>
                <DataGridTextColumn Header="Nachname" Binding="{Binding Nachname}"/>
                <DataGridTextColumn Header="Alter" Binding="{Binding Alter}"/>
            </DataGrid.Columns>
        </DataGrid>

Liebe Grüße und Durchhaltevermögen mit mir 😁

16.827 Beiträge seit 2008
vor 4 Jahren

Verstehe ich es richtig, dass wenn man in eine Klasse diese "NotifyPropertyChanged"-Methode einbaut, man es dann ViewModel nennt ?

Nichts zwangsläufig; in anderen Technologien als WPF gibt es auch ViewModels - aber dort gibts evtl. kein NotifyPropertyChanged.
ViewModel sind in Konzepten namentlich enthalten - nicht beschrieben durch funktionale Eigenschaften.

Es geht darum, dass alles, was in der View so enthalten ist, komplett getrennt von den anderen Schichten ist.
[Artikel] Drei-Schichten-Architektur

Was ich noch nicht ganz nachvollziehen kann ist, warum man hier das UserViewModel in das MainViewModel stecken muss. Das UserViewModel gibt doch schon Meldung darüber, wenn sich ein Wert ändert !?

Verstehst Du falsch.
Das UserViewModel benachrichtigt, wenn sich etwas von sich selbst geändert hat. Also von der einzelnen Instanz, zB Vorname.

Die Collection benachrichtigt, wenn sich innerhalb der Liste etwas geändert hat.
Und im MainWindowViewModel selbst gibt es die Benachrichtigung, sobald sich die Instanz Users geändert hat.
In diesem einfachen Beispiel ändert sich nur der Inhalt von Users; aber nicht Users selbst - in anderen Fällen wird das jedoch so sein.

Oder ist das einfach nur die Regel, dass man das nicht machen soll ?

List benachrichtigt niemand über die Änderung des Inhalts. ObservableCollection schon.
Der Rest ist vor allem die falsche Umsetzung von Bindings.

Ich habe jetzt jedenfalls mal versucht, das DataGrid in XAML zu binden. Scheint aber wohl auch noch nicht so ganz richtig zu sein:

Funktioniert denn das?


<DataGrid Margin="10,50,10,25" ItemsSource="{Binding Users}">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Vorname" Binding="{Binding Vorname}"/>
                <DataGridTextColumn Header="Nachname" Binding="{Binding Nachname}"/>
                <DataGridTextColumn Header="Alter" Binding="{Binding Alter}"/>
            </DataGrid.Columns>
        </DataGrid>

Ansonsten in das Output Window von Visual Studio schauen.
Da werden Binding Errors gelistet.

1.029 Beiträge seit 2010
vor 4 Jahren

Sollte es beim Binding nicht "Users" heißen?

16.827 Beiträge seit 2008
vor 4 Jahren

Danke, korrigiert.
Forum hat halt leider noch kein IntelliSense 😃

Edit: nun gesehen, dass es im Xaml von Threadstarter auch schon falsch ist.
Das erklärt natürlich, wieso es nicht funktionieren kann.

Aber das hätte man dann auch im Debugging und im Output Window gesehen.
Daher der Hinweis: immer mal selbst debuggen! 😃

C
26 Beiträge seit 2016
vor 4 Jahren

@TheLion092:

Ich würde dir MVVM Light empfehlen.
Das macht das Erstellen von MVVM-Applikationen wesentlich leichter, da Du Dir z.B. das hantieren mit INotifyPropertyChanged ersparst.

T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

Ok, mit Hilfe vom Output Windows habe ich es jetzt zum Laufen bekommen, danke erstmal dafür 🙂

Wenn ich der ?-> Klasse <-? "UserViewModel" jetzt eine weiteres Attribut vom Typ ObservableCollection<> hinzufügen wollen würde, dann sollte man dafür wieder ein ViewModel erstellen, richtig ?
Und wenn ich dann am Ende im DataGrid, Attribute aus der neuen ObservableCollection<> eine Ebene tiefer, nennen wir sie jetzt mal ObservableCollection<ADgroups> anzeigen lassen möchte, dann kann ich in XAML den entsprechenden Context oder Pfad auswählen ?

also z.B. so ? (Es ist mit Sicherheit nicht richtig)


<DataGrid Margin="10,50,10,25" IsReadOnly="True" AutoGenerateColumns="False" ItemsSource="{Binding Users, UpdateSourceTrigger=PropertyChanged, Mode=OneWay}">
    <DataGrid.Columns>
        <DataGridTextColumn Header="Vorname" Binding="{Binding Vorname}"/>
            <DataGridTextColumn Header="Nachname" Binding="{Binding Nachname}"/>
            <DataGridTextColumn Header="Alter" Binding="{Binding Alter}"/>

            <!-- Neues Binding eine Ebene tiefer -->
            <DataGridTextColumn Header="Anzahl AD-Gruppen" Binding="{Binding UserViewModel/gruppenanzahl}"/>
        </DataGrid.Columns>
</DataGrid>

nun gesehen, dass es im Xaml von Threadstarter auch schon falsch ist.

Ja, genau das war der Fehler 😁

16.827 Beiträge seit 2008
vor 4 Jahren

Mach einfach mal ordentlich nen Tutorial durch - dann wird das Thema hier auch nicht zum Endlosthread.

Erster Treffer in der Google Suche zu Deinem Problem, Stichwort nested property.
WPF: How to bind to a nested property?
👍

T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

Alles klar, werde ich machen. Dann erstmal vielen Danl für eure Hilfe und eure Geduld =)

T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

So, nach einiger Zeit und viel nachlesen denke ich, dass ich MVVM ganz gut in meinem Projekt umgesetzt habe. Gerne dürft ihr mal drüberschauen, ob ich alles richtig gemacht habe, oder mir Tipps geben, wo man noch was besser machen kann.

Jedoch habe ich auch noch eine kleine Stelle, bei der ich nicht weiterkomme.
Ich habe jetzt eine eigene Klasse mit dem Namen "UserlistLogic" erstellt in der Methoden stecken, die beim Klicken eines Buttons ausgelöst werden sollen (Einfach um die Button-Logik auszulagern). Dazu habe ich im ViewModel RelayCommands erstellt. Ebenso habe ich im ViewModel eine ObservableCollection (Userlist) erstellt, sowie die UserlistLogic Klasse deklariert und instanziiert. Leider kann ich von der UserlistLogic Klasse nicht auf die erstellte ObservableCollection (Userlist) im ViewModel zugreifen. Ich denke nicht, dass das besonders schwierig ist, aber ich komme leider nicht auf die Lösung.

Kann mir da eventuell jemand weiterhelfen ?

Hier der Link zum Repository:
https://github.com/TheLion092/NTFS-Tool

Liebe Grüße

4.938 Beiträge seit 2008
vor 4 Jahren

Die Logik-Klasse sollte auch keinen Zugriff auf die ViewModel-Klasse haben (denn sie sollte komplett unabhängig von der verwendeten UI-Technologie entwickelt sein, also daß sie z.B. auch in einem Konsolenprogramm oder WebService verwendet werden kann).
Warum meinst du denn das zu benötigen?

T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

Hmm.. da der Setter der ObservableCollection (Userlist) ein "NotifyPropertyChanged()" hat, müsste ich ja wenn ich die Userlist in die Logik-Klasse verschieben würde, dort auch das INotifyPropertyChanged implementieren. Ich bin mir nicht sicher, ob das richtig ist. Die Überlegung war einfach, dass mal relativ viel Logik geschrieben werden soll, weshalb ich das auslagern will. Oder ist es üblich, dass man die gesamte Button-Logik mit in das ViewModel schreibt ? Oder verstehe ich wieder was falsch ?

Liebe Grüße

T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

Ich denke, ich habe es jetzt hinbekommen, jedenfalls funktioniert es. Ich übergebe beim RelayCommand im ViewModel, der Methode aus der Logik-Klasse jetzt einfach die ObservableCollection aus dem ViewModel und returne das ganze wieder. Ob das jedoch richtig ist, weiß ich trotzdem nicht. Mich würde also interessieren, wie man mit Code-behind von Buttons etc. am besten umgeht. Schreibt man das alles ins ViewModel oder kann man das so wie ich in eine Logik-Klasse auslagern ?

Ausßerdem habe ich jetzt das "PropertyChangedEvent" und den "RelayCommand" in einen Helpers-Ordner verschoben und implementiere im ViewModel und im Model die Klasse "PropertyChangedEvent". Ist das so in Ordnung ?

Liebe Grüße

5.658 Beiträge seit 2006
vor 4 Jahren

Das PropertyChangedEvent wird nur für die Darstellung benötigt, und ist daher Teil des ViewModels, nicht des Models.

Die Commands sind Teil des ViewModels, daher sind dafür keine Helper-Klassen oder -Namespaces nötig.

Dort, wo du das RelayCommand erstellst, hast du doch Zugriff auf die Collection. Wenn nicht, verstehe ich deine Herangehensweise nicht. Wenn die Logik des Commands umfangreicher ist, dann kannst du anstatt des RelayCommands eine eigene Command-Klasse implementieren, der du die benötigte Collection im Konstruktor übergibst. Eine eigene "Logik-Klasse" ist auch da nicht erforderlich, dafür gibt es ja bereits die Commands. Siehe auch dazu diesen Artikel: [Artikel] MVVM und DataBinding

Bitte beachte zukünftig [Hinweis] Wie poste ich richtig?, besonders Punkt 1.2: Nur ein Thema pro Thread.

Weeks of programming can save you hours of planning

1.040 Beiträge seit 2007
vor 4 Jahren

Kurze Ergänzung:
Ein NotifyPropertyChanged ist bei einer Property vom Typ ObservableCollection<T> i.d.R. nicht notwendig, da man die Liste nur 1x anlegt.

Ein Add oder Remove wird bereits durch die ObservableCollection selbst überwacht.
Nur wenn man die komplette Liste neu setzt, benötigt man es.

namespace Test
{
    public class MainViewModel : INotifyPropertyChanged
    {
        public MainViewModel()
        {
            Users = new ObservableCollection<UserViewModel>();
        }

        ...

        public ObservableCollection<UserViewModel> Users { get; }

        ...
    }
}
T
TheLion092 Themenstarter:in
17 Beiträge seit 2018
vor 4 Jahren

Vielen dank an euch, für die Hilfe und Hinweise 😃