Grundlagen - MVVM-Anwendungs-Struktur

    • WPF

    Es gibt 2 Antworten in diesem Thema. Der letzte Beitrag () ist von ErfinderDesRades.

      Grundlagen - MVVM-Anwendungs-Struktur

      ("MVVM"-Pattern bedeutet: "Model-View-Viewmodel")
      Anbei mal eine Sample-Solution, die die grundlegende Architektur einer Wpf-Anwendung nach MVVM verdeutlichen soll - zumindest eine übliche Variante.

      Die Dateistruktur weist eine klare Trennung von Viewmodel und View auf, zumal ja eh deren Datei-Endung verschieden ist.
      Ein Prinzip ist auch, dass es ein MainViewmodel gibt, wo alles "zusammenfließt", und (meist) mehrere partielle Viewmodel-Klassen, die Teilstrukturen definieren - hier etwa einen Person-Datensatz.
      Und weil im Bild noch Platz war hab ich gleich die Application.Xaml gezeigt, und wie da in den Resourcen eine MainModel-Instanz hinterlegt ist, an die im weiteren in der kompletten Anwendung in verschiedenster Weise angebunden werden kann.

      In diesem Beipiel spiegelt sich die Struktur MainModel -> Person auch im View wieder, als MainWindow -> uclPerson, wobei uclPerson, ein UserControl ist, was speziell eine Person präsentiert:

      Zu beachten, dass hier bereits die Designer-Vorschau Test-Daten zeigt - für den Entwickler ein wertvolles Feedback, ob die Bindings stimmen.
      Diese Designer-Daten-Preview ist nicht selbstverständlich: Legt man seine Anwendungs-Struktur ungeschickt an, oder generiert zur Designzeit keine TestDaten, dann bringt man sich um den Genuss dieser Unterstützung.
      Auch gibt es Szenarien, in denen diese Unterstützung leider überhaupt nicht in Gang gebracht werden kann - Xaml ist im Grunde eine lausig zu programmierende Sprache voll von Kinderkrankheiten.
      Dennoch sollte man von Anfang an gucken, sich diese Features zu erhalten, und nur notgedrungen drauf verzichten.
      Sonst lernt man im Grunde "behindertes Xaml", vergleichbar dem "behinderten Vb.Net", was man lernt, wenn man die Visual Basic - Empfohlene Einstellungen nicht tätigt.

      Ja, auch das MainWindow glänzt mit Test-Data-Preview, und zeigt mir an, dass die Listbox links richtig gebunden ist, denn sie zeigt korrekt die Namen meiner Person-Test-Daten:



      Die Sample-Anwendung
      macht im Grunde nichts besonderes: eine Liste von Person-Datensätzen wird geladen, bearbeitet, rückgespeichert.
      Dabei - wie obiges Bild andeutet - kann man eine Person links auswählen, und rechts dann in Detail-Ansicht bearbeiten - also ein Detail-View (wem der Begriff "DetailView" nicht sonnenklar ist, der folge bitte dem Link).
      Eine kleine Besonderheit ist die Person.Rate-Property, die vorsieht, jeder Person eine "Bewertung" von 1 - 9 zuzuordnen.

      Hier mal das Person-Viewmodel:

      VB.NET-Quellcode

      1. Public Class Person : Inherits NotifyPropertyChanged
      2. Public Property cmdUpRate() As New RelayCommand(Sub() Rate += 1, Function() _Rate < 10)
      3. Public Property cmdDownRate() As New RelayCommand(Sub() Rate -= 1, Function() _Rate > 1)
      4. Private _Rate As Integer = 5
      5. Public Property Rate() As Integer
      6. Get
      7. Return _Rate
      8. End Get
      9. Set(ByVal value As Integer)
      10. ChangePropIfDifferent(value, _Rate)
      11. End Set
      12. End Property
      13. Private _Name As String
      14. Public Property Name() As String
      15. Get
      16. Return _Name
      17. End Get
      18. Set(value As String)
      19. ChangePropIfDifferent(value, _Name)
      20. End Set
      21. End Property
      22. Private _Phone As String
      23. Public Property Phone() As String
      24. Get
      25. Return _Phone
      26. End Get
      27. Set(value As String)
      28. ChangePropIfDifferent(value, _Phone)
      29. End Set
      30. End Property
      31. Private _BirthDay As DateTime
      32. Public Property BirthDay() As DateTime
      33. Get
      34. Return _BirthDay
      35. End Get
      36. Set(value As DateTime)
      37. ChangePropIfDifferent(value, _BirthDay)
      38. End Set
      39. End Property
      40. End Class
      Das schöne am MVVM ist, dass im Viewmodel-Code keinerlei Button, Listbox, Combobox oder whatever auftauchen, sondern der Datensatz bleibt genau das, was er ist: ein Datensatz.
      Mit hauptsächlich den vier Properties: Name, Phone, BirthDay und Rate, und diese Properties enthalten im Setter auch keine weitere Logik, als nur das PropertyChanged-Event zu senden, wenn die Property changed.

      Aber es gibt noch 2 weitere Properties, und die sind speziell für Logik ausgelegt: cmdUpRate, cmdDownRate (ja - das sind Properties! 8| )
      Das ist eine Wpf-Besonderheit, dass ein Viewmodel Logik oft als Property bereitstellt, anstatt wie üblich als Sub - nämlich dann, wenn Xaml ins Viewmodel hineinwirken soll. Denn Xaml kann nur an Properties binden.
      Und so stellt Person diese beiden Properties bereit.

      So kann Logik sehr gezielt zugeordnet werden: Etwa im MainModel gibt es Commands zum Laden und Speichern, aber die Commands zum Up-/Down-Raten einer einzelnen Person hab ich ins Person-Viewmodel verfrachtet - das Mainmodel muss ich damit nicht überfrachten. Hier mal das MainModel komplett:

      VB.NET-Quellcode

      1. Public Class MainModel : Inherits MainModelBase(Of MainModel)
      2. Private Const _DataSeparator As Char = Microsoft.VisualBasic.ChrW(&HF018)
      3. Private _DataFile As New FileInfo("..\..\PersonData.txt")
      4. Private __Persons As New ObservableCollection(Of Person)()
      5. Public Property Persons() As New ListCollectionView(__Persons) ' enables to access/observe the currently selected Person
      6. Public Property cmdFill() As New RelayCommand(AddressOf Load)
      7. Public Property cmdSave() As New RelayCommand(AddressOf Save)
      8. Public Property cmdDoSomething() As New RelayCommand(AddressOf DoSomethingWithAPerson, Function() Persons.CurrentPosition >= 0)
      9. Public Property cmdDelete() As New RelayCommand(Of Person)(Sub(p) Persons.Remove(p), Function() Persons.CurrentPosition >= 0)
      10. Public Property cmdAdd() As New RelayCommand(Sub() Persons.AddNewItem(New Person() With {.Name = "<aName>"}))
      11. Public Sub New()
      12. If IsProvisional Then ' TestData for Designer-Data-Preview
      13. Enumerable.Range(0, 10).ForEach(Sub(i) __Persons.Add(Person.FromInt(i)))
      14. Persons.MoveCurrentToFirst() ' Ensure a Record as selected - for Designer-Data-Preview
      15. Return
      16. End If
      17. Load()
      18. End Sub
      19. Private Sub Save()
      20. Using writer = _DataFile.CreateText
      21. For Each pers In __Persons
      22. With pers
      23. writer.WriteLine(_DataSeparator.Between(.Name, .Phone, .BirthDay.ToString(CultureInfo.InvariantCulture), .Rate))
      24. End With
      25. Next
      26. End Using
      27. System.Media.SystemSounds.Asterisk.Play()
      28. End Sub
      29. Private Sub Load()
      30. __Persons.Clear()
      31. Using reader = _DataFile.OpenText
      32. While Not reader.EndOfStream
      33. Dim data = reader.ReadLine.Split(_DataSeparator)
      34. __Persons.Add(New Person(data(0), data(1), Date.Parse(data(2), CultureInfo.InvariantCulture), Integer.Parse(data(3))))
      35. End While
      36. End Using
      37. Persons.MoveCurrentToFirst()
      38. End Sub
      39. Private Sub DoSomethingWithAPerson()
      40. Dim person = __Persons(Persons.CurrentPosition) ' access the currently selected Person
      41. Msg("Aktuell angewählte Person: ", person.Name)
      42. End Sub
      43. End Class
      Man sieht vor allem Speichern und Laden, und wie das von den Commands ausgelöst wird.
      Ausserdem gibts noch ein DoSomethingWithAPerson(), um zu demonstrieren, dass Xaml bei Bedarf durchaus auch im MainModel Logik auslösen kann, die eine einzelne Person betrifft - nämlich die aktuell ausgewählte Person.

      Beachte auch die cmdDelete-Variante: Beim Delete muss ja ebenfalls das MainModel mit einer bestimmten Person agieren - aber anders als beim parameterlosen cmdDoSomething wird beim Delete die zu deletende Person übergeben.
      Ein weiterer Unterschied ist, dass cmdDoSomething mittels AddressOf DoSomethingWithAPerson den Aufruf weiterleitet, während cmdDelete seinen Aufruf direkt in einer kleinen anonymen Sub abhandelt - eine klassische, explizite Methode hinzuschreiben, und darauf umzuleiten hab ich mir einfach gespart :P .
      cmdDelete und cmdDoSomething zeigen übrigens noch eine weitere Eigenart des Command-Patterns: Wenn man genau guckt, sieht man, dass das RelayCommand sogar zwei Methoden enthält: Die erste Methode wird ausgeführt, wenn der Command aufgerufen wird - es ist eine Sub, also ohne Rückgabewert.
      Die zweite Methode hingegen ist eine Function und ihr Rückgabewert ist ein Boolean, nämlich die Aussage, ob das Command überhaupt ausführbar ist.
      So kann man überaus elegant bewirken, dass der User den Delete-Button garnet betätigen kann, wenns zum Deleten nix gibt - also Deleted werden kann nur, wenn - hingeguckt: Persons.CurrentPosition >= 0

      Hier mal das Xaml des Delete-Buttons - es ist in uclPerson.Xaml verortet:

      XML-Quellcode

      1. <Button Grid.Column="2" Margin="3" ToolTip="Delete"
      2. Command="{Binding Source={StaticResource Mainmodel},Path= cmdDelete}"
      3. CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}">
      4. <Image Height="20" Source="/PersonList;component/Resources/W95MBX01.ICO" />
      5. </Button>
      Also der Button bindet sein Command an Mainmodel.cmdDelete, und bindet zusätzlich den CommandParameter, und zwar an den DataContext von sich selbst! - und der DataContext des Buttons selbst (innerhalb von uclPerson) ist natürlich genau die Person - was sonst?
      Ausserdem hat der Button einen ToolTip, und statt einer Aufschrift enthält er ein Image mit einem Icon - macht sich insgesamt ganz gut - gugge Bildle - es ist das zweite von oben.


      TestDaten-Generierung
      Ich hab ein eigenes kleines Helpers-Framework - siehe die Datei-Struktur im ersten Bildle. Darin befindet sich eine MainModelBase-Klasse, die eine Unterscheidungsmöglichkeit bereitstellt, ob das Mainmodel zur Designzeit instanziert wird oder zur Laufzeit. Das Mainmodel nutzt das folgendermassen:

      VB.NET-Quellcode

      1. Public Sub New()
      2. If IsProvisional Then ' TestData for Designer-Data-Preview
      3. Enumerable.Range(0, 10).ForEach(Sub(i) __Persons.Add(Person.FromInt(i)))
      4. Persons.MoveCurrentToFirst() ' Ensure a Record as selected - for Designer-Data-Preview
      5. Return
      6. End If
      7. Load()
      8. End Sub
      Also falls es provisorisch erstellt wird (IsProvisional), befüllt es sich mit generierten TestDaten, und führt die für einen richtigen Startup vorgesehene Load()-Methode nicht aus.
      Beachte auch Persons.MoveCurrentToFirst(), was die CollectionView dazu bringt, den ersten generierten Datensatz als CurrentItem zu "veröffentlichen" - andernfalls wäre kein Datensatz selected, es gäbe also kein DesignTime-CurrentItem, und ans CurrentItem gebundes Xaml würde folglich in der Vorschau auch keine TestDaten anzeigen.
      Im Xaml wird dann über den d:-Namespace an diese TestDaten gebunden:

      XML-Quellcode

      1. <UserControl x:Class="uclPerson"
      2. ...
      3. xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      4. mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      5. d:DataContext="{Binding Path=Persons/, Source={StaticResource Mainmodel}}" >
      Man sieht: das uclPerson hat seinen DataContext auf MainModel.Persons - gesetzt, wobei das abschliessende / das CurrentItem der CollectionView bestimmt - nicht die ganze Auflistung. Dadurch wird der erste der im MainModel generierten TestDatensätze zum DesignTime-DataContext, und wird korrekt angezeigt, sofern die Bindings stimmen. Siehe nochmal das 2. Bildle von oben.
      Der d:-Namespace gilt nur zur DesignTime, und das ist auch gut so, denn zur Laufzeit wird der DataContext dem uclPerson dann von aussen zugewiesen, im MainWindow:

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. ...
      3. DataContext="{Binding Source={StaticResource Mainmodel}}">
      4. <Grid> <-- 2 Columns, 2 Rows -->
      5. <Button Content="Add" Command="{Binding cmdAdd}"/>
      6. <ListBox Grid.Row="1" ItemsSource="{Binding Persons}" IsSynchronizedWithCurrentItem="True" DisplayMemberPath="Name" Padding="4"/>
      7. <my:uclPerson DataContext="{Binding Persons/}" Grid.Column="1" Grid.RowSpan="2" />
      8. </Grid>
      9. </Window>
      Die "Zuweisung" ist etwas indirekt, nämlich Listbox.IsSynchronizedWithCurrentItem definiert, dass die ListBox das CollectionView.CurrentItem selektiert - und daran ist ja das uclPerson gebunden.
      Das MainWindow selbst hat seinen DataContext natürlich nicht mittels d:-Namespace gebunden, sondern normal. Denn beim MainWindow unterscheidet sich das Laufzeit-DataContext-Binding ja nicht vom DesignTime-DataContext-Binding.


      Wo ist das Model?
      Eigentlich gehören zum Model-View-Viewmodel-Pattern natürlich auch Model-Klassen, aber die gibt es hier gar nicht. View und Viewmodel sind wie gezeigt klar getrennt, aber Model und Viewmodel sind hier (noch) eins. Ist bei mir oft so, dass es sich als nicht sinnvoll erweist, Viewmodel und Model nochmal extra auseinanderzudividieren.
      Zum Mainmodel existiert grundsätzlich niemals eine Model-Klasse, denn es ist ja ein Composicum anderer Viewmodels, zuzüglich Startup- und meist noch weiterer Logik.
      Und das Person-Viewmodel ist bereits jetzt schon so schlank - wenn man da noch eine Person-Model-Klasse auslagern wollte - man würde absolut nichts gewinnen an Transparenz.
      Aber ich kann die Ausdifferenzierung spasseshalber mal fiktiv vorführen - vergleiche:
      Wie es derzeit ist

      VB.NET-Quellcode

      1. Public Class Person : Inherits NotifyPropertyChanged
      2. Public Property cmdUpRate() As New RelayCommand(Sub() Rate += 1, Function() _Rate < 10)
      3. Public Property cmdDownRate() As New RelayCommand(Sub() Rate -= 1, Function() _Rate > 1)
      4. Private _Rate As Integer = 5
      5. Public Property Rate() As Integer
      6. Get
      7. Return _Rate
      8. End Get
      9. Set(ByVal value As Integer)
      10. ChangePropIfDifferent(value, _Rate)
      11. End Set
      12. End Property
      13. Private _Name As String
      14. Public Property Name() As String
      15. Get
      16. Return _Name
      17. End Get
      18. Set(value As String)
      19. ChangePropIfDifferent(value, _Name)
      20. End Set
      21. End Property
      22. Private _Phone As String
      23. Public Property Phone() As String
      24. Get
      25. Return _Phone
      26. End Get
      27. Set(value As String)
      28. ChangePropIfDifferent(value, _Phone)
      29. End Set
      30. End Property
      31. Private _BirthDay As DateTime
      32. Public Property BirthDay() As DateTime
      33. Get
      34. Return _BirthDay
      35. End Get
      36. Set(value As DateTime)
      37. ChangePropIfDifferent(value, _BirthDay)
      38. End Set
      39. End Property
      40. Public Sub New()
      41. End Sub
      42. Public Sub New(name As String, phone As String, birth As DateTime, rate As Integer)
      43. _Name = name
      44. _Phone = phone
      45. _BirthDay = birth
      46. _Rate = rate
      47. End Sub
      48. Public Shared Function FromInt(i As Integer) As Person
      49. 'to create TestData at DesignTime
      50. Return New Person("Person" & i, "54321" & i, DateTime.Today.AddYears(-60 + i), 1 + i Mod 9)
      51. End Function
      52. End Class
      mit
      Wie es mit ausdifferenziertem Model wäre

      VB.NET-Quellcode

      1. Public Class PersonVm : Inherits NotifyPropertyChanged
      2. Public Property cmdUpRate() As New RelayCommand(Sub() Rate += 1, Function() _Person.Rate < 10)
      3. Public Property cmdDownRate() As New RelayCommand(Sub() Rate -= 1, Function() _Person.Rate > 1)
      4. Private _Person As Person
      5. Public Property Rate() As Integer
      6. Get
      7. Return _Person.Rate
      8. End Get
      9. Set(ByVal value As Integer)
      10. ChangePropIfDifferent(value, _Person.Rate)
      11. End Set
      12. End Property
      13. Public Property Name() As String
      14. Get
      15. Return _Person.Name
      16. End Get
      17. Set(value As String)
      18. ChangePropIfDifferent(value, _Person.Name)
      19. End Set
      20. End Property
      21. Public Property Phone() As String
      22. Get
      23. Return _Person.Phone
      24. End Get
      25. Set(value As String)
      26. ChangePropIfDifferent(value, _Person.Phone)
      27. End Set
      28. End Property
      29. Public Property BirthDay() As DateTime
      30. Get
      31. Return _Person.BirthDay
      32. End Get
      33. Set(value As DateTime)
      34. ChangePropIfDifferent(value, _Person.BirthDay)
      35. End Set
      36. End Property
      37. Public Sub New()
      38. _Person = New Person
      39. End Sub
      40. Public Sub New(name As String, phone As String, birth As DateTime, rate As Integer)
      41. Me.New()
      42. _Person.Name = name
      43. _Person.Phone = phone
      44. _Person.BirthDay = birth
      45. _Person.Rate = rate
      46. End Sub
      47. Public Shared Function FromInt(i As Integer) As PersonVm
      48. 'to create TestData at DesignTime
      49. Return New PersonVm("Person" & i, "54321" & i, DateTime.Today.AddYears(-60 + i), 1 + i Mod 9)
      50. End Function
      51. End Class
      52. Public Class Person
      53. Public Property Rate() As Integer
      54. Public Property Name() As String
      55. Public Property Phone() As String
      56. Public Property BirthDay() As DateTime
      57. End Class
      Wie gesagt: in meinen Augen keinerlei Gewinn, nur eine Klasse mehr würde herumfahren. Es bestünde sogar die Gefahr, dass direkt auf der Person-Klasse agiert würde, unter Umgehung des Viewmodels.
      Derlei kann zu schwer zu debuggendem Fehlverhalten führen: oberflächlich liefe alles, nur zB Aktualisierungen würden ausbleiben. In Folge würde dann was anderes präsentiert, als was tatsächlich gegeben ist 8| .
      Wie dem auch sei.
      Dieser Punkt - "Model-Segregation oder nicht" - wird höchst kontrovers diskutiert (weil Pattern-Abweichungen immer höchst kontrovers diskutiert werden - das ist selbst ein Pattern ;) ) - jedenfalls ich stelle hier nochmal heraus, dass dieses Tutorial schon nicht mehr die ganz reine Lehre präsentiert - mag es als Vor- oder Nach-teil gelten.
      Dateien
      • PersonList.zip

        (64,33 kB, 495 mal heruntergeladen, zuletzt: )
      • PersonListCs.zip

        (25,37 kB, 362 mal heruntergeladen, zuletzt: )

      Dieser Beitrag wurde bereits 19 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Im obigen Sample-Code sind in Wahrheit zwei Solutions enthalten, von denen ich nur eine bischen besprochen habe. Die andere Solution heißt "Variations", und zeigt etwas von der Variabilität von Wpf. Zum Beispiel ans selbe Viewmodel kann man sehr unterschiedliche Views binden.
      Daher ist der View-Bereich der Datei-Struktur nun "diversifiziert": Wo in "PersonList" nur ein MainWindow und ein uclPerson das View war, habe ich jetzt drei verschiedene Person-Views, und sogar fünf "Workspaces".
      Mit "Workspace" ist ein UserControl gemeint, was eigentlich auch ein MainWindow sein könnte.
      Da eine Anwendung aber nur ein MainWindow haben kann, habe ich die Alternativen halt nicht als Window ausgeführt, sondern als UserControl - wenn man wollte, könnte man deren Oberfläche mit Copy/Paste in eigene Windows umkopieren, und diese dann in Application.Xaml als StartupUri angeben.
      Also nochmal Bildle Dateistruktur:
      . . . und so die Laufzeit:
      Man sieht, das "Meta-Main-Window" enthält alle "uclWorkspace-Pseudo-MainWindow-UserControls" als TabItems eines TabControls - die Variation "ComboRatedEx" hab ich mal nach vorn geholt wegen der hübschen Rating-Combobox mit den Smileys drinne :) .
      Das ist natürlich Gimmick - im Grunde, und als einfachste Präsentation würde ein "PlainTable"-View reichen, wie er etwa mittels eines Datagrids umsetzbar wäre - Xaml:

      XML-Quellcode

      1. <DataGrid ItemsSource="{Binding Persons}" AutoGenerateColumns="False"
      2. IsSynchronizedWithCurrentItem="True">
      3. <DataGrid.Columns>
      4. <DataGridTextColumn Binding="{Binding Name}" Header="Name"/>
      5. <DataGridTextColumn Binding="{Binding Phone}" Header="Phone"/>
      6. <DataGridTextColumn Binding="{Binding BirthDay, StringFormat=\{0:d\}}" Header="Birth"/>
      7. <DataGridTextColumn Binding="{Binding Rate}" Header="Rate"/>
      8. </DataGrid.Columns>
      9. </DataGrid>
      Dabei erübrigt sich ein gesonderter PersonView - der ist gewissermassen durch die DatagridColumns gebildet - sieht aber auch nicht umwerfend aus:

      (Man kann übrigens auch einen PlainTable-View durchaus ansprechend designen, und v.a. ergonomisch - wenn man nur will.)

      Also ich mach immer zuerst PlainTable wie oben, da sehe ich, obs funktioniert. Dann bastel ich evtl. einen DetailView - hier nochmal der DetailView aus dem PersonList-Projekt:

      XML-Quellcode

      1. <UserControl x:Class="uclPerson"
      2. ...
      3. d:DataContext="{Binding Persons/, Source={StaticResource Mainmodel}}" >
      4. <Grid Margin="4">
      5. <!-- 3 Columns, 4 Row-Definitions... -->
      6. ...
      7. <Button Grid.Column="2" Margin="3" ToolTip="Delete"
      8. Command="{Binding Source={StaticResource Mainmodel},Path= cmdDelete}"
      9. CommandParameter="{Binding DataContext, RelativeSource={RelativeSource Self}}">
      10. <Image Height="20" Source="/Variations;component/Resources/W95MBX01.ICO" />
      11. </Button>
      12. <TextBlock Margin="4,0" VerticalAlignment="Center" Grid.Row="0" Text="Name:"/>
      13. <TextBlock Margin="4,0" VerticalAlignment="Center" Grid.Row="1" Text="Phone:"/>
      14. <TextBlock Margin="4,0" VerticalAlignment="Center" Grid.Row="2" Text="Birthday:"/>
      15. <TextBox Margin="3" VerticalAlignment="Center" Grid.Row="0" Grid.Column="1" Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"/>
      16. <TextBox Margin="3" Grid.Row="1" Grid.Column="1" Text="{Binding Phone}"/>
      17. <DatePicker Margin="3" Grid.Row="2" Grid.Column="1" SelectedDate="{Binding BirthDay}" DisplayDateStart="1900-01-01" DisplayDateEnd="2200-01-01"/>
      18. <GroupBox Grid.Row="3" Grid.ColumnSpan="2" Header="Rating" >
      19. <DockPanel>
      20. <TextBlock VerticalAlignment="Center" Padding="4" Text="{Binding Rate}" Background="White"/>
      21. <Button Margin="3" Width="55" Content="UpRate" Command="{Binding cmdUpRate}" />
      22. <Button Margin="3" Width="65" Content="DownRate" Command="{Binding cmdDownRate}" />
      23. </DockPanel>
      24. </GroupBox>
      25. </Grid>
      26. </UserControl>
      Interessant ist glaub, wie der Delete-Button recht umständlich ans Mainmodel gebunden ist (Zeilen #7-9), während Bindings ans lokale Person-Viewmodel viel einfacher formuliert sind: siehe die Up-/Down-Rate-Buttons Zeilen #21, 22.
      Wie gesagt: Da dem Designer der Datentyp aller DataContexte bekannt ist, kann man im Eigenschaftenfenster fürs StaticResource-Binding die gewünschte StaticResource auswählen, und für lokale Bindings halt direkt aus dem lokalen DataContext.
      An dieser Stelle sollte ich vlt. auf mein Video-Tut verweisen, was die Designer-Arbeit konkret vorturnt: "Binding-Picking" im Xaml-Editor.
      Seit 2013 ticken die Designer zwar schon wieder bischen anders, aber Hauptsache, das Konzept ist mal in Aktion gesehen - dann findet man sich auch mit geänderten Designern bald zurecht.
      Noch zu obigem Xaml: Beachte auch, wie das Rating in eine Groupbox gepackt ist, weil als Textbox kann mans ja nicht anbieten - der User soll da ja nicht direkt reinschreiben dürfen:


      Aber letztlich ist das auch Quatsch mit Buttons zum Raten - günstiger ist eigentlich, die Rating-Optionen als Combobox anzubieten:

      XML-Quellcode

      1. <ComboBox Margin="3" Grid.Row="3" Grid.Column="1"
      2. ItemsSource="{Binding RateOptions, Source={StaticResource VmConstants}}"
      3. SelectedValue="{Binding Rate}"/>
      Hier wundert man sich, was als ItemsSource gebunden ist - das ist weder der lokale DataContext, noch das Mainmodel, sondern ein drittes, neu hinzugekommenes Viewmodel:

      VB.NET-Quellcode

      1. Public Class VmConstants
      2. Public Property RateOptions As Integer() = Enumerable.Range(1, 9).ToArray()
      3. End Class
      Und das hab ich ebenso wie das Mainmodel in die Application.Resources gelegt, und nun bindet meine Combobox ihre ItemSource daran, und deswegen kann sie die RateOptions zur Auswahl stellen:

      Wie man oben im Xaml sieht, ist Combobox.SelectedValue an den lokalen DataContext (Person) gebunden, natürlich an die Rate-Property.
      Also das kommt bei mir öfter vor, dass es auch extrem primitive Viewmodelse gibt - so wie hier - einfach ein konstantes, festverdrahtetes Integer-Array - und daran können dann Comboboxen binden, und stellen die Werte zur Auswahl.
      Anders als normale Viewmodels sind diese HilfsViewmodels nicht Elemente des MainViewmodel-Composits, sondern sind als Extra-Objekt in der Application.Xaml angelegt.

      Um meine Smiley-ComboBox zu unterstützen habe ich im VmConstants-Viewmodel sogar noch eine zweite Property, RateOptionsEx, ein Array von Arrays von Objekten:

      VB.NET-Quellcode

      1. Public Class VmConstants
      2. Public Property RateOptions As Integer() = Enumerable.Range(1, 9).ToArray()
      3. Public Property RateOptionsEx As Object()() = RateOptions.Select(Function(i) New Object(i - 1) {}).ToArray()
      4. End Class
      Im Xaml ist das nun recht trickreich zurechtgemacht, nämlich für die ComboItems gibts ein DataTemplate, und das ist selbst wieder ein ItemsControl, und deren Items haben auch wieder ein Datatemplate, und dieses zeigt letztlich ein Smiley an.
      Und das ItemsPanel des inneren ItemsControls ist ein horizontales Stackpanel, also wenn das innere ItemsControl seine Source an ein Array mit 3 Objekten bindet, dann erscheinen dementsprechend auf dem Stackpanel 3 Smileys nebeneinander.
      Also die Objects selber werden garnicht angezeigt, sondern wenn ein Object da ist, wird dafür ein Smiley gezeigt.
      (naja - schwierig zu erklären, aber gugget Xaml:)

      XML-Quellcode

      1. <ComboBox Margin="3" Grid.Row="3" Grid.Column="1"
      2. ItemsSource="{Binding RateOptionsEx, Source={StaticResource VmConstants}}"
      3. SelectedValuePath="Length" SelectedValue="{Binding Rate}">
      4. <ItemsControl.ItemTemplate>
      5. <DataTemplate>
      6. <ItemsControl ItemsSource="{Binding}">
      7. <ItemsControl.ItemTemplate>
      8. <DataTemplate>
      9. <Image Height="16" Margin="1" Source="/Variations;component/Resources/ICO4161.ico"/>
      10. </DataTemplate>
      11. </ItemsControl.ItemTemplate>
      12. <ItemsControl.ItemsPanel>
      13. <ItemsPanelTemplate>
      14. <StackPanel Orientation="Horizontal"/>
      15. </ItemsPanelTemplate>
      16. </ItemsControl.ItemsPanel>
      17. </ItemsControl>
      18. </DataTemplate>
      19. </ItemsControl.ItemTemplate>
      20. </ComboBox>
      Im Unterschied zum Xaml der vorherigen Combobox muss bei dieser hier auch ein SelectedValuePath angegeben sein.
      Vorher war ja an ein Integer-Array gebunden, das SelectedValue war daher automatisch ein Integer - passend zum Datentyp der Person.Rate-Property.
      Jetzt aber ist an ein Object-Array-Array gebunden, selected ist also ein Object-Array, und der gemeinte Wert ergibt sich als Array.Length, deshalb ist Length als SelectedValuePath anzugeben.


      Zum Abschluss noch ein besonderes Gimmick, nämlich diese MainView besteht nur aus einem ItemsControl, mit einem Detailview als ItemTemplate, und ItemsPanel ist ein WrapPanel.

      XML-Quellcode

      1. <UserControl x:Class="uclWorkspaceWrapPanel"
      2. ...
      3. DataContext="{Binding Source={StaticResource Mainmodel}}">
      4. ...
      5. <ItemsControl ItemsSource="{Binding Persons}">
      6. <ItemsControl.ItemTemplate>
      7. <DataTemplate>
      8. <my:uclComboRatedExtended/>
      9. </DataTemplate>
      10. </ItemsControl.ItemTemplate>
      11. <ItemsControl.ItemsPanel>
      12. <ItemsPanelTemplate>
      13. <WrapPanel/>
      14. </ItemsPanelTemplate>
      15. </ItemsControl.ItemsPanel>
      16. </ItemsControl>
      17. </UserControl>
      Dieser View präsentiert nun alle Personen gleichzeitig im DetailView (mit Umbruch):

      Naja - mag bei Personen nicht sehr sinnvoll sein, aber bei TicTacToe die Präsentation des Spielfeldes verwendet ebenso ein ItemsControl mit speziellem ItemsPanel (ein Uniformgrid).
      Übrigens empfehle ich sehr, das TicTacToe jetzt auch anzugucken.
      Denn die dortige MVVM-Struktur ist exakt dieselbe wie die hiesige - mit allen Vorzügen, inklusive der DesignData-Preview.
      Gleichzeitig ists eine doch recht andere Anwendung - also grad an so verschiedenen Beispielen treten die strukturellen Gemeinsamkeiten besonders deutlich hervor.
      Wo man ebenfalls dasselbe Prinzip wiederfindet - sogar tw. mit denselben Erklärungen - aber wieder eine ganz andere Anwendung - ist: MVVM-Pattern, DataContext und DataTemplates im Treeview
      (Also nach diesen nu insgesamt 3 Samples (und jedes ja auch in Variationen) sollten sowohl das Prinzip von MVVM als auch die Flexiblität des Ansatzes hinreichend aufgezeigt sein)

      Dieser Beitrag wurde bereits 9 mal editiert, zuletzt von „ErfinderDesRades“ ()

      Wpf-Helpers-Bibliotheken

      Eine Wpf-Anwendung kann man mit Framework-Bibliotheken allein nicht sinnvoll entwickeln.
      Aus irgendeinem Grunde fehlt ganz grundlegende Infrastruktur, nämlich
      1. eine ViewmodelBase - Klasse, die man als Basisklasse aller Viewmodel-Klassen hernehmen könnte.
        Denn in jeder Viewmodel-Klasse das INotifyPropertyChanged-Interface neu zu implementieren muss ja wohl als Brain-F...k bezeichnet werden. In meinen Helpers habich die ViewmodelBase halt NotifyPropertyChanged genannt, denn das ist, was sie tut.
        In anderen MVVM-Frameworks heißt sie gewöhnlich tatsächlich ViewmodelBase
      2. eine RelayCommand-Klasse, also eine Klasse, die das ICommand-Interface implementiert, aber in der Form, dass man ihr Delegaten angeben kann, an die die Methoden des Interfaces delegiert werden können.
        In meinen Helpers heißt diese Klasse RelayCommand, in vielen anderen MVVM-Frameworks firmiert sie unter dem Namen DelegateCommand.
      3. ein Mechanismus, der unterscheidet, ob sich das System in der Designzeit oder in der Laufzeit befindet. Denn zur Designzeit ist empfehlenswert, TestDaten zu generieren, während zur Laufzeit natürlich "echte" Daten heranzuschaffen sind.
        Das Framework stellt für Wpf die System.ComponentModel.DesignerProperties.IsInDesignModeProperty - AttachedProperty bereit, und man muss da einen Wrapper für schreiben, um das komfortabel und performant abrufen zu können.
        Naja, meine IsProvisional-Property funzt bischen anders, aber mit demselben Ergebnis. Zumindest vom GalaSoft-MVVMLight-Toolkit weiß ich, dass es auch eine solche Shared Property bereitstellt - ich gehe davon aus, andere MVVM-Tookits ebenfalls.
      Wie gesagt: Es gibt viele MVVM-Toolkits, Galasoft-MVVMLight ist eine, eine andere ist Prism, aber das sind längst nicht alle.
      Mir sind die alle zu umfangreich, und dann fehlt mir doch das eine oder andere Feature, daher habich meine eigenen Wpf-Helpers, und ich denke, ob nun mit oder ohne MVVM-Toolkit von Dritt-Anbietern: eigene Helpers werden sich bei jedem Entwickler einstellen, wie von selbst, denn es entsteht immer mehrfach wiederverwendbarer Code, und Helpers-Projekte sind halt das Mittel, wiederverwendbaren Code für verschiedene Projekte verfügbar zu machen.