die vier Views (in Wpf)

    • WPF

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

      die vier Views (in Wpf)

      Zu beachten: Ist vlt. bisserl ungeschickt, dassichhier mit einer ganz allgemeinen Grundlage datenbänkerischen Denkens einsteige, und mich auch darauf konzentriere. Dabei mussich ganz viele Eigenheiten und Prinzipien von Wpf vorraussetzen, auf die ich im WpfTreeview-Tut eingehe:
      Wpf: MVVM-Pattern, DataContext und DataTemplates im Treeview.
      Dabei ist die Wpf-Treeview ein viel anspruchsvolleres Control als die List- und Text-Boxen, mit denen man die 4 Views zusammenbasteln kann.
      Vlt. mache ich auch mal ein 4-Views Tut für WindowsForms, denn es handelt sich um allgemeine Architektur- und Präsentations-Prinzipien der Informatik - überhaupt nicht auf Wpf oder WinForms beschränkt - auch nicht auf Vb.Net, oder die .Net-Sprachen überhaupt.

      Ein wesentliches Konzept der Datenbänkerei ist, was ich "die vier Views" nenne: vier verschiedene Konzepte, wie sich komplexe Daten präsentieren lassen.
      1. Detail-View: Gegeben ist eine Liste von DatenObjekten, deren jedes mehrere Properties hat. In einem ListenControl präsentiert man diese Datenobjekte nun bewusst unvollständig, häufig zeigt man nur eine einzige (signifikante) Property an - etwa den Namen.
        In der Liste wählt man nun ein Objekt aus, und in beigeordneten EinzelControls werden alle weiteren Properties des aktuell angewählten Objekts präsentiert.
        Ein DetailView ist angesagt, wenn Datenobjekte viele Properties beinhalten, oder wenn einzelne Properties (etwa Bilder, oder langer Text) zuviel Platz beanspruchen würden, um in eine Tabellen-Zeile zu passen.

      2. Parent-Child-View: Gegeben sind DatenObjekte, deren jedes weitere DatenObjekte enthalten. Links werden zB. die übergeordneten Objekte angezeigt, und rechts eine Liste oder Tabelle der untergeordneten Objekte.
        Ein Beispiel, was jeder kennt - der DateiBrowser: links wählt man einen Ordner, und rechts finden sich alle Dateien, die in diesem Ordner drin sind.

      3. Joining-View: In einer großen Tabelle werden alle untergeordneten DatenObjekte angezeigt, aber bestimmte Spalten bestehen aus ComboBoxen, die Werte aus übergeordneten Tabellen anzeigen.
        Beispielsweise können alle Artikel angezeigt sein, und in einer Combobox-Spalte wird jeweils die Kategorie angezeigt, der ein Artikel angehört. Der JoiningView sieht prinzipiell aus wie das Ergebnis einer Inner-Join - Abfrage in Sql, jedoch, da die Tabellen getrennt geladen wurden, ist es möglich, über die Comboboxen einen Artikel einer anderen Kategorie zuzuordnen.

      4. m:n-View: vom Prinzip her eine Kombination aus Parent-Child und Joining-View. Angenommen die Daten von Kunden, Kundenbetreuern und Bestellungen.
        Im Parent-Child-View kann ein Kunde aus einer Liste gewählt werden, und auf der Child-Seite sieht man seine Bestellungen. Aber (Joining-View) eine Combobox-Spalte der Bestellung zeigt den KundenBetreuer an, der diese Bestellung entgegengenommen hat.

      Alle diese Views präsentieren die Daten - damit meine ich: prinzipiell ermöglichen sie volle Bearbeitung (Löschen, Zufügen, editieren von Datensätzen) - was natürlich je nach Erforderlichkeit vom Programm her eingeschränkt wird.
      Die Views sind auch beliebig kombinierbar - man kann auch 3-stufige Parent-Child-Views bauen, oder Detail-Präsentation mit den anderen Views kombinieren - wies eben erforderlich und sinnvoll ist.

      Ich beginne das Tut mit einem super-einfachen Detail-View - nur Lese-Funktionalität: Nämlich eine Liste von FileInfos wird angezeigt (weil ich das einfach generieren kann).
      Vom FileInfo werden 3 Properties angezeigt: Name, Fullname und Length (Attribute, IsReadOnly, LastAccessTime und Zeugs lassich einfach weg)


      Das ViewModel:

      VB.NET-Quellcode

      1. Imports System.Collections.ObjectModel
      2. Imports System.IO
      3. Public Class MainModel
      4. Public Shared ReadOnly Root As New MainModel
      5. Private Sub New()
      6. If GalaSoft.MvvmLight.ViewModelBase.IsInDesignModeStatic Then Return
      7. Dim di = New DirectoryInfo("..\..")
      8. Files = New ObservableCollection(Of FileInfo)(di.GetFiles("*.*", SearchOption.AllDirectories))
      9. End Sub
      10. Public Property Files As ObservableCollection(Of FileInfo)
      11. End Class
      Es gibt nur eine Property, Files - nämlich die Liste mit den FileInfos. Zum MVVM-Pattern, und wie und warum man so ein Model ans Xaml bindet, mögeman sich im grundlegenderen Tut schlau machen: WpfTreeview-Tut

      Das Xaml:

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:io="clr-namespace:System.IO;assembly=mscorlib"
      5. Title="MainWindow" Height="350" Width="525"
      6. xmlns:my="clr-namespace:DetailView"
      7. DataContext="{x:Static my:MainModel.Root}" WindowState="Maximized">
      8. <Grid>
      9. <Grid.ColumnDefinitions>
      10. <ColumnDefinition Width="Auto" MinWidth="20" />
      11. <ColumnDefinition />
      12. </Grid.ColumnDefinitions>
      13. <ListBox Grid.Column="0" MinWidth="60"
      14. ItemsSource="{Binding Path=Files}" DisplayMemberPath="Name" IsSynchronizedWithCurrentItem="True"/>
      15. <Border Grid.Column="1" Height="100" Margin="10" BorderBrush="Aqua" BorderThickness="1" Padding="8" DataContext="{Binding Path=Files}">
      16. <Grid >
      17. <Grid.RowDefinitions>
      18. <RowDefinition />
      19. <RowDefinition />
      20. <RowDefinition />
      21. </Grid.RowDefinitions>
      22. <Grid.ColumnDefinitions>
      23. <ColumnDefinition Width="Auto" />
      24. <ColumnDefinition/>
      25. </Grid.ColumnDefinitions>
      26. <TextBlock Grid.Row="0" Text="Name:" Margin="0,0,5,0" />
      27. <TextBlock Grid.Row="1" Text="FullName:" Margin="0,0,5,0" />
      28. <TextBlock Grid.Row="2" Text="Length:" Margin="0,0,5,0" />
      29. <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Name}"/>
      30. <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=FullName}"/>
      31. <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Path=Length}"/>
      32. </Grid>
      33. </Border>
      34. </Grid>
      35. </Window>
      Zunächstmal ganz wesentlich für jedes Xaml, wie der DataContext definiert ist, denn dieser pflanzt sich durch alle eingeschachtelten Controls fort. Hier ist DataContext="{x:Static my:MainModel.Root}" gebunden, also ans MainModel.
      Im Grid links (Grid.Column="0") ist nun die Listbox plaziert, mit ItemsSource gebunden an MainModel.Files.
      Rechts (Grid.Column="1") eine Border (also ein Rahmen) - mit DataContext gebunden an dieselbe Property!
      In der Border ein Grid mit ungebundenen Textblöcken zur Beschriftung, und gebundenen Textblöcken zur Anzeige der einzelnen Properties eines FileInfos.

      Die Synchronisation, also dass der Inhalt der Border nun genau das FileInfo präsentiert, welches in der Listbox angewählt ist - das wird erzwungen durch: Listbox.IsSynchronizedWithCurrentItem="True".

      Jo, lustig, ne? Mehr zur Erläuterung speziell des DetailViews ist nicht erforderlich - alles andere wurde im WpfTreeview-Tut besprochen.
      Dateien
      • DetailView.zip

        (21,66 kB, 342 mal heruntergeladen, zuletzt: )

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

      Parent-Child-View

      Mein ParentChild-View - Sample zeigt alle Directories in einer Listbox an, und auf der rechten Seite alle Dateien, die im angewählten Directory enthalten sind. Im weitesten Sinne also ein Datei-Browser - ich muß gleich hinzufügen: Ist natürlich idiotisch, alle Directories in einer Liste anzuzeigen - genau für sowas ist eiglich eine Treeview da, denn Directories sind ja ineinander verschachtelt.
      Daher sieht das Ergebnis - gelinde gesagt - "suboptimal" aus:


      Für diesen View muss ich auch eine richtige Viewmodel-Klasse einführen, genannt DirectoryVM. Denn anders als beim DetailView-Sample, wo mir die FileInfo-Klasse mit ihren Properties als Viewmodel gereicht hat, brauche ich fürs DirectoryInfo einen Wrapper, denn DirectoryInfo stellt keine Property Files bereit - ich brauche aber diese Property, um daran zu binden:

      VB.NET-Quellcode

      1. Imports System.IO
      2. Imports System.Collections.ObjectModel
      3. Public Class DirectoryVM
      4. Public Property Files As New ObservableCollection(Of FileInfo)
      5. Public Property Directory As DirectoryInfo
      6. End Class


      Das Mainmodel ist prinzipiell wieder gleich gestrickt - ein Mainmodel halt mit nur einer bindebaren Property: Directories - der Liste der DirectoryVMs:

      VB.NET-Quellcode

      1. Imports System.Collections.ObjectModel
      2. Imports System.IO
      3. Public Class MainModel
      4. Public Shared ReadOnly Root As New MainModel
      5. Private Sub New()
      6. If GalaSoft.MvvmLight.ViewModelBase.IsInDesignModeStatic Then Return
      7. Dim root = New DirectoryInfo("..\..")
      8. Dim loadRecursive As Action(Of DirectoryInfo) = Nothing
      9. loadRecursive = _
      10. Sub(di As DirectoryInfo)
      11. Dim divm = New DirectoryVM
      12. divm.Directory = di
      13. divm.Files = New ObservableCollection(Of FileInfo)(di.GetFiles)
      14. Directories.Add(divm)
      15. For Each childDir In di.GetDirectories
      16. loadRecursive(childDir)
      17. Next
      18. End Sub
      19. loadRecursive(root)
      20. End Sub
      21. Public Property Directories As New ObservableCollection(Of DirectoryVM)
      22. End Class
      Und wie zuvor wird diese Liste im Konstruktor gleichmal befüllt (hoho! - mittels anonymer rekursiver Methode! - gugge auch RecursiveFileSearch)

      Auch das Xaml ist prinzipiell gleich gestrickt wie zuvor:

      XML-Quellcode

      1. <Window x:Class="MainWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:io="clr-namespace:System.IO;assembly=mscorlib"
      5. Title="MainWindow" Height="350" Width="525"
      6. xmlns:my="clr-namespace:ParentChild"
      7. DataContext="{x:Static my:MainModel.Root}" WindowState="Maximized">
      8. <Grid>
      9. <Grid.ColumnDefinitions>
      10. <ColumnDefinition Width="Auto" MinWidth="20" />
      11. <ColumnDefinition Width="10" />
      12. <ColumnDefinition />
      13. </Grid.ColumnDefinitions>
      14. <ListBox Grid.Column="0" MinWidth="60"
      15. ItemsSource="{Binding Path=Directories}"
      16. DisplayMemberPath="Directory.FullName" IsSynchronizedWithCurrentItem="True"/>
      17. <GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" />
      18. <ListBox Grid.Column="2" ItemsSource="{Binding Path=Directories/Files}">
      19. <ListBox.Resources>
      20. </ListBox.Resources>
      21. <ListBox.ItemTemplate>
      22. <DataTemplate>
      23. <Border Margin="2" BorderBrush="Aqua" BorderThickness="1" Padding="2">
      24. <Grid >
      25. <Grid.RowDefinitions>
      26. <RowDefinition/>
      27. <RowDefinition/>
      28. <RowDefinition/>
      29. </Grid.RowDefinitions>
      30. <Grid.ColumnDefinitions>
      31. <ColumnDefinition Width="Auto" />
      32. <ColumnDefinition/>
      33. </Grid.ColumnDefinitions>
      34. <TextBlock Grid.Row="0" Text="Name:" Margin="0,0,5,0" />
      35. <TextBlock Grid.Row="1" Text="FullName:" Margin="0,0,5,0" />
      36. <TextBlock Grid.Row="2" Text="Length:" Margin="0,0,5,0" />
      37. <TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Name}"/>
      38. <TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding Path=FullName}"/>
      39. <TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding Path=Length}"/>
      40. </Grid>
      41. </Border>
      42. </DataTemplate>
      43. </ListBox.ItemTemplate>
      44. </ListBox>
      45. </Grid>
      46. </Window>
      Ein Unterschied ist, dassich dem äußeren Grid eine dritte ColumnDefinition spendiere, damit ich einen GridSplitter zwischen Listbox und ContentPresenter einfügen kann - ich habe also aus dem Grid etwas gebastelt, was man aus WinForms als SplitContainer kennt.
      Die linke Listbox ist nun an Mainmodel.Directories gebunden, und wie zuvor wird Synchronisation erzwungen über Listbox.IsSynchronizedWithCurrentItem="True".
      Rechts die Listbox ist wieder an dieselbe Property gebunden, nur vertieft sie auch gleich, nämlich auf ItemsSource="{Binding Path=Directories/Files}"
      Für die Listbox-Items ist ein aufwändiges DataTemplate geproggt - potzblitz! - das ist ja dieselbe Border, mit allem drum und dran und innendrin, die auch im DetailView für die Präsentation der FileInfos zuständig war - na sowas! ;)
      Dateien
      • ParentChild.zip

        (22,32 kB, 336 mal heruntergeladen, zuletzt: )

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

      Exkurs: relational datenbänkern

      So, beim JoiningView ist Schluss mit lustig, was faule Tricks mittm Datenmodell angeht.
      Also: Kunden, Berater und Bestellungen - wie in Post#1 angedroht.
      Mit Listen von Objekten, die ihrerseits Listen enthalten (Post#2: ObservableCollection(Of DirectoryVM)) kommen wir hier nicht weiter, denn eine Bestellung ist gleichzeitig zwei verschiedenen Objekten zugeordnet, nämlich vom Kunden wurde sie aufgegeben, und ein Bearbeiter hat sie entgegengenommen, und das will modelliert sein.
      Der Trick relationaler Datenbänkerei besteht nun darin, dasses gar keine Objekte mit Listen drinne gibt, sondern es gibt nurnoch Tabellen mit Datensätzen. Jeder Datensatz bekommt eine eindeutige ID, den PrimaryKey.
      Dass ein Datensatz nun eine Liste enthält wird nicht implementiert, sondern es wird simuliert: Nämlich Datensätze, die eigentlich in einer Liste eines übergeordneten Datensatzes drin sein sollten, bekommen stattdessen einen zusätzlichen ForeignKey für diesen übergeordneten Datensatz.
      Also ich habe die Tabelle Kunde, und jeder Kunde hat eine ID. Auch habe ich die Tabelle Bestellung, und jede Bestellung hat eine ID. Und Bestellung hat zusätzlich noch einen ForeignKey: "KundeID".
      Die Zuordnung einer Bestellung zu einem Kunden besteht ganz dumm darin, dass die Bestellung.KundeID identisch ist mit Kunde.ID.
      Dann gibts ein listiges Objekt im KundeDatensatz, das EntitySet(Of Bestellung), das tut so, als sei es eine Liste, aber in Wirklichkeit guckt es nur blitzgeschwind die Bestellung-Tabelle durch und filtert die Bestellungen heraus, deren KundeID mit der ID des Kunden identisch ist.
      Nu habichja noch eine Tabelle: Bearbeiter. Für die hat unsere Bestellung auch einen ForeignKey, nämlich "BearbeiterID".
      Der Witz ist nun, dass auch in den Bearbeiter-Datensätzen so ein EntitySet(Of Bestellung) sitzt, also kann auch der Bearbeiter blitzgeschwind die von ihm bearbeiteten Bestellungen abrufen.
      Und damit ist die Quadratur des Kreises perfekt: Ich brauche nur eine Bestellung aus der Bestellung-Tabelle zu löschen - wupps! - ist sie weg, sowohl aus dem Kunden-EntitySet, als auch aus dem Bearbeiter-EntitySet.
      Ein anderes Must-Have für Daten-Integrität ist die Löschweitergabe: Lösche ich einen Kunden, so fliegen automatisch auch alle seine Bestellungen raus. Denn die Datenbank wäre sofort unbrauchbar, wenn da Bestellungen drin rumfahren würden, die auf Kunden verweisen, dies nicht mehr gibt.
      Auch im (untergeordneten) Bestellung-Datensatz sind listvolle Dinge am Werke: nämlich eine EntityRef(Of Kunde) und eine EntitiRef(Of Bearbeiter). Drumrum gewrappert sind die Properties Bestellung.Kunde und Bestellung.Bearbeiter, sodass eine Bestellung immer weiß, von wem sie aufgegeben wurde, und wer sie bearbeitet hat.
      Das ganze ist zusammengepackt in einem Dings namens DataContext, und für so einen DataContext gibt es einen Designer, wo man die Entities (Kunde, Berater und Bestellung) schön anlegen kann, und auch die Zuordnungen, also dass eine Kunde-Entity viele Bestellungen enthält - eine Berater-Entity enthält aber auch viele Bestellungen:

      Diesen DataContext habe ich erstellt mit [Menü: Projekt-Zufügen-Daten-LinqToSql_Datenklassen]. Dann habe ich mit Rechtsklick in den Designer die Entities zugefügt, mit RechtsKlick in die Entities die Eigenschaften und auch die Zuordnungen.

      Obiges Bild zeigt auch die Eigenschaften, die ich für den PrimKey der Kunde-Entity festgelegt habe:
      • Typ: Integer
      • Zugriff: Friend - Standard ist Public, aber tatsächlich möchte ich mit den IDs überhaupt nichts zu tun haben, und daher verberge ich sie von vornherein vor dem Databinding.
      • Automatisch generiert: True - dieses teilt dem DataContext mit, dass der PrimKey von der Datenbank generiert wird - er muß seine Persistierungs-Strategie danach ausrichten - interessiert uns nicht, wie er das macht - hauptsache er machts richtig ;).
      • Primärschlüssel: True - wichtig für die Zuordnung, insbes. Löschweitergabe
      • Quelle: ID - Quelle bezeichnet den Namen der Datenspalte in der Datenbank. Theoretisch könnteman in der Datenbank eine Tabellenspalte "Nr." anlegen, die dann in die Entity-Property "ID" geladen wird. Also wer sich gerne selbst hinters Licht führt, der kann sowas machen, für die anderen empfehle ich, als Quelle denselben Namen anzugeben, den auch die Entity-Property führt ;).
      Alles annere habich gelassen, wies war.

      Wollt ihr auch die Konfiguration des entsprechenden ForeignKeys sehen?

      Auch die KundeID willich beim Databinding nicht sehen (Zugriff: Friend), und natürlich muß ein ForeignKey denselben Datentyp haben wie der Primkey, auf den er verweist.
      Die Nutz-Properties: Kunde.Name, Bearbeiter.Name, Bestellung.Text zeige ich nicht - das sind in diesem Sample einfach Public Strings ohne weitere Finessen.

      Soooo - Datenbank wurde schon erwähnt - aber wo issedenn? Schauma Code:

      VB.NET-Quellcode

      1. Public Class MainModel : Inherits ViewModelBase
      2. Public Shared ReadOnly Root As New MainModel
      3. Private _DataFile As New FileInfo("Bestellungen.sdf")
      4. Public Property ReLoad As New RelayCommand( _
      5. Sub()
      6. If Context IsNot Nothing Then Context.Dispose()
      7. Context = New BestellungenDataContext("Data Source=" & _DataFile.FullName)
      8. Context.Log = Console.Out
      9. If Not _DataFile.Exists Then Context.CreateDatabase()
      10. RaisePropertyChanged("Context")
      11. End Sub)
      12. Public Property Save As New RelayCommand(Sub() Context.SubmitChanges())
      13. Private Sub New()
      14. If ViewModelBase.IsInDesignModeStatic Then Return
      15. ReLoad.Execute(Nothing)
      16. End Sub
      17. Public Property Context As BestellungenDataContext
      18. End Class
      Sicherlich langweilt euch, dass meine MainModels immer auf die gleiche Weise gestrickt sind: ein Root-MainModel, eine ViewModel-Property (Context As BestellungenDataContext), und ein RelayCommand - naja - hier sinds zwei.
      Aber guggemol Zeile #12: steht da nicht: Context.CreateDatabase()?
      Jo, und das ist wirklich so: Wenns die Datenbank noch nicht gibt, dann erstellen wir sie eben :D. Hammer, oder? Mir ist keine einfachere Methode bekannt, eine Datenbank inklusive OR-Mapper (nämlich der BestellungenDataContext) aufzusetzen, als mit diesen paar Klicksen im DataContext-Designer, und gezeigten drei Zeilen Code (#5, #10, #12).
      Die Datenbank-Datei heißt "Bestellungen.sdf", ist eine SqlCE-Datenbank, und der ConnectionString lautet: "Data Source=C:\bla\blu\...\Bestellungen.sdf" (siehe Zeile#10)
      Und wo Sql? - na, das lassen wir mal schön den machen, der wirklich was davon versteht: nämlich den DataContext.
      Aber wir können ihm dabei zugucken - das ist nett (#13): Context.Log = Console.Out - und der Context ist so freundlich, uns all sein Sql ins Ausgabefenster zu protokollieren.
      Gespeichert wird (#16) über das Save-RelayCommand - ein Einzeiler.
      Wo aber wird geladen? Geladen wird sequentiell: also immer wenn Daten abgerufen werden, die noch nicht im Cache des Contextes drinne sind, schickt der Context geschwind eine entsprechende Abfrage an die DB.
      Und wann werden Daten abgerufen? Na, wenn die Controls Daten anzeigen sollen - erstmalig beim Initialisieren der Bindings. Also in dem Moment, wo einer Listbox ein Binding auf Root.Context.Kunden gesetzt wird - genau dann lädt der Context die Kunde-Tabelle aus der DB in sein Kunde-EntitySet.
      Aber wie macht man einen Reload - also alle Daten verwerfen und neu laden?
      Schauen Sie Methode (#7): der Context wird disposed, und ein ganz neuer Context wird erstellt. Und dann wird ein Ereignis versendet: RaisePropertyChanged("Context") - mit dem jeder, der an den Context gebunden ist, darüber informiert wird, dass dieser Context sich geändert hat. ZB das Binding unserer Kunde-Listbox kriegt das mit, und ruft sofort die neuen Daten ab und pusht sie in die Listbox. (und der Daten-Abruf läßt den Context ja die Abfrage an die DB schicken)
      Dieses Ereignis - PropertyChanged - ist übrigens der Kern der ganzen Databinding-Hexerei. Es ist als Standard spezifiziert im INotifyPropertyChanged - Interface, welches freundlicherweise in der BasisKlasse - ViewModelBase - für uns implementiert wurde - wir erben es einfach.
      Mittels dieses Ereignisses horcht ein Binding darauf, ob und welche Property einer Datenklasse sich ändert, und aktualisiert daraufhin das gebundene Control.

      Uff, uff - Menge Stoff, was? Von der Einführung in relationales Datenbanking bis hin zum Aufsetzen einer kompletten DB mit allen Tabellen und Beziehungen - alles in einem Post ;).
      Diese Technologie hat übrigens mit Wpf garnichts zu tun - dasselbe kann man auch in einer WinForms-Anwendung treiben.
      Zur Wiederholung das im letzten Post erstellte Datenmodell:


      Der JoiningView wird nun die Bestellung-Tabelle präsentieren, und zwar alle 3 Eigenschaften einer Bestellung: Text, Kunde und Bearbeiter. Wie gesagt - in der Datenbank enthält die Bestellung-Tabelle gar keine Bearbeiter, sondern verweist nur auf den Bearbeiter - über die BearbeiterID (mit Kunde dito). Aber ich werde Comboboxen in die Anzeige frickeln, die diesen Verweis auflösen, und sogar darüber hinaus: In ihrem DropDown wird die Combo alle verfügbaren Bearbeiter anzeigen, und wenn man einen anderen Bearbeiter anwählt, so hat man damit wirksam die Bestellung neu zugeordnet (also die Bestellung-Entity vom EntitySet des einen Bearbeiters ins EntitySet des anderen verfrachtet).
      Der Entity-Designer unterstützt uns hierbei, indem er in der Bestellung-Entity eine richtige Bearbeiter-Property generiert, sodaß man mit der eigentlich dahinterstehenden BearbeiterID nichts mehr zu tun hat.
      Vorraussetzung des JoiningViews ist allerdings, dass ich auch für Kunden und Bearbeiter Datagrids bereitstelle, nämlich um überhaupt Bearbeiter- und Kunden-Entities anlegen zu können: denn ohne Kunden bliebe die Kunden-Combobox natürlich leer - überhaupt: ohne Kunden und Bearbeiter kann gar keine Bestellung angelegt werden. Das ist durchs Datenmodell so erzwungen, und spiegelt einfach die Realität wieder: Kunden und Bearbeiter sind zwingende Vorraussetzung der Existenz von Bestellungen.
      Solche Übereinstimmungen erweisen übrigens die volle Bedeutung des Begriffs "Datenmodell": Es ist Abbild ganz realer Verhältnisse.

      Jo, das MainModel wurdeja schon gezeigt, kommen wir also gleich zum Xaml:

      XML-Quellcode

      1. <Window x:Class="JoiningWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:io="clr-namespace:System.IO;assembly=mscorlib"
      5. Title="JoiningWindow" Height="250" Width="Auto"
      6. xmlns:my="clr-namespace:JoiningView"
      7. DataContext="{x:Static my:MainModel.Root}" Top="0" SizeToContent="Width">
      8. <Window.Resources>
      9. <CollectionViewSource x:Key="srcKunden" Source="{Binding Path=Context.Kundes}" />
      10. <CollectionViewSource x:Key="srcBearbeiter" Source="{Binding Path=Context.Bearbeiters}" />
      11. <Style TargetType="{x:Type DataGrid}" >
      12. <Setter Property = "CanUserAddRows" Value="True"/>
      13. <Setter Property = "CanUserDeleteRows" Value="True"/>
      14. <Setter Property = "CanUserReorderColumns" Value="False"/>
      15. <Setter Property = "CanUserResizeRows" Value="False"/>
      16. <Setter Property = "Margin" Value="1 1 1 1"/>
      17. <Setter Property = "AutoGenerateColumns" Value="False"/>
      18. </Style>
      19. </Window.Resources>
      20. ...
      Betrachten wir zunächst die beiden CollectionViewSources in den Window.Resources.
      Oh - Resource selbst sollte ich mal erklären: .Resources ist ein Container, den jedes FrameworkElement hat (alle Controls sind FrameworkElemente). Da hinein kann man beliebige Objekte tun, die man innerhalb des Controls nutzen will. Listigerweise haben auch ins Control eingeschachtelte Controls auf diese Objekte Zugriff.
      Man kann also differenziert den Scope wählen, innerhalb dessen alle eingeschachtelten Controls eine Resource nutzen können.
      Jedenfalls, in unserem Window-Scope sind genannte beiden CollectionViewSources verfügbar. Sie haben einen x:Key - das ist eine Art Name, über den zugegriffen wird. Und sie sind an eine Property des BestellungenContexts gebunden: srcKunden an die Kunden-Tabelle, srcBearbeiter an die Bearbeiter-Tabelle.
      Lustig ist, dass der Designer die Namen Kunde und Bearbeiter gleich pluralisiert hat, denn es sind ja viele Kunden/Bearbeiter in einer Tabelle. Und weil Denglisch die Sprache der Zukunft ist, pluralisiert er englisch (s anhängen) - heraus kommen: Kundes und Bearbeiters, an die zu binden ist. Das tut einem Germanisten natürlich im Schädel weh, aber ich finds lustig und auch sinnvoll, weil so der Name gleich anzeigt, dass es sich um eine Auflistung von Entitäten handelt, und nicht um eine einzelne.
      Das ist nämlich das wesentliche einer CollectionViewSource: Es ist ein Gruppier-, Sortier- und Filter- Instrument für Auflistungen (hier im Rohzustand wern die Auflistungen aber nur durchreicht).
      Weiters findet sich ein Style in den Resources: Dieser hat jetzt keinen x:Key - Namen, sondern nur eine DatenTyp-Angabe (nämlich DataGrid). Daher wird auf diese Resource nicht benamt zugegriffen, sondern sie wird innerhalb des Scopes (nämlich des Windows) auf alle Controls angewendet, die auf diesen Datentyp matchen. Das Prinzip kennt ihr bereits von den DataTemplates im WpfTreeview-Tut
      Man kann auch x:key-benannte Styles und DataTemplates einsetzen, aber mit diesem Style ist eben beabsichtigt, dass er auf alle DataGrids angewendet wird.
      Das hilft, den Code der DataGrids zu vereinfachen, denn die im Style gesetzten Properties brauche ich nicht mehr in jedem DataGrid einzeln zu setzen. Ich denke, es ist ersichtlich, was die Properties bezwecken:
      Der User soll Rows zufügen und löschen können, er soll aber nicht die Anordnung der Spalten verändern, und auch nicht die Höhe der Zeilen verstellen. Ausserdem bekommt jedes Datagrid ein Margin, welches den Abstand zu allen 4 Seiten auf 1 Pixel festlegt. Und die Auto-Generierung von Datenspalten kann ich auch nicht brauchen - weil ich will die Spalten ja differenziert gestalten.
      So weit die Window-Resources, als nächstes kommen die ins Window geschachtelten Controls.

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

      XML-Quellcode

      1. <Window x:Class="JoiningWindow"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. xmlns:io="clr-namespace:System.IO;assembly=mscorlib"
      5. Title="JoiningWindow" Height="250" Width="Auto"
      6. xmlns:my="clr-namespace:JoiningView"
      7. DataContext="{x:Static my:MainModel.Root}" Top="0" SizeToContent="Width">
      8. <Window.Resources>
      9. <CollectionViewSource x:Key="srcKunden" Source="{Binding Path=Context.Kundes}" />
      10. <CollectionViewSource x:Key="srcBearbeiter" Source="{Binding Path=Context.Bearbeiters}" />
      11. <Style TargetType="{x:Type DataGrid}" >
      12. <Setter Property = "CanUserAddRows" Value="True"/>
      13. <Setter Property = "CanUserDeleteRows" Value="True"/>
      14. <Setter Property = "CanUserReorderColumns" Value="False"/>
      15. <Setter Property = "CanUserResizeRows" Value="False"/>
      16. <Setter Property = "Margin" Value="1 1 1 1"/>
      17. <Setter Property = "AutoGenerateColumns" Value="False"/>
      18. </Style>
      19. </Window.Resources>
      20. <Grid >
      21. <Grid.ColumnDefinitions>
      22. <ColumnDefinition MinWidth="30" Width="80" />
      23. <ColumnDefinition MinWidth="30" Width="125" />
      24. <ColumnDefinition MinWidth="30" Width="Auto" />
      25. </Grid.ColumnDefinitions>
      26. <Grid.RowDefinitions>
      27. <RowDefinition Height="Auto" />
      28. <RowDefinition Height="Auto" />
      29. <RowDefinition Height="*" />
      30. </Grid.RowDefinitions>
      31. <Menu Grid.ColumnSpan="3">
      32. <MenuItem Header="ReLoad" Command="{Binding Path=ReLoad}" />
      33. <MenuItem Header="Save" Command="{Binding Path=Save}" />
      34. </Menu>
      35. <Label
      36. Grid.Row="1" Grid.Column="0" Background="#FFBBD6D6" FontSize="12"
      37. Content="Alle Kunden" HorizontalContentAlignment="Center" Margin="1" />
      38. <Label
      39. Grid.Row="1" Grid.Column="1" Background="#FFBBD6D6" FontSize="12"
      40. Content="Alle Bearbeiter" HorizontalContentAlignment="Center" Margin="1" />
      41. <Label
      42. Grid.Row="1" Grid.Column="2" Background="#FFBBD6D6" FontSize="12"
      43. Content="Alle Bestellungen" HorizontalContentAlignment="Center" Margin="1" />
      44. <DataGrid
      45. Grid.Row="2" ItemsSource="{Binding Path=Context.Kundes}" HeadersVisibility="None" >
      46. <DataGrid.Columns>
      47. <DataGridTextColumn Binding="{Binding Path=Name}" Width="*" />
      48. </DataGrid.Columns>
      49. </DataGrid>
      50. <DataGrid Grid.Row="2" Grid.Column="1"
      51. ItemsSource="{Binding Path=Context.Bearbeiters}" HeadersVisibility="None" >
      52. <DataGrid.Columns>
      53. <DataGridTextColumn Binding="{Binding Path=Name}" Width="*" />
      54. </DataGrid.Columns>
      55. </DataGrid>
      56. <DataGrid
      57. Grid.Row="2" Grid.Column="2" RowHeaderWidth="20" SelectionUnit="CellOrRowHeader"
      58. ItemsSource="{Binding Path=Context.Bestellungs}" >
      59. <DataGrid.Columns>
      60. <DataGridTextColumn Binding="{Binding Path=Text}" Header="Text" />
      61. <DataGridComboBoxColumn
      62. ItemsSource="{Binding Source={StaticResource srcKunden}}" DisplayMemberPath="Name"
      63. SelectedItemBinding="{Binding Path=Kunde}" Header="Kunde" />
      64. <DataGridComboBoxColumn
      65. ItemsSource="{Binding Source={StaticResource srcBearbeiter}}" DisplayMemberPath="Name"
      66. SelectedItemBinding="{Binding Path=Bearbeiter}" Header="Bearbeiter" />
      67. </DataGrid.Columns>
      68. </DataGrid>
      69. </Grid>
      70. </Window>
      Also jetzt die Controls: Zunächst mal wieder ein Grid, und zwar mit 3 Spalten und 3 Zeilen.
      Mein Plan ist nämlich, in der ersten Grid-Zeile ein Menü anzulegen, in der zweiten Überschriften für die DataGrids, und in der dritten Zeile dann die DGs selbst. Height="Auto" bestimmt dabei, dass sich die Höhe auf die größte Höhe der enthaltenen Controls einstellt, während bei Height="*" der gesamte verbleibende Platz ausgefüllt wird.
      Also die erste Zeile wird so hoch sein, wie das Menü, die zweite wird so hoch wie die Überschriften-Labels, und die Datagrids werden den restlichen Raum einnehmen.
      Da kommt auch schon das Menü - und zwar in seiner Breite überspannt es alle 3 Grid-Columns: <Menu Grid.ColumnSpan="3">.
      Im Menü die beiden MenuItem, ans Reload- und ans Save-Command gebunden.
      Es folgen die drei Labels, die die Datagrids beschriften - mussichglaub nix zu erläutern, höchstens, dass der angezeigte Text über die Content-Property gesetzt ist - eine Text-Property kennt das Wpf-Label ühaupt nicht.
      Es folgt das erste Datagrid, gebunden ans Kunden-EntitySet - und ohne SpaltenÜberschriften. Das ist ungewöhnlich, aber hier sinnvoll, denn das Grid soll eh nur die Namen der Kunden anzeigen - was sollteda eine SpaltenÜberschrift "Name"?
      Besagte Name-Spalte füllt die gesamte DG-Breite aus: Width="*".
      Das zweite DG ist identisch, nur ans Bearbeiter-EntitySet gebunden.
      Das folgende, an Bestellungs gebundene DG hats aber in sich (nämlich - uff, uff: die DataGridComboBoxColumns).
      Aber zunächstmal SelectionUnit="CellOrRowHeader": es kann also jede Zelle einzeln selektiert werden, und die ganze Row wird über den RowHeader selektiert (etwa zum löschen einer Entity).
      Die DataGridTextColumn ist auch einfach: die bindet an die Text-Property der Bestellung-Entity.
      Ihr seht wieder: Der Datacontext innerhalb eines Datagrids ist nicht der fürs Window festgelegte allgemeine DataContext, sondern DG.ItemsSource definiert als DataContext innerhalb des DGs die einzelne Entity der ItemsSource, also je eine Bestellung (und die hat die Text-Property).

      So, die DataGridComboBoxColumn - die ist kompliziert, und schwierig zu handeln. So eine Combobox braucht ja viel mehr Informationen als eine einfache Textbox.
      Zunächstmal ihre .ItemsSource - also aus welcher Tabelle soll sie die Items holen, zur Anzeige im DropDown? Und eigenartigerweise kann man hier nicht über einen DataContext binden, sondern man muß die in den Resourcen eingerichtete CollectionViewSource bemühen: ItemsSource="{Binding Source={StaticResource srcKunden}}". (Die Syntax eines solchen StaticResource-Bindings ist gewöhnungsbedürftig. Aber der Xaml-Designer hilft einem bei der Auswahl, und so gewöhnt man sich tatsächlich daran).
      Jetzt weiß die Combo also, aus welcher Tabelle ihre DropDownItems kommen, aber - eine Tabelle hat ja mehrere Spalten - welche Spalte soll sie anzeigen? DisplayMemberPath="Name" - sie soll den Namen des Kunden anzeigen.
      Aber für welche Property der Bestellung-Entity soll sie gelten - soll sie etwa in Bestellung.Text schreiben? Nein - der in der Combo selektierte Kunde soll in die Kunde-Property der Bestellung eingetragen werden: SelectedItemBinding="{Binding Path=Kunde}"
      Das Ergebnis im Bild:


      Die rechte Tabelle ist der JoiningView - und zwar sind sogar 3 Tabellen zusammen-gejoint. In Sql ausgedrückt etwa folgendermaßen:

      SQL-Abfrage

      1. SELECT Bestellung.Text, Kunde.Name, Bearbeiter.Name FROM ((Bestellung
      2. INNER JOIN Kunde On Kunde.ID = Bestellung.KundeID)
      3. INNER JOIN Bearbeiter On Bearbeiter.ID = Bestellung.BearbeiterID)
      Daraus könnte man einen exakt gleich aussehenden JoiningView basteln - viel einfacher sogar - nämlich ausschließlich mit TextboxColumns.

      Das folgende könnte dieser "Sql-JoiningView" aber nicht:

      Nämlich Zufügen einer neuen Bestellung, unter Angabe des neuen Texts, und Auswahl von Kunde und Bearbeiter.

      Nachtrag: zwei Komplikationen beim Umgang mit DataGridComboBoxColumn
      1. Für andere Szenarien verfügt die DataGridComboBoxColumn noch über weitere Properties, die sehr ähnlich erscheinen: .SelectedValueMemberPath, .SelectedValueBinding und .TextBinding. Wenn man nun in seinem Verständnis nicht sicher ist und rumprobiert, ergibt sich daraus eine erhebliche Menge an Kombinations-Möglichkeiten, die nicht funktionieren.
      2. Aufgrund eines Bugs erkennt der Xaml-Designer bei ComboboxColumns nicht den lokalen, durch Datagrid.ItemsSource definierten DataContext. Das Setzen von ComboColumn.SelectedItemBinding muß daher händisch eingetragen werden. Das ist zum einen unsicher, zum anderen überaus verwirrend: Denn wenn man sich das sichere Setzen von Bindings über den Designer zueigen macht, denkt man nun natürlich: "So gehts nicht - der Designer zeigt keine brauchbare Property an" - und es geht aber doch so.
      Dateien
      • JoiningView.zip

        (30,43 kB, 325 mal heruntergeladen, zuletzt: )

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

      (Code-Sample im Joining-View enthalten, s. Vorpost)

      Zum m:n - View gibts im Grunde nichts neues mehr zu erläutern. Wie in Post#1 gesagt: Es ist eine Kombination aus ParentChild- und Joining-View. ParentChild wurde in Post#2 behandelt, und JoiningView in Post#4.
      Hier das Ergebnis - ich habe sogar beide Richtungen - m->n und n->m - implementiert:

      Interessant die Bestellung "txt2C" - ich habe in beiden Views so selektiert, dass sie jeweils sichtbar ist: Klar ersichtlich ist, dass "txt2C" die einzige Bestellung von Kunde2 ist - BearbeiterC jedoch hat noch 2 weitere Bestellungen entgegengenommen - von anderen Kunden.

      Am Datenmodell hat sich überhaupt nichts geändert. Ich habe sogar den m:n-View einfach ins JoiningView-Projekt mit hineingepackt, und nur im Einsprungspunkt der Anwendung - "Application.Xaml" den StartupUri geändert:

      XML-Quellcode

      1. <Application x:Class="Application"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. StartupUri="Views\M_N_View.xaml"
      5. >
      6. <!--
      7. StartupUri="Views\JoiningWindow.xaml"
      8. -->
      9. <Application.Resources>
      10. </Application.Resources>
      11. </Application>
      Deshalb zum M_N_View auch kein Sample-Solution-Upload - den StartUpUri ändern werdter wohl noch hinkriegen, odr? ;)

      Nur der Vollständigkeit halber hier das Xaml:
      M_N_Views.xaml

      XML-Quellcode

      1. <Window x:Class="M_N_View"
      2. xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      3. xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      4. Title="M_N_View" Height="350" Width="300"
      5. xmlns:my="clr-namespace:JoiningView"
      6. DataContext="{x:Static my:MainModel.Root}" Top="0" >
      7. <Window.Resources>
      8. <CollectionViewSource x:Key="srcKunden" Source="{Binding Path=Context.Kundes}" />
      9. <CollectionViewSource x:Key="srcBearbeiter" Source="{Binding Path=Context.Bearbeiters}" />
      10. <Style TargetType="{x:Type DataGrid}" >
      11. <Setter Property = "CanUserAddRows" Value="True"/>
      12. <Setter Property = "CanUserDeleteRows" Value="True"/>
      13. <Setter Property = "CanUserReorderColumns" Value="False"/>
      14. <Setter Property = "CanUserResizeRows" Value="False"/>
      15. <Setter Property = "Margin" Value="1 1 1 1"/>
      16. <Setter Property = "AutoGenerateColumns" Value="False"/>
      17. </Style>
      18. </Window.Resources>
      19. <Grid>
      20. <Grid.ColumnDefinitions>
      21. <ColumnDefinition MinWidth="30" Width="90" />
      22. <ColumnDefinition MinWidth="30" Width="*" />
      23. </Grid.ColumnDefinitions>
      24. <Grid.RowDefinitions>
      25. <RowDefinition Height="Auto" />
      26. <RowDefinition Height="Auto" />
      27. <RowDefinition Height="*" />
      28. <RowDefinition Height="Auto" />
      29. <RowDefinition Height="*" />
      30. </Grid.RowDefinitions>
      31. <Menu Grid.ColumnSpan="3">
      32. <MenuItem Header="ReLoad" Command="{Binding Path=ReLoad}" />
      33. <MenuItem Header="Save" Command="{Binding Path=Save}" />
      34. </Menu>
      35. <Label
      36. Grid.Row="1" Background="#FFBBD6D6" FontSize="12" Grid.ColumnSpan="2"
      37. Content="Bestellungen nach Kunde" HorizontalContentAlignment="Center" Margin="1" />
      38. <Label
      39. Grid.Row="3" Background="#FFBBD6D6" FontSize="12" Grid.ColumnSpan="2"
      40. Content="Bestellungen nach Bearbeiter" HorizontalContentAlignment="Center" Margin="1" />
      41. <DataGrid ItemsSource="{Binding Path=Context.Kundes}" IsSynchronizedWithCurrentItem="True"
      42. Grid.Row="2" HeadersVisibility="Column">
      43. <DataGrid.Columns>
      44. <DataGridTextColumn Binding="{Binding Path=Name}" Width="*" Header="Kunde" />
      45. </DataGrid.Columns>
      46. </DataGrid>
      47. <DataGrid DataContext="{Binding Path=Context.Kundes}" ItemsSource="{Binding Path=Bestellungs}"
      48. RowHeaderWidth="20" SelectionUnit="CellOrRowHeader" Grid.Row="2" Grid.Column="1">
      49. <DataGrid.Columns>
      50. <DataGridTextColumn Binding="{Binding Path=Text}" Header="Text" />
      51. <DataGridComboBoxColumn
      52. ItemsSource="{Binding Source={StaticResource srcBearbeiter}}" DisplayMemberPath="Name"
      53. SelectedItemBinding="{Binding Path=Bearbeiter}" Header="Bearbeiter" Width="*" />
      54. </DataGrid.Columns>
      55. </DataGrid>
      56. <DataGrid ItemsSource="{Binding Path=Context.Bearbeiters}" IsSynchronizedWithCurrentItem="True"
      57. Grid.Row="4" HeadersVisibility="Column">
      58. <DataGrid.Columns>
      59. <DataGridTextColumn Binding="{Binding Path=Name}" Width="*" Header="Bearbeiter" />
      60. </DataGrid.Columns>
      61. </DataGrid>
      62. <DataGrid DataContext="{Binding Path=Context.Bearbeiters}" ItemsSource="{Binding Path=Bestellungs}"
      63. RowHeaderWidth="20" SelectionUnit="CellOrRowHeader" Grid.Row="4" Grid.Column="1">
      64. <DataGrid.Columns>
      65. <DataGridTextColumn Binding="{Binding Path=Text}" Header="Text" />
      66. <DataGridComboBoxColumn
      67. ItemsSource="{Binding Source={StaticResource srcKunden}}" DisplayMemberPath="Name"
      68. SelectedItemBinding="{Binding Path=Kunde}" Header="Kunde" Width="*" />
      69. </DataGrid.Columns>
      70. </DataGrid>
      71. </Grid>
      72. </Window>

      Der m:n-View hat vlt. eine höhere praktische Relevanz als der reine JoiningView: Praktisch vorstellbar ist (unterer m:n-View) dass ein Bearbeiter sich fest voreingestellt hat, und dann wählt er den Kunden an, der grade anruft, und nimmt dessen Bestellung auf.

      Die "feste Voreinstellung" könnte auch über ein LogIn erfolgen, sodaß dem Bearbeiter nur die rechte Seite des Views präsentiert wird.
      Diese (rechte) Tabelle ließe sich auch als (parametrisiertes) Sql formulieren:

      SQL-Abfrage

      1. SELECT Bestellung.Text, Kunde.Name
      2. FROM (Bestellung INNER JOIN Kunde ON Bestellung.KundeID=Kunde.ID)
      3. WHERE Bestellung.BearbeiterID=@BearbeiterIdInput

      Dieser "Sql-m:n-View" wäre dann natürlich ebenso auf Readonly eingeschränkt, wie der Sql-JoiningView aus Post#5

      SQL - INNER JOIN ist Readonly
      Betrachten wir folgenden Bestellung-Datensatz, der den zuständigen Bearbeiter mit aufführt:

      SQL-Abfrage

      1. SELECT Bestellung.Text, Bestellung.ID, Bearbeiter.Name, Bearbeiter.ID
      2. FROM (Bestellung INNER JOIN Bearbeiter ON Bestellung.BearbeiterID=Bearbeiter.ID)
      3. WHERE Bestellung.ID=@BestellungIdInput

      Den Text können wir ändern:

      SQL-Abfrage

      1. UPDATE [Bestellung] SET [Text] = 'Neuer Text' WHERE [ID]=@BestellungIdInput

      Aber wie ändern wir den Bearbeiter?

      SQL-Abfrage

      1. UPDATE [Bestellung] SET [Name] = 'annererBearbeiter' WHERE [ID]=@BestellungIdInput
      ??
      Ist ja Quatsch, denn die Tabelle Bestellung enthält keine Name-Spalte - das ist ja eine Spalte der Bearbeiter-Tabelle.

      SQL-Abfrage

      1. UPDATE [Bearbeiter] SET [Name] = 'annererBearbeiter' WHERE [ID]=@BearbeiterId
      ??
      Dieser Quatsch ist zumindest theoretisch möglich, denn die BearbeiterId haben wir ja im Datensatz enthalten (s.o.).
      Aber damit hätten wir nicht die Bestellung geändert, sondern den Bearbeiter umgetauft.

      Die einzig sinnvolle Möglichkeit, den Bearbeiter einer Bestellung zu ändern ist: einen anderen nehmen. Und dazu reicht die Information eines gejointen Datensatzes nicht aus, denn um einen gültigen anderen Bearbeiter zu nehmen, müssteman erstmal alle Bearbeiter abrufen, dass man einen auswählen kann.
      Und genau dieses: "erstmal alle Bearbeiter abrufen" - ist die Vorgehensweise eines JoiningViews: Bestellungen und Bearbeiter werden getrennt geladen, und erst im View zusammengeführt. Und "dass man einen auswählen kann" wird ganz intuitiv präsentiert: Combobox - dafür ist sie da.
      (In einem DetailView kann man die Auswahl übrigens auch als Listbox, ListView oder auch Datagrid präsentieren, falls die Umstände es geeigneter erscheinen lassen.)

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

      Binding-Mismatches

      Zu Xaml muß man allgemein sagen: es ist leider noch mit Kinderkrankheiten behaftet, im Vergleich zum Entwicklungsstand der älteren .Net-Sprachen C# und VB.
      ZB bei Bindings kann man im Editor als Pfad-Angabe hinschreiben, was man will - es gibt keine Intellisense oder Compiler-Überprüfung - ja es gibt nichtmal einen Codestop, wenn ein ungültiges Binding nicht ausgeführt wird.
      Sondern die Anwendung hat einfach eine Fehlfunktion, und die Performance ist im Keller, denn die Ausführung der internen Error-Catch-Mechanismen ist wesentlich kostspieliger als die Auflösung eines korrekten Binding.
      Also im Xaml-Code Bindings im Editor hinzuschreiben ist gewissermaßen Programmieren mit unsachgemäßem TryCatch und Option Strict Off gleichzeitig.
      Ich empfehle, konsequent zu versuchen, alle Bindings ausschließlich über das Property-Fenster zu setzen. Man bekommt einen Dialog, wo man aus den gültigen Pfaden auswählen kann, und darüberhinaus mächtig gewaltige Optionen, das Bindungs-Verhalten zu modifizieren.
      Stellt man beim Setzen eines Binding fest, dass keine brauchbaren Pfade angeboten werden, so ist dies ein starkes Indiz für einen Fehler im Aufbau.
      Leider nur ein Indiz, denn es gibt auch Fälle, wo man um solche "Schmuddel-Programmierung" nicht herumkommt.
      In meinen Samples sind alle Bindings Designer-überprüfbar - nur die DataContext-Bindung des Gesamt-Windows ans RootModel ist jeweils ohne Designer-Unterstützung reingeschrieben (es gibt auch saubere Alternativen, aber zT. aufwändiger, und ich bin mir selbst noch nicht im klaren über meine Standard-Vorgehensweise in diesem Punkt)
      Auch in Styles kann man Bindings nur ohne Designer-Unterstützung setzen, wovon ich daher abraten würde, wenns irgend geht. Wie gesagt - Kinderkrankheit - in einer typisierten Sprache sollten Deklarations-Möglichkeiten bestehen, welche Binding-Mismatches gar nicht erst zulassen.