codingfreaks

codingfreaks

Experiencing Microsoft

  • Archive
  • Tools
  • About
  • Privacy
  • RSS-Feed
  • Github
  • Youtube

WPF und MVVM richtig einsetzen - Teil 5

Alexander Schmidt  |  August 14, 2017

Wie immer hatte ich mir vorgenommen, nicht zu viel Zeit bis zum nächsten Beitrag verstreichen zu lassen. Das hat nicht gut funktioniert. Nun aber endlich die Fortsetzung der WPF-MVVM-Serie. Dieses Mal möchte ich mich etwas näher mit Listen beschäftigen. Im Fokus werden Typen, wie ObservableCollection<>, BindingList<>, ICollectionView und andere stehen. Ich habe bewusst wieder etwas mehr geschrieben als unbdedingt notwendig gewesen wäre, um in altbewährter Tradition möglichst auch die Hintergründe zu erläutern.


Links

  • Teil 1 mit den Grundlagen
  • Teil 2 zu INotifyPropertyChanged
  • Teil 3 zu IDataErrorInfo
  • Teil 4 zu Binding von Listen
  • Beispiel-Projekt-Repo auf GitHub
  • Guter Artikel zu CollectionView von Andy O’Neill

Screencast

Einleitung

Eine der häufigsten Aufgaben im Rahmen der Entwicklung von User Interfaces im Business-Bereich ist die Umsetzung von sogenannten Master-Detail-Ansichten. Die Master-Ansicht ist dabei meist eine Liste von Elementen eines bestimmten Typs. Aus dieser wählt der Benutzer ein Element aus, welches ihm dann in der Detail-Ansicht präsentiert wird.

Übersetzt auf WPF bedeutet dies, dass wir in einem Listen-Steuerelement (ListBox, ListView, Grid usw.) den Master umsetzen und in einem “normalen” Dialog oderBereich den Detail-View. Die Frage ist nun, wie man dieses Muster in WPF und MVVM Light möglichst effizient und vor allem technisch richtig einsetzt.

Normale Listen

Gehen wir zunächst einmal intuitiv vor und beleiben dabei in unserem Beispiel-Code. Wir verfügen über den Typ PersonModel. Der intuitiv erste Schritt für eine Master-Ansicht würde code-seitig wahrscheinlich Folgendes ergeben:

Listing 1
public List<PersonModel></PersonModel> Persons{ get; set; }

Wenn wir diese Eigenschaft in unser MainViewModel integrieren, können wir z.B. ein DataGrid in MainWindow.xaml wie folgt dagegen binden:

Listing 2
<DataGrid ItemsSource="{Binding Persons}" />

Die Eigenschaft ItemsSource erbt das Control dabei vom Typ ItemsControl. Eine schnelle Suche mit ReSharper ergibt folgende Standard-Typen, die von ItemsControl erben:

  • System.Windows.Controls.ComboBox
  • System.Windows.Controls.Primitives.DataGridColumnHeadersPresenter
  • System.Windows.Controls.Primitives.MenuBase
  • System.Windows.Controls.ContextMenu
  • System.Windows.Controls.DataGrid
  • System.Windows.Controls.DataGridComboBoxColumn.TextBlockComboBox
  • System.Windows.Controls.HeaderedItemsControl
  • System.Windows.Controls.ListBox
  • System.Windows.Controls.ListView
  • System.Windows.Controls.Menu
  • System.Windows.Controls.MenuItem
  • System.Windows.Controls.Primitives.DataGridCellsPresenter
  • System.Windows.Controls.Primitives.MultiSelector
  • System.Windows.Controls.Primitives.Selector
  • System.Windows.Controls.Primitives.StatusBar
  • System.Windows.Controls.TabControl
  • System.Windows.Controls.ToolBar
  • System.Windows.Controls.TreeView
  • System.Windows.Controls.TreeViewItem
  • MS.Internal.Documents.Application.ZoomComboBox
  • MS.Internal.Documents.FindToolBar

Anders ausgedrückt: alle diese Typen können erst einmal für Master-Ansichten verwendet werden, weil sie in der Lage sind, eine Menge von Elementen eines Typs über die ItemsSource zu binden.

So gut wie alle Dritthersteller-Komponenten, die ich kenne nutzen übrigens ebenfalls ItemsControl als Basis für ihre Grids, Listen etc.

Was aber ergibt denn nun das Beispiel aus den oberen beiden Listings? Damit wir das etwas besser verstehen können, habe ich den Konstruktur von MainViewModel um folgende Logik erweitert:

Listing 3
var personList = new List<PersonModel>();
for (var i = 0; i < 100; i++)
{
    personList.Add(
        new PersonModel
        {
            Firstname = Guid.NewGuid().ToString("N").Substring(0, 10),
            Lastname = Guid.NewGuid().ToString("N").Substring(0, 10)
        });
}
Persons = new List<PersonModel>(personList);

(Die umständliche Initilisierung mit List in List erkläre ich später.)

Wenn wir das Projekt jetzt ausführen, erscheint das Grid ca. so:

Abb. 1: DataGrid mit Beispieldaten
Abb. 1: DataGrid mit Beispieldaten

Da wir alle Optionen im Standard belassen haben, hat das Control seine Spalten selbst erzeugt und benannt und zeigt somit alle Eigenschaften an, die unser PersonModel aktuell innehat (auch die Berechneten). Ganz am unteren Ende (im Screenshot nicht zu sehen) befindet sich dann noch eine leere Zeile, die es uns erlaubt, neue Einträge vorzunehmen.

Warum dieser Ansatz nicht reicht

Man könnte meinen, der Artikel ist jetzt am Ende angelangt. Ist er nicht und in Wahrheit geht es jetzt erst los. Eine kleine Abwandlung des Codes soll das Problem zeigen, die dieser Ansatz hat. Dazu ergänzen wir das MainViewModel nun um ein neues Command:

Listing 4
public RelayCommand AddPersonCommand { get; }

und initialiseren es im Konstuktor des MainViewModel:

Listing 5
AddPersonCommand = new RelayCommand(() => Persons.Add(new PersonModel()));

Zum Schluss rufe ich das Command im UI über ein Menu-Item in MainWindow.xaml auf:

Listing 6
<MenuItem Command="{Binding AddPersonCommand, Mode=OneWay}" Header="Add Person" />

Ich habe den Code aus Listing 3 noch so abgeändert, dass nur noch 10 Elemente gefüllt werden.

Abb. 2: Liste zeigt Änderungen nicht an

Wie man sieht, zeigt das DataGrid keinerlei Reaktion. Das liegt ganz einfach daran, dass List<T> keine Logik enthält, um einen Aufrufer über neue oder gelöschte Elemente zu informieren. Mit anderen Worten bekommt das WPF-Binding einfach nicht mit, dass es nun 11 Elemente in Persons gibt.

Die Lösung ganz einfach

Es gibt einen .NET-Typen, der unser Problem ohne Weiteres lösen kann: ObservableCollection<T>. Sehen wir uns das einmal an, indem wir im MainViewModel einfach die Property Persons in eine ObservableCollection<PersonModel> umwandeln:

Listing 7
public ObservableCollection<PersonModel> Persons{ get; set; }

Das müssen wir nun im Konstructor (siehe Listing 3) an einer stelle nachziehen (letzte Zeile aus Listing 3):

Listing 8
Persons = new ObservableCollection<PersonModel>(personList);

Das erklärt nebenbei nun auch, warum der Code in Listing 3 so umständlich ist :-). Führen wir das Programm nun aus, verhält es sich wie erwartet:

Abb. 3: Liste zeigt Änderungen an

Was ist nun der Unterschied zwischen List<T> und ObservableCollection<T>? Letzteres implementiert 2 Interfaces:

  • INotifyCollectionChanged
  • INotifyPropertyChanged

Das erklärt dann einiges. INotifyCollectionChanged kümmert sich darum, dass ein Ereignis CollectionChanged aufgerufen wird, wenn die Anzahl der Elemente sich ändert. Unser alter Bekannte INotifyPropertyChanged wird genutzt, damit die abhängigen Eigenschaften, wie z.B. Count oder Items auf der Liste ihre Änderung ebenfalls propagieren.

An dieser Stelle lässt sich unser Beispiel sehr schön rund machen, indem wir das XAML für das DataGrid um ein Binding erweitern:

Listing 9
<DataGrid ItemsSource="{Binding Persons}" SelectedItem="{Binding PersonModel, Mode=TwoWay}" />

Was ich mit der SelectedItem-Eigenschaft eines ItemsControl umsetze ist, dass das aktuell ausgewählte Element im entsprechenden ViewModel in einer Eigenschaft gespeichert werden soll. Diese Eigenschaft hatten wir in unseren vorhergehenden Teilen der Serie bereits im MainViewModel umgesetzt:

Listing 10
public PersonModel PersonModel { get; set; } = new PersonModel();

Durch die Ergänzung aus Listing 9 entsteht nun ohne viel Aufwand eine voll funktionale Master-Detail-Ansicht:

Abb. 4: Master-Detail funktioniert

Limitierungen

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.

Schlechte Alternative

Ein Typ, der immer wieder genannt wird, um das Pattern angeblich besser zu implementieren ist BindingList<T>. Um es gleich vorweg zu nehmen: Benutzt es nicht! BindingList<T> ist extrem ressourcen-hungrig. Ein Beispiel für die schlechte Performance von BindingList<T> ist, dass es für jedes PropertyChanged-Event jedes Elements der Liste die gesamte Liste durchgeht, um die Indizes der Elemente zu aktualisieren. Man kann sich leicht vorstellen, welche Auswirkungen das haben kann.

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.

Unser Sample kann zum aktuellen Zeitpunkt recht simpel auf ICollectionView getrimmt werden. Im folgenden Code-Snippet sind alle notwendigen Änderungen am MainViewModel komplett dargestellt (der komplette Quellcode kann hier eingesehen werden):

Listing 11
// changed to private
private ObservableCollection<PersonModel> Persons { get; }

// new property
public ICollectionView PersonsView { get; }

// changes in constructor
PersonsView = CollectionViewSource.GetDefaultView(Persons);
PersonsView.CurrentChanged += (s, e) =>
{
    RaisePropertyChanged(() => PersonModel);
};

// changed property
public PersonModel PersonModel
{
    get => PersonsView.CurrentItem as PersonModel;
    set
    {
        PersonsView.MoveCurrentTo(value);
        RaisePropertyChanged();
    }
}  

Zunächst wird die Persons-Eigenschaft vor dem UI versteckt indem wir die Eigenschaft als private deklarieren. Somit verhindern wir das versehentliche Binden direkt gegen diese Eigenschaft. Als nächstes bringen wir eine neue Eigenschaft vom Typ ICollectionView an den Start. Gegen diese soll das UI später binden.

Damit diese Eigenschaft PersonsView auch einen sinnvollen Wert erhält, müssen wir im Konstruktor nun die statische Methode CollectionViewSource.GetDefaultView() benutzen. Wir übergeben ihr unsere interne ObservableCollection<PersonModel>, damit daraus eine Repräsentation für das UI gebaut wird. Hierbei ist wichtig zu verstehen, dass ICollectionView es uns prinzipiell erlaubt, beliebig viele Sichten auf die gleichen Daten zu definieren. GetDefaultView() ist hier die einfachste (und am hähfigsten benutzte) Methode, einen View zu erzeugen.

Im letzten Schritt sorgen wir nun dafür, dass das ViewModel selbst “weiß”, wie man aus der ICollectionView das aktuell selektierte Element ermittelt. Um diesen letzten Schritt vollständig zu verstehen, sehen wir uns die notwendigen XAMl-Anpassungen in MainWindow.xaml an:

Listing 12
<DataGrid ItemsSource="{Binding PersonsView}" IsSynchronizedWithCurrentItem="True" EnableRowVirtualization="True" />

Wie man sieht, binden wir das Grid nun an die Eigenschaft PersonsView. Die WPF-Eigenschaft IsSynchronizedWithCurrentItem dient dazu, dass das Control (DataGrid) der ICollectionView “mitteilt”, wenn sich das selektierte Elemente ändert und anders herum darauf reagiert, wenn dies im Quellcode durchgeführt wird. Die Eigenschaft EnableRowVirtualization sorgt letztlich dazu, dass die ICollectionView dem Control dabei “hilft” nur die Daten anzuzeigen, die laut Fenstergröße, Scrollposition etc. aktuell sichtbar sind. Das hat natürlich vor allem Performance-Verbesserungen zur Folge.

IsSynchronizedWithCurrentItem erklärt nun auch die neue Gestaltung der Eigenschaft PersonModel aus Listing 11. Der Getter der Eigenschaft nutzt ICollectionView.CurrentItem, um jeweils das aktuell selektierte Element zu liefern. Deshalb registrieren wir uns im Konstruktor am Ereignis ICollectionView.CurrentChanged, damit wir jedes Mal, wenn dies im UI passiert im ViewModel mitteilen, dass sich der Wert von PersonModel geändert hat. Der Setter von PersonModel dient für die Rückwärts-Synchronisation im Falle das wir im ViewModel ein neues Element selektieren möchten.

Wir können diese Änderungen nun sehr gut testen, indem wir unser AddPersonCommand aus Listing 5 ein wenig umbauen:

Listing 13
AddPersonCommand = new RelayCommand(
    () =>
    {
        var newPerson = new PersonModel();
        Persons.Add(newPerson);
        PersonModel = newPerson;
    });

Sehen wir uns das Ergebnis einmal an:

Abb. 5: Master-Detail nutzt nun ICollectionView

Wir sehen hier, dass die Synchronisierung durch IsSynchronizedWithCurrentItem funktioniert und dass der Aufruf des Setters von MainViewModel.PersonModel in Zeile 6 aus Listing 13 tatsächlich direkt im UI sichtbar wird. Allein diese Funktionalität zeigt, wieviel Logik uns ICollectionView bringt.

Detaillierte Möglichkeiten durch Implementierungen

Ich benutze in diesem Sample erst einmal immer das Interface ICollectionView. Dieses Interface wird von einigen Typen aus PresentationFramework implementiert, die man direkt verwenden kann, um mehr Funktionalität zu erhalten:

  • MS.Internal.Controls.InnerItemCollectionView
  • MS.Internal.Data.CollectionViewProxy
  • MS.Internal.Data.CompositeCollectionView
  • MS.Internal.Data.EnumerableCollectionView
  • System.Windows.Controls.ItemCollection
  • System.Windows.Data.BindingListCollectionView
  • System.Windows.Data.CollectionView
  • System.Windows.Data.ListCollectionView

Vor allem die letzten beiden Typen können genutzt. ListCollectionView erbt übrigens von CollectionView. Sieht man sich die durch diese Typen ergänzend zu ICollectionView definierten Methoden und Eigenschaften an, wird schnell klar, dass man sie wahrscheinlich der reinen ICollectionView vorziehen wird. Z.B. kennt die ICollectionView keine Count-Eigenschaft. Sie kennt außerdem keine Add-Methode. Diese und andere Limitierungen sorgen dafür, dass mein Quellcode im jetzigen Stadium immer auf die ObservableCollection zugreift, wenn die reinen Daten benötigt werden. Dies könnte ich mir sparen, wenn ich z.B. ListCollectionView direkt verwende, was übrigens die meisten Projekte tun.

Noch mehr Vorteile durch ICollectionView

Die Trennung von Präsentation und Daten von ICollectionView erlaubt noch weitere nette Umsetzungen. Vorab sei hier nur erwähnt, dass die meisten der mir bekannten Dritthersteller-Controls ICollectionView erkennen und nativ unterstützen. Das wird vor allem dann interessant, wenn wir uns ein paar Möglichkeiten von ICollectionView vor Augen führen.

Sortierung im Backend

Das Sortieren wird bei Nutzung z.B. von ObservableCollection<T> einfach dem Control überlassen. Das bedeutet, dass das Steuerelement direkt auf den vorhandenen Daten aber halt im UI die Sortierung übernimmt. Anders ist dies bei Einsatz von ICollectionView. Wir können hier bestimmte Views fix sortieren (wenn man z.B. Sortierung über das Grid nicht wünscht). In unserem Beispiel könnte das wie folgt aussehen:

Listing 14
PersonsView = CollectionViewSource.GetDefaultView(Persons);
PersonsView.CurrentChanged += (s, e) =>
{
    RaisePropertyChanged(() => PersonModel);
};
PersonsView.SortDescriptions.Clear();
PersonsView.SortDescriptions.Add(new SortDescription(nameof(PersonModel.Firstname), ListSortDirection.Ascending));

Die Zeilen 6 und 7 werden im Konstruktor nach den beiden anderen Befehlen eingefügt. Das Grid zeigt nun beim Öffnen direkt die sortierte Version. Das Problem hier ist nun, dass die Sortierung sich nicht anpasst, wenn wir Änderungen vornehmen:

Abb. 6: Sortierungsproblem

ICollectionView hat für diese Funktionalität die Methode Refresh() im Angebot, die wir nun nutzen wollen.

BY SIDE: DIE OBSERVABLECOLLECTION-LÜGE

An dieser Stelle möchte ich kurz auf einen aus meiner Sicht heftigen Fehler von ObservableCollection<T> eingehen. Sehen wir uns folgenden Screenshot an:

Abb. 7: Die Lüge
Abb. 7: Die Lüge

Ich habe die “Lüge” im Screenshot markiert. Das Problem ist, dass ObservableCollection<T> das Ereignis CollectionChanged eben NICHT wirft, wenn sich Elemente darin ändern, sondern nur, wenn neue hinzu kommen oder gelöscht werden.

BY SIDE ENDE

Die Lösung ist Folgende:

Listing 15
Persons = new ObservableCollection<PersonModel>(personList);
foreach (var item in Persons)
{
    item.PropertyChanged += PersonsOnPropertyChanged;
}
PersonsView = CollectionViewSource.GetDefaultView(Persons);    
PersonsView.CurrentChanged += (s, e) =>
{
    RaisePropertyChanged(() => PersonModel);
};    
PersonsView.SortDescriptions.Clear();
PersonsView.SortDescriptions.Add(new SortDescription(nameof(PersonModel.Firstname), ListSortDirection.Ascending));
Persons.CollectionChanged += (s, e) =>
{
    foreach (INotifyPropertyChanged added in e.NewItems)
    {
        added.PropertyChanged += PersonsOnPropertyChanged;
    }
    foreach (INotifyPropertyChanged removed in e.OldItems)
    {
        removed.PropertyChanged -= PersonsOnPropertyChanged;
    }
};

private void PersonsOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    PersonsView.Refresh();
}

Der Event-Handler unten dient nur dazu, jedes Mal Refresh für das ICollectionView aufzurufen, wenn eines der Elemente in Persons eine Eigenschafts-Änderung erfährt. Mit dieser Änderung verhält sich die Anwendung nun wie folgt:

Abb. 8: Sortierung funktioniert

Filterung im Backend

ICollectionView erlaubt es, dass die Filterung von Datensätzen im ViewModel definiert und ausgeführt wird. Besonders hier zeigt sich, dass die Unterteilung in mehrere Views pro Datenquelle sehr sinnvoll sein kann. Ein Beispiel, das ich hier aus Platzgründen nicht weiter ausführen werde ist das Vorladen von sagen wir 100 Datensätzen von einem Webservice in einen ersten View und das Nachladen aller z.B. 10.000 Datensätze in einen zweiten. Somit kann man dem Benutzer schon einmal Daten zeigen während man im Hintergrund nachlädt und das alles mit einer internen ObservableCollection<T>.

Gruppierung im Backend

Genau wie bei Sortierung und Filterung kann auch die Gruppierung von Daten im ViewModel erfolgen.

IDataErrorInfo am Beispiel des DataGrid

Nachdem wir nun festgestellt haben, dass man unbdefingt ICollectionView verwenden sollte, um Listen von Elementen im UI zu binden soll als letzter Abschnitt in diesem Beitrag noch kurz auf die Integration von IDataErrorInfo in Listen eingegangen werden. Ich mache das hier am Beispiel eines DataGrid.

Alle dazu notwendigen Anpassungen auf Seiten des ViewModels haben wir in Teil 3 dieser Serie bereits implementiert. Das heißt, wir müssen “nur noch” im MainWindow.xaml Hand anlegen:

Listing 17
<DataGrid ItemsSource="{Binding PersonsView}" IsSynchronizedWithCurrentItem="True"
        EnableRowVirtualization="True">
    <DataGrid.Resources>
        <Style x:Key="ErrorTextInput" TargetType="{x:Type TextBox}">
            <Setter Property="Padding" Value="-2" />
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="Background" Value="Red" />
                    <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}" />
                </Trigger>
            </Style.Triggers>
        </Style>
    </DataGrid.Resources>
    <DataGrid.RowValidationErrorTemplate>
        <ControlTemplate>
            <Grid Margin="0,-2,0,-2"
                ToolTip="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type DataGridRow}}, Path=(Validation.Errors)[0].ErrorContent}">
                <Ellipse StrokeThickness="0" Fill="Red"
                        Width="{TemplateBinding FontSize}"
                        Height="{TemplateBinding FontSize}" />
                <TextBlock Text="!" FontSize="{TemplateBinding FontSize}"
                        FontWeight="Bold" Foreground="White"
                        HorizontalAlignment="Center" />
            </Grid>
        </ControlTemplate>
    </DataGrid.RowValidationErrorTemplate>
    <DataGrid.Columns>
        <DataGridTextColumn Header="Firstname"
                        Binding="{Binding Firstname, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
                        EditingElementStyle="{StaticResource ErrorTextInput}" />
        <DataGridTextColumn Header="Lastname"
                        Binding="{Binding Lastname, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged}"
                        EditingElementStyle="{StaticResource ErrorTextInput}" />
        <DataGridTextColumn Header="Birthday"
                        Binding="{Binding Birthday, ValidatesOnDataErrors=True, UpdateSourceTrigger=PropertyChanged, StringFormat=d}"
                        EditingElementStyle="{StaticResource ErrorTextInput}" />
        <DataGridTextColumn Header="Age"
                        Binding="{Binding Age}"
                        IsReadOnly="True" />
    </DataGrid.Columns>
    <DataGrid.ContextMenu>
        <ContextMenu>
            <MenuItem Header="Set date" Command="{Binding SetSomeDateCommand}"
                    CommandParameter="{Binding PersonModel}" />
        </ContextMenu>
    </DataGrid.ContextMenu>
</DataGrid>

Gehen wir das nun einmal Schritt für Schritt durch. Zunächst speichere ich innerhalb der Ressourcen des DataGrid einen Style mit dem Schlüssel “ErrorTextInput”. Diesen verwenden wir später innerhalb der Spalten um dem Control mitzuteilen, dass es diesen Style anwenden soll, wenn es im Editier-Modus eine Textbox zeigte das hier am Beispiel eines DataGrid. Der Style selbst definiert eine rote Hintergrundfarbe und einen Tooltip, der einfach den Inhalt des ersten Fehlers ausgibt.

Als nächstes überschreibe ich das vom DataGrid bereits angebotene RowValidationErrorTemplate. Dieses wird immer dann verwendet, wenn innerhalb einer Zeile mindestens ein Fehler durch IDataErrorInfo gemeldet wird. Im GIF unten kann man dann sehen, wie es ein Ausrufe-Zeichen in einem rotem Kreis zeigt und auf dem Kreis einen Tooltip bringt. Das ist sozusagen die Entsprechung für die Fehlermeldung, wenn wir nicht im Editiriermodus sind.

Dann definiere ich die Spalten, die letztlich angezeigt werden sollen per Hand und gebe jeder Spalte wie bereits oben erwähnt ein EditingElementStyle, um das Aussehen und Verhalten der Editierbox anzupassen.

Ganz zum Schluss habe ich noch ein Kontextmenu eingebunden, dass ein neues SetSomeDataCommand im MainViewModel nutzt:

Listing 17
SetSomeDateCommand = new RelayCommand<PersonModel>(person => person.Birthday = DateTime.Now.AddYears(-20));

// new command
public RelayCommand<PersonModel> SetSomeDateCommand { get; }

Alles zusammen verhält sich dann ungefähr so:

Abb. 8: Alles zusammen

Fazit

Man kann viel falsch machen beim Binden von Listen über WPF und MVVM. Wie immer hat Microsoft aber glücklicherweise auch an dieses Einsatzgebiet gedacht und dieser Artikel sollte zum einen den Schubs in die richtige Richtung geben und zum anderen aufzeigen, wie man sich dieser Schritt für Schritt nähert.


Alexander Schmidt

Written by Alexander Schmidt who lives and works in Magdeburg building useful things. You should follow him on Youtube