Windows Forms-Datenbindung und Objekte
Veröffentlicht: 12. Mai 2003 | Aktualisiert: 22. Jun 2004
Von Rockford Lhotka
Rocky Lhotka zeigt Ihnen, wie Sie Geschäfts- und Auflistungsklassen um das Datenbindungsfeature in Windows Forms erweitern.
Auf dieser Seite
Einfache Windows Forms-Datenbindung
Benachrichtigung über geänderte Eigenschaften
Datenbindung an eine stark typisierte Auflistung
Implementieren von "IBindingList"
Änderungsbenachrichtigung
Bearbeiten und Entfernen von Elementen
Hinzufügen von Elementen
Implementieren von "IEditableObject" in untergeordneten Klassen
Kopieren von Daten
Sicherstellen neuer Objekte
BeginEdit
EndEdit
CancelEdit
Verarbeiten des "RemoveMe"-Ereignisses
Implementieren von "IDataErrorInfo" in untergeordneten Klassen
Schlussfolgerung
Microsoft hat sich bemüht, die Datenbindung zu einem hilfreichen Instrument zu machen, wenn es darum geht, Windows Forms und Windows-Forms-Oberflächen zu programmieren. Zum ersten Mal, seit das Datenbindungsfeature vor einigen Jahren in Microsoft Visual Basic® eingeführt wurde, ist es in vielen Anwendungsszenarien wirklich praktisch handhabbar.
Eine wesentliche Weiterentwicklung besteht darin, dass die Datenbindung nicht nur das DataSet, sondern auch Objekte, Strukturen und Objekt- oder Strukturauflistungen unterstützt. Die elementare Fähigkeit, ein Objekt oder eine Auflistung an ein Steuerelement eines Windows Form oder Web Form zu binden, erfordert quasi kein Zutun Ihrerseits. Es ist eine automatische Funktion.
Das Datenbindungsfeature in Web Forms ist schreibgeschützt. Das heißt, dieses Feature entnimmt der Datenquelle (DataSet, Objekt oder Auflistung) Daten und füllt diese in die Steuerelemente, die dann an den Client zurückgegeben werden. Dies ist ein umkompliziertes Verfahren, für das Sie keinen Code schreiben müssen, wenn Sie Objekte oder Benutzeroberflächen erstellen.
Das Datenbindungsfeature in Windows Forms ist weder lese- noch schreibgeschützt und daher ein weitaus komplexerer Vorgang. Hierbei werden Daten der Datenquelle entnommen und in den Steuerelementen der Benutzeroberfläche angezeigt, wobei Änderungen der Werte in den Steuerelementen auch automatisch in der Datenquelle aktualisiert werden. Ein Großteil dieser Verhaltensweise läuft automatisch ohne Ihr Zutun ab. Es steht Ihnen aber eine Reihe von Funktionen zur Verfügung, für die Sie jedoch zusätzlichen Code schreiben müssen.
Dieser Artikel beschäftigt sich also mit Code, den wir den Geschäfts- und Auflistungsklassen hinzufügen können, um die Datenbindungsfunktionen in Windows Forms besser zu unterstützen. Zu diesen Funktionen gehören:
Benachrichtigen der Benutzeroberfläche durch das Objekt oder die Auslistung über Änderungen bei den Daten.
Zulässiges Binden des DataGrid-Steuerelements an eine leere Auflistung.
Direktes Bearbeiten von untergeordneten Objekten in einem DataGrid.
Dynamisches Hinzufügen oder Entfernen von untergeordneten Objekten eines DataGrid.
Bei einfachen Objekten können wir Ereignisse implementieren, die die Windows Forms-Datenbindung informieren, wenn Eigenschaftenwerte geändert wurden. Durch Hinzufügen dieser Ereignisse wird die Anzeige auf der Benutzeroberfläche automatisch aktualisiert, sobald das Objekt eine Änderung erfährt. Gleichfalls müssen wir klären, wie die Benutzeroberfläche informiert werden kann, dass eine Validierungsregel durch neu eingegebene Daten verletzt wurde. Eine falsche Implementierung der Validierung kann zu unerwünschten Resultaten bei der Datenbindung führen.
Außerdem steht eine Reihe von optionalen Funktionen zur Verfügung, die wir in den Auflistungen unterstützen können. Auflistungen sind normalerweise an Listensteuerelemente wie das DataGrid gebunden. Durch das richtige Implementieren von stark typisierten Auflistungsobjekten veranlassen wir das DataGrid zu einer intelligenten Interaktion mit der Auflistung und den untergeordneten Objekten. Wir können ebenfalls die IBindingList-Schnittstelle implementieren, damit die Auflistung mit dem DataGrid sinnvoll und vielfältig interagieren kann.
Schließlich existieren noch einige optionale Funktionen, die wir in Objekten unterstützen können, die in einer Auflistung enthalten sind. Diese werden untergeordnete Objekte genannt. Untergeordnete Objekte können die IEditableObject-Schnittstelle implementieren, damit das DataGrid während der direkten Bearbeitung korrekt mit dem Objekt interagieren kann. Des Weiteren können die untergeordneten Objekte die IDataErrorInfo-Schnittstelle implementieren, so dass das DataGrid die Zeile als fehlerhaft markiert, wenn eine Validierungsregel bei der Änderung von Objektdaten verletzt wurde.
Durch Schreiben von zusätzlichem Code für die Klassen und Auflistungen können wir also einige sehr leistungsfähige Funktionen des Windows Forms-Datenbindungsfeatures nutzen.
Einfache Windows Forms-Datenbindung
Das Binden von Objekteigenschaften an Eigenschaften eines Formularsteuerelements ist nicht sehr aufwändig. Sehen Sie sich beispielsweise folgende Order-Klasse an:
Public Class Order Private mID As String = "" Private mCustomer As String = "" Public Property ID() As String Get Return mID End Get Set(ByVal Value As String) mID = Value End Set End Property Public Property Customer() As String Get Return mCustomer End Get Set(ByVal Value As String) mCustomer = Value End Set End Property End Class
Es gibt hier nur speziellen Code bei der Deklaration der Variablen:
Private mID As String = "" Private mCustomer As String = ""
Beachten Sie, dass die Variablen bei ihrer Deklaration mit leeren Werten initialisiert werden. Dies ist kein typischer Code, da Visual Basic .NET Variablen automatisch bei deren Deklaration initialisiert.
Wir initialisieren die Variablen hier explizit, weil wir sicherstellen möchten, dass die Datenbindung funktioniert. Die automatische Initialisierung der Variablen erfolgt nicht, wenn die Datenbindung versucht, mit dem Objekt zu interagieren, was zur Ausgabe einer Laufzeitausnahme führt, sobald die Datenbindung versucht, den Wert aus den uninitialisierten Variablen abzurufen.
Eine explizite Initialisierung der Variablen geschieht jedoch, bevor die Datenbindung mit dem Objekt interagiert. Dadurch werden die Variablen einwandfrei initialisiert, wenn die Datenbindung Werte abruft, und wir erhalten keine Laufzeitausnahme.
Wenn wir ein ähnliches Formular wie das in Abbildung 1 gezeigte erstellen, können wir die Objekteigenschaften beim Laden des Formulars problemlos an die Steuerelemente binden.
Abbildung 1. Einfaches Formular für das "Order"-Objekt
Der Code, mit dem wir ein Order-Objekt an das Formular binden, könnte folgendermaßen aussehen:
Private mOrder As Order Private Sub OrderEntry_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load mOrder = New Order() txtID.DataBindings.Add("Text", mOrder, "ID") txtCustomer.DataBindings.Add("Text", mOrder, "Customer") End Sub
Das Geheimnis besteht darin, dass jedes Windows Forms-Steuerelement über eine DataBindings-Auflistung verfügt. Diese Auflistung enthält eine Liste der Bindungen zwischen den Eigenschaften des Steuerelements und den Eigenschaften der Datenquelle(n). Ein interessanter Nebeneffekt dieses Systems besteht darin, dass wir Eigenschaften einer Datenquelle an mehrere unterschiedliche Steuerelementeigenschaften binden können. Wir können im Gegenzug auch unterschiedliche Steuerelementeigenschaften an Eigenschaften von mehreren Datenquellen binden.
Allein mit Hilfe dieses einfachen Datenbindungscodes können wir komplexe Benutzeroberflächen erstellen. Am Codebeispiel des vorliegenden Artikels sehen Sie, dass wir die Enabled-Eigenschaft der Schaltfläche Save an die IsValid-Eigenschaft des Geschäftsobjekts binden können. Auf diese Weise ist die Schaltfläche für den Benutzer nur verfügbar, wenn das Objekt endgültig gespeichert werden soll.
Bedenken Sie, dass diese Art der Datenbindung bidirektional ist. Es werden nicht nur die Objektdaten im Formular angezeigt, sondern auch Änderungen der Daten durch den Benutzer werden automatisch im Objekt aktualisiert. Dies geschieht, wenn der Benutzer mit der TAB-Taste zum nächsten Feld wechselt. Wenn der Benutzer beispielsweise den Wert im txtID-Steuerelement ändert, wird der neue Wert im Objekt aktualisiert, sobald der Benutzer das Steuerelement verlässt. Die Daten werden im Objekt über die Set-Routine der Eigenschaft aktualisiert. Dies ist bequem, denn unser vorhandener property-Code wird automatisch aufgerufen, und wir benötigen keinen weiteren Eingriff, um die bidirektionale Datenbindung zu unterstützen.
Benachrichtigung über geänderte Eigenschaften
Nachdem wir gelernt haben, wie unkompliziert wir Objektdaten an Steuerelemente binden können, erfahren wir jetzt, wie wir das Objekt erweitern, damit eine automatische Benachrichtigung über geänderte Eigenschaften unterstützt wird. Wenn ein anderer Code in unserer Anwendung die Daten eines Objekts ändert, stoßen wir hier auf das Problem, dass die Steuerelemente der Benutzeroberfläche nicht über diese Änderung informiert werden. Benutzeroberfläche und Objekt stimmen dann nicht mehr überein, und der Benutzer sieht auf dem Bildschirm nicht die richtigen Daten.
Wir benötigen daher die Möglichkeit einer Benachrichtigung der Benutzeroberfläche durch das Objekt, sobald ein Eigenschaftswert geändert wurde. Ereignisse, die wir optional deklarieren und aus dem Objekt aufrufen, unterstützen diese Benachrichtigung. Wenn wir ein Steuerelement an eine Eigenschaft des Objekts binden, sucht die Datenbindung automatisch nach dem Ereignis einer geänderten Eigenschaft, das den Namen propertyChanged trägt, wobei property der Name der Eigenschaft des Objekts ist.
Die Order-Klasse zum Beispiel definiert eine ID-Eigenschaft. Wird die ID-Eigenschaft an ein Steuerelement gebunden, sucht die Datenbindung nach einem IDChanged-Ereignis. Wenn dieses Ereignis durch das Objekt ausgelöst wird, aktualisiert die Datenbindung automatisch alle Steuerelemente, die an das Objekt gebunden sind.
Wir erweitern die Order-Klasse, indem wir folgende Ereignisse deklarieren:
Public Class Order Public Event IDChanged As EventHandler Public Event CustomerChanged As EventHandler
Beachten Sie, dass diese Ereignisse als EventHandler-Ereignisse deklariert werden müssen. Dies ist erforderlich, damit die Datenbindung das Ereignis erkennen kann. Wenn wir die Ereignisse nicht auf diese Weise deklarieren, erhalten wir eine Laufzeitausnahme, sobald die Datenbindung versucht, mit dem Objekt zu interagieren.
EventHandler ist das standardmäßige Ereignismodell in Windows Forms. Es definiert das Ereignis mit Hilfe von zwei Parametern: sender (das Objekt, das das Ereignis auslöst) und e (ein EventArgs-Objekt).
Wenn diese Ereignisse deklariert sind, müssen wir sicherstellen, dass sie ausgelöst werden, sobald der entsprechende Eigenschaftenwert geändert wird. Vorzugsweise geschieht dies in der Set-Routine. In der ID-Eigenschaft würden wir Folgendes definieren:
Public Property ID() As String Get Return mID End Get Set(ByVal Value As String) mID = Value RaiseEvent IDChanged(Me, New EventArgs()) End Set End Property
Kniffliger ist es hingegen, zu bedenken, dass wir auch bei jeder Änderung von mID in der Klasse dieses Ereignis auslösen müssen. Viele Klassen enthalten Code, der nicht nur die Set-Routinen der Eigenschaft, sondern auch interne Variablen ändert. Wir müssen immer das entsprechende Ereignis auslösen, wenn ein Wert geändert wurde, der Auswirkungen auf eine Eigenschaft hat.
Für ein besseres Verständnis nehmen wir einmal an, dass das Order-Objekt über eine Auflistung von LineItem-Objekten verfügt. Diese Auflistung implementieren wir später. Konzentrieren wir uns zunächst auf die Ereignis- und Variablendeklarationen der grundlegenden LineItem-Klasse:
Public Class LineItem Public Event ProductChanged As EventHandler Public Event QuantityChanged As EventHandler Public Event PriceChanged As EventHandler Public Event AmountChanged As EventHandler Private mProduct As String Private mQuantity As Integer Private mPrice As Double
Wir haben vier Ereignisse, eines pro Eigenschaft, aber nur drei Variablen. Die Amount-Eigenschaft ergibt sich aus der Multiplikation von Quantity und Price:
Public ReadOnly Property Amount() As Double Get Return mQuantity * mPrice End Get End Property
Es handelt sich hierbei um eine schreibgeschützte Eigenschaft. Sie kann sich jedoch ändern. Sie ändert sich in der Tat, wenn sich der Wert von Price oder Quantity ändert, und folglich können wir ein Ereignis auslösen, das die Änderung signalisiert. Wenn sich Price ändert:
Public Property Price() As Double Get Return mPrice End Get Set(ByVal Value As Double) mPrice = Value RaiseEvent PriceChanged(Me, New EventArgs()) RaiseEvent AmountChanged(Me, New EventArgs()) End Set End Property
Wir lösen nicht nur das PriceChanged-Ereignis aus, da die Price-Eigenschaft geändert wurde, sondern wir lösen auch das AmountChanged-Ereignis aus, weil damit auch indirekt die Amount-Eigenschaft modifiziert wurde. Dieses Beispiel zeigt, dass wir beim Schreiben von Code wachsam sein müssen, damit diese Ereignisse auch wirklich im richtigen Augenblick ausgelöst werden.
Daraus ergibt sich, dass das AmountChanged-Ereignis nicht unbedingt erforderlich sein muss. Wenn wir Steuerelemente eines Formulars an Eigenschaften eines Objekts binden, sucht die Datenbindung nach propertyChanged-Ereignissen für jede Eigenschaft, die an die Steuerelemente gebunden ist. Wird eines der Ereignisse ausgelöst, werden alle an das Objekt gebundenen Steuerelemente aktualisiert.
Wenn also das Formular Steuerelemente enthält, die an die Price- und Amount-Eigenschaften gebunden sind, bewirkt das Auslösen des PriceChanged-Ereignisses, dass die Datenbindung nicht nur das an die Price-Eigenschaft gebundene Steuerelement, sondern auch das an die Amount-Eigenschaft gebundene Steuerelement aktualisiert.
Der Nachteil hierbei ist, dass die Benutzeroberfläche sehr eng an die Objektimplementierung gebunden ist. Wenn wir später ein Steuerelement nur an die Amount-Eigenschaft binden möchten, wird die Benutzeroberfläche nicht ordnungsgemäß funktionieren, da kein AmountChanged-Ereignis ausgelöst wird. Daher sollte vorzugsweise ein propertyChanged-Ereignis für jede Objekteigenschaft deklariert und ausgelöst werden.
Der restliche Code für die LineItem-Klasse befindet sich im Codebeispieldownload zu diesem Artikel.
Datenbindung an eine stark typisierte Auflistung
Das Order-Objekt enthält, wie bereits erläutert, eine Auflistung von LineItem-Objekten. Bei unserer Benutzeroberfläche können wir ein DataGrid an diese Auflistung binden, damit der Benutzer LineItem-Objekte problemlos hinzufügen, entfernen oder bearbeiten kann. Das Ergebnis wird in Abbildung 2 dargestellt.
Abbildung 2. An die Objektauflistung gebundenes DataGrid in einem Formular
Mit einer einzigen Codezeile können wir ein DataGrid-Steuerelement an ein Array oder eine Auflistung binden:
Dim arLineItems As New ArrayList() dgLineItems.DataSource = arLineItems
Obwohl dabei die Daten aus dem Array in der Tabelle angezeigt werden, liefert diese nicht alle Funktionen, die wir von der Datenbindung an das DataGrid-Steuerelement erwartet haben, da die grundlegenden Array- und Auflistungsklassen nicht genügend Daten bereitstellen, damit das DataGrid-Steuerelement auf gleiche Weise funktioniert, als wäre es an eine DataTable gebunden.
Wir können ein eigenes, stark typisiertes Auflistungsobjekt erstellen, das zusätzlichen Code enthält und somit besser die Funktionen des DataGrid unterstützen kann. Wir werden insbesondere folgende Funktionen unterstützen können:
Datenbindung an eine leere Auflistung
DataGrid-Benachrichtigung über Auflistungsänderungen
Dynamisches Hinzufügen und Entfernen von untergeordneten Objekten
Direktes Bearbeiten von untergeordneten Objekten
Gehen wir diese Punkte nacheinander an. Die erste Funktion - Bindung eines DataGrid an eine leere Auflistung - ist relativ einfach zu realisieren. Das DataGrid muss hierbei jedoch in der Lage sein, die Spalten zu erkennen, die in der Datenquelle verfügbar sind. Wie kann das DataGrid dies herausfinden, wenn wir es an ein einfaches Array-, ArrayList- oder Auflistungsobjekt binden?
Das DataGrid schaut sich das erste Element in der Auflistung an und ruft dann die Liste der öffentlichen Eigenschaften und Felder dieses Elements ab (gleichgültig, ob es sich um eine Struktur, ein Objekt oder einen einfachen Typ wie eine Ganzzahl handelt).
Wenn die Auflistung leer ist, funktioniert diese Methode nicht, und das DataGrid kann nicht automatisch eine Spaltenliste erstellen. Der Benutzer sieht daher das Steuerelement ohne Inhalt, wie in Abbildung 3 veranschaulicht.
Abbildung 3. An eine leere Auflistung gebundenes DataGrid
Diesen Umstand können wir beheben, indem wir eine benutzerdefinierte, stark typisierte Auflistung mit einer standardmäßigen, stark typisierten Item-Eigenschaft erstellen. Mit Hilfe dieser Item-Eigenschaft kann das DataGrid eine Liste der spezifischen öffentlichen Eigenschaften und Felder dieses Typs abrufen. Es muss daher nicht mehr nach dem ersten Element der Auflistung suchen, um die Spaltenliste zu erstellen. Wir können es also an eine leere Auflistung binden, und es arbeitet trotzdem fehlerfrei.
Abbildung 4. An eine leere, stark typisierte Auflistung gebundenes DataGrid
Aus dem Vergleich von Abbildung 3 mit Abbildung 4 geht eindeutig hervor, dass die stark typisierte Auflistung bevorzugt werden sollte, da das DataGrid eine Spaltenliste anzeigt, selbst wenn keine Elemente vorhanden sind.
Das Implementieren einer stark typisierten Auflistung ist unkompliziert. Die folgenden Schritte beschreiben den grundlegenden Vorgang:
Hinzufügen einer neuen Klasse zum Projekt
Vererben von System.Collections.CollectionBase
Implementieren einer standardmäßigen, stark typisierten Item-Eigenschaft
Implementieren von stark typisierten Add- und Remove-Methoden
Sie finden beispielsweise im Codedownload eine LineItems-Klasse, die eine stark typisierte Auflistung von LineItem-Objekten implementiert. Damit das DataGrid an eine leere Auflistung gebunden werden kann, benötigen wir die Item-Eigenschaft.
Default Public ReadOnly Property Item(ByVal index As Integer) _ As LineItem Get Return CType(list(index), LineItem) End Get End Property
Beachten Sie, dass die Eigenschaft dadurch stark typisiert ist, dass sie Objekte des Typs LineItem zurückgibt. Des Weiteren müssen Sie berücksichtigen, dass sie als Default markiert und eine Eigenschaft und keine Funktion ist. Datenbindung benötigt diese spezielle Deklaration für ihr Funktionieren.
In der Klasse aus dem Codedownload sehen Sie auch, dass es stark typisierte Implementierungen der Add- und Remove-Methoden gibt, die Teil jeder beliebigen stark typisierten Auflistung bilden, aber für das Funktionieren der Datenbindung nicht vonnöten sind.
Mit sehr wenig zusätzlichem Code haben wir das erste Problem bei der Bindung des DataGrid an eine leere Auflistung gelöst. Packen wir jetzt die Lösung der nächsten Probleme an!
Implementieren von "IBindingList"
Das Datenbindungsfeature verfügt über ein formelles Schema, mit dem eine Auflistung anzeigen kann, dass sie geändert wurde. Die Auflistung muss dafür die IBindingList-Schnittstelle implementieren, die ein ListChanged-Ereignis enthält. Immer, wenn die Auflistung geändert wird (durch Hinzufügen, Entfernen oder Ändern eines Elements), wird dieses Ereignis ausgelöst und signalisiert, dass die zugrunde liegenden Daten geändert wurden.
IBindingList unterstützt mehr als nur die Benachrichtigung bezüglich einer Änderung. In der folgenden Tabelle werden die optionalen Funktionen aufgelistet, die durch Implementierung der Schnittstelle unterstützt werden können:
Optionale Funktion |
Beschreibung |
Änderungsbenachrichtigung |
Informiert die Benutzeroberfläche über Änderungen (Hinzufügungen, Entfernungen oder Bearbeitungen) an der Auflistung. |
Automatisches Hinzufügen von Elementen |
Mit dieser Funktion kann das DataGrid neue Elemente in die Auflistung einfügen, wenn sich der Benutzer zum Ende der Tabelle bewegt. |
Automatisches Entfernen von Elementen |
Mit dieser Funktion kann das DataGrid Elemente aus der Auflistung entfernen, wenn der Benutzer in der Tabelle die ENTF-Taste drückt. |
Direktes Bearbeiten von Elementen |
Mit dieser Funktion kann das DataGrid Elemente direkt in der Auflistung bearbeiten (dies erfordert, dass das Element die IEditableObject-Schnittstelle implementiert, wie weiter unten beschrieben). |
Suchen |
Aktiviert eine Find-Methode für die Suche nach einen bestimmten Element in der Auflistung. |
Sortieren |
Aktiviert eine Sort-Methode, die die Auflistung nach bestimmten Spalten sortiert. |
Es steht uns frei, diese Funktionen zu implementieren. Die IBindingList-Schnittstelle definiert lediglich die Eigenschaften, Methoden und Ereignisse. Unsere Aufgabe ist es, den eigentlichen Code zu schreiben. Glücklicherweise sind alle diese Funktionen rein optional. Die IBindingList-Schnittstelle definiert eine Reihe von booleschen Eigenschaften, die wir zum Festlegen der Funktionen benötigen, die von unserer speziellen Auflistung unterstützt werden sollen.
SupportsChangeNotification
AllowNew
AllowEdit
AllowRemove
SupportsSearching
SupportsSorting
Wir geben TRUE nur für solche Funktionen zurück, die wir implementieren möchten. Im Codedownload finden wir die LineItems-Auflistung, die die Änderungsbenachrichtigung sowie das Hinzufügen, Bearbeiten und Entfernen implementiert. Sie unterstützt nicht die Funktionen Suchen oder Sortieren, für die wir FALSE zurückgeben.
Die grundlegende Implementierung der Schnittstelle verlangt die Verwendung des Implements-Schlüsselworts:
Public Class LineItems Inherits CollectionBase Implements IBindingList
Wenn wir diese Anweisung hinzufügen, geben wir damit an, dass wir die Schnittstelle implementieren, und wir müssen Implementierungen für alle durch die Schnittstelle definierten Methoden bereitstellen. Dazu zählen Methoden, die wir vielleicht nicht implementieren müssen, wie SortDirection (weil wir die Sortierfunktion nicht implementieren). Wir müssen gar keinen Code in die Methode, sondern nur Code für die Methodenshell schreiben:
Public ReadOnly Property SortDirection() _ As System.ComponentModel.ListSortDirection _ Implements System.ComponentModel.IBindingList.SortDirection Get End Get End Property
Wir schreiben richtigen Code nur für Methoden, die wir auch implementieren.
Änderungsbenachrichtigung
Wenn wir zum Beispiel die Änderungsbenachrichtigungsfunktion unterstützen, wird die Datenbindung immer benachrichtigt, sobald die Auflistung geändert wird. Jedes Mal, wenn ein Element also hinzugefügt, entfernt oder geändert wird, müssen wir Code schreiben.
Zunächst müssen wir angeben, dass wir die Änderungsbenachrichtung unterstützen.
Public ReadOnly Property SupportsChangeNotification() _ As Boolean Implements _ System.ComponentModel.IBindingList.SupportsChangeNotification Get Return True End Get End Property
Des Weiteren müssen wir das ListChanged-Ereignis als von der Schnittstelle definiert deklarieren:
Public Event ListChanged(ByVal sender As Object, _ ByVal e As System.ComponentModel.ListChangedEventArgs) _ Implements System.ComponentModel.IBindingList.ListChanged
Danach müssen wir nur noch dieses Ereignis auslösen, sobald die Auflistung geändert wird. Dies ist gar nicht so schwer, wie es sich vielleicht anhört, da die CollectionBase-Klasse überschreibbare Methoden definiert und wir somit genau wissen, wann die Auflistung geändert wurde. Wir benötigen folgende Methoden:
OnClearComplete
OnInsertComplete
OnRemoveComplete
OnSetComplete
Wir können das ListChanged-Ereignis innerhalb der einzelnen Methoden auslösen. Wenn zum Beispiel ein Element eingefügt wird, lösen wir das folgende Ereignis aus:
Protected Overrides Sub OnInsertComplete(ByVal index As Integer, _ ByVal value As Object) RaiseEvent ListChanged( _ Me, New ListChangedEventArgs(ListChangedType.ItemAdded, index)) End Sub
Das Auflistungsobjekt informiert dann die Benutzeroberfläche über Änderungen, und datengebundene Steuerelemente (wie das DataGrid) können jederzeit den Inhalt der Auflistung präzise wiedergeben.
Bearbeiten und Entfernen von Elementen
Die IBindingList-Schnittstelle definiert die AllowEdit- und AllowRemove-Eigenschaften. Damit können wir steuern, ob das DataGrid ein direktes Bearbeiten und ein dynamisches Entfernen von Auflistungselementen zulassen soll. Es gibt keine weiteren IBindingList-Methoden, die diese Funktionen unterstützen. Sie sind einfach nur eine Art von Schalter zur Steuerung der zulässigen Funktionen.
Wenn wir jedoch TRUE für AllowEdit zurückgeben, müssen die untergeordneten Objekte selbst das direkte Bearbeiten unterstützen, indem die IEditableObject-Schnittstelle implementiert wird. Diesen Punkt werden wir weiter unten diskutieren. Es werden keine Ausnahmen ausgelöst, wenn die untergeordneten Objekte die Schnittstelle nicht implementieren. Wenn wir aber ein ordnungsgemäßes Verhalten erzielen möchten, muss die Schnittstelle implementiert werden.
Hinzufügen von Elementen
Wir können auch zulassen, dass das DataGrid dynamisch neue Elemente in die Auflistung aufnimmt. Mit dieser praktischen Funktion kann der Benutzer an das Ende der Tabelle navigieren, und es wird dort eine neue Zeile angefügt, in die er neue Daten eingeben kann. Diese Funktion hängt davon ab, ob wir das direkte Bearbeiten unterstützen oder nicht. Wenn wir das Hinzufügen von Elementen unterstützen, müssen wir auch das Bearbeiten von Elementen zulassen. AllowEdit muss deshalb TRUE zurückgeben, und die untergeordneten Objekte müssen die IEditableObject-Schnittstelle implementieren.
IBindingList definiert die AllowNew-Eigenschaft, die TRUE zurückgeben muss, wenn wir diese Funktion unterstützen. Diese Schnittstelle definiert auch die zu implementierende AddNew-Methode. Diese Methode muss Code enthalten, der ein neues untergeordnetes Objekt erstellt, es der Auflistung hinzufügt und es als Ergebnis der Methode zurückgibt.
Wir können beispielsweise ein neues LineItem-Objekt wie folgt erstellen:
Public Function AddNew() As Object Implements _ System.ComponentModel.IBindingList.AddNew Dim item As New LineItem() list.Add(item) Return item End Function
Die Schwierigkeit dabei besteht darin, dass wir untergeordnete Objekte ohne Zutun des Benutzers erstellen können müssen. Das DataGrid erfordert das Hinzufügen eines untergeordneten Objekts. Wir müssen also jederzeit ein neues untergeordnetes Objekt programmtechnisch auf Anforderung erzeugen können, wie es im Code dargestellt ist.
Einige Objektentwürfe erfordern beim Erstellen von untergeordneten Objekten die Bereitstellung von Konstruktordaten, die zusammen mit den Informationen vorab geladen werden. Dieses Modell funktioniert in unserem Fall aber nicht, da wir keine spezifischen Informationen vom Benutzer über das untergeordnete Objekt vor Erstellung desselben erhalten.
Sobald wir die AllowNew- und AddNew-Eigenschaft implementiert haben, kann sich der Benutzer mit Hilfe des DataGrid an das Ende der Tabelle begeben, und neue untergeordnete Objekte, die der Benutzer bearbeiten kann, werden der Auflistung (und somit der Tabelle) automatisch hinzugefügt.
Implementieren von "IEditableObject" in untergeordneten Klassen
Wie bereits gesagt, verlangt das direkte Bearbeiten nicht nur die Implementierung der IBindingList-Schnittstelle in der benutzerdefinierten Auflistung, sondern auch die Implementierung von IEditableObject in den untergeordneten Klassen.
Die IEditableObject-Schnittstelle erscheint unkompliziert. Sie definiert drei Methoden:
Methode |
Definition |
BeginEdit |
Wird von der Datenbindung aufgerufen, um den Beginn eines Bearbeitungsvorgangs zu signalisieren und um das Objekt anzuweisen, einen Snapshot seines aktuellen Status aufzuzeichnen. |
CancelEdit |
Wird von der Datenbindung aufgerufen, um das Ende des Bearbeitungsvorgangs anzuzeigen und um das Objekt anzuweisen, seinen Status auf die ursprünglichen Werte zurückzusetzen. |
EndEdit |
Wird von der Datenbindung aufgerufen, um das Ende des Bearbeitungsvorgangs anzuzeigen und um das Objekt anzuweisen, alle geänderten Werte beizubehalten. |
Kopieren von Daten
Das Implementieren dieser Schnittstelle erfordert die Möglichkeit, einen Snapshot des aktuellen Status des Objekts aufzeichnen zu können. Dies bedeutet das Kopieren der Werte aller Instanzenvariablen im Objekt. Für das Anfertigen der Kopie gibt es verschiedene Möglichkeiten. Aber leider sprengt diese Diskussion den Rahmen dieses Artikels. In diesem Artikel kopieren wir einfach die Variablenwerte in andere Variablen.
mOldProduct = Product mOldPrice = Price mOldQuantity = Quantity
Zum Wiederherstellen kopieren wir einfach die Werte in umgekehrter Richtung.
Sicherstellen neuer Objekte
Für eine ordnungsgemäße Implementierung von IEditableObject müssen wir wissen, ob dieses untergeordnete Objekt der Auflistung neu hinzugefügt wurde oder nicht. Diese Information ist zwingend erforderlich, denn wir müssen ein neues untergeordnetes Objekt aus der Auflistung entfernen, wenn der Benutzer die ESC-Taste drückt, um die Änderung zu verwerfen. Wenn der Benutzer andererseits ein bereits vorhandenes untergeordnetes Objekt bearbeitet und ESC drückt, wollen wir das Objekt nicht aus der Auflistung entfernen. Wir möchten seinen Status lediglich auf die vorherigen Werte zurücksetzen.
Wir benötigen eine Variable, die anzeigt, ob das Objekt neu ist oder nicht, um dieses Problem lösen zu können. Wir deklarieren eine Variable namens mIsNew und setzen den Wert auf TRUE, wenn sie das erste Mal deklariert wird.
Private mIsNew As Boolean = True
Nach der ersten Bearbeitung wird der Wert auf FALSE gesetzt. Dies erfolgt beim Aufruf von CancelEdit oder EndEdit.
BeginEdit
Das Schwierige an dieser Methode ist, dass BeginEdit mehrmals während des Bearbeitungsvorgangs aufgerufen werden kann. Die Onlinehilfe für das .NET Framework-SDK stellt klar, dass wir nur den ersten Aufruf berücksichtigen und alle weiteren vernachlässigen sollen. Dafür deklarieren und verwenden wir die boolesche Variable mEditing wie folgt:
Public Sub BeginEdit() Implements _ System.ComponentModel.IEditableObject.BeginEdit If Not mEditing Then mEditing = True mOldProduct = Product mOldPrice = Price mOldQuantity = Quantity End If End Sub
Diese Variable wird in den Methoden CancelEdit und EndEdit auf FALSE gesetzt, so dass die weiteren Bearbeitungsgänge reibungslos funktionieren.
EndEdit
Die EndEdit-Methode ist ganz einfach zu implementieren, da diese Methode nach Beendigung der Bearbeitung aufgerufen wird und wir die Änderungen beibehalten möchten. Wir müssen mEditing lediglich auf FALSE setzen, um anzuzeigen, dass die Bearbeitung abgeschlossen ist:
Public Sub EndEdit() Implements _ System.ComponentModel.IEditableObject.EndEdit mEditing = False mIsNew = False End Sub
Beachten Sie, dass wir auch in dieser Methode mIsNew auf FALSE setzen. An dieser Stelle wissen wir, dass der Benutzer das untergeordnete Objekt akzeptiert hat und es nicht länger neu ist (zumindest aus Sicht der Datenbindung und des DataGrid).
CancelEdit
Die CancelEdit-Methode ist vielleicht die komplizierteste der drei Methoden. Sie muss nicht nur die Werte des Objekts auf die Werte zurücksetzen, die wir beim Aufruf von BeginEdit gespeichert haben, sondern wir müssen auch feststellen, ob das untergeordnete Objekt neu ist. Wenn es neu ist, müssen wir sicherstellen, dass es aus der Auflistung entfernt wird.
Wir deklarieren ein Ereignis, damit das richtige Auflistungsobjekt entfernt wird:
Friend Event RemoveMe(ByVal LineItem As LineItem)
Das Ereignis verwendet einen Parameter des Typs LineItem, so dass wir einen Verweis an das untergeordnete Objekt selbst übergeben können. Auf diese Weise stellen wir sicher, dass der Auflistungscode das richtige Element erkennt, das entfernt werden soll.
Der Code für CancelEdit sieht dann folgendermaßen aus:
Public Sub CancelEdit() Implements _ System.ComponentModel.IEditableObject.CancelEdit mEditing = False Product = mOldProduct Price = mOldPrice Quantity = mOldQuantity If mIsNew Then mIsNew = False RaiseEvent RemoveMe(Me) End If End Sub
Zunächst erklären wir den Bearbeitungsvorgang für beendet, indem wir mEditing auf FALSE setzen. Dann setzen wir den Status des Objekts auf die Werte zurück, die wir in BeginEdit gespeichert haben. Alle Eigenschaftenwerte werden auf den vorherigen Status zurückgesetzt. Abschließend prüfen wir die mIsNew-Variable, um festzustellen, ob sie ein neu hinzugefügtes untergeordnetes Objekt ist. Falls ja, lösen wir das RemoveMe-Ereignis aus, um der Auflistung mitzuteilen, dass dieses Objekt entfernt werden soll.
Verarbeiten des "RemoveMe"-Ereignisses
Da die untergeordneten Objekte jetzt Ereignisse in der Auflistung auslösen können, müssen wir den Code in der benutzerdefinierten Auflistungsklasse so erweitern, dass er dieses Ereignis verarbeiten kann. Dazu fügen wir der LineItems-Auflistungsklasse erst eine Verarbeitungsmethode für das Ereignis hinzu. Der Code wird ausgeführt, wenn das Ereignis ausgelöst wird:
Private Sub RemoveChild(ByVal Child As LineItem) list.Remove(Child) End Sub
Er entfernt das angegebene untergeordnete Objekt aus der Auflistung. Beachten Sie, dass es in dieser Methode keine Handles-Klausel gibt. Wie ruft sie das Ereignis ab? Die Antwort darauf ist die AddHandler-Funktion. Mit dieser Funktion können wir ein Ereignis zur Laufzeit dynamisch mit einem Ereignishandler verknüpfen. Dies ist besonders beim Erstellen von Auflistungsklassen hilfreich, da wir mit dieser Funktion Ereignisse für untergeordnete Objekte einbinden können, wenn die einzelnen untergeordneten Objekte der Auflistung hinzugefügt werden.
In der Auflistungsklasse haben wir bereits eine OnInsertComplete-Methode, in der wir das ListChanged-Ereignis auslösen. Wir können dieser Methode den AddHandler-Aufruf hinzufügen, so dass sichergestellt ist, dass dieses Ereignis bei jedem neu hinzugefügten untergeordneten Objekt aufgerufen wird.
Protected Overrides Sub OnInsertComplete(ByVal index As Integer, _ ByVal value As Object) AddHandler CType(value, LineItem).RemoveMe, AddressOf RemoveChild RaiseEvent ListChanged( _ Me, New ListChangedEventArgs(ListChangedType.ItemAdded, index)) End Sub
Sobald der Auflistung ein neues untergeordnetes Objekt hinzugefügt wurde, erstellen wir eine Verknüpfung zwischen dem RemoveMe-Ereignis für das Objekt und dem RemoveChild-Ereignishandler. Wenn der Benutzer die ESC-Taste für ein neu hinzugefügtes Zeilenobjekt drückt, wird dieses untergeordnete Objekt aufgrund dieses Mechanismus automatisch aus der Auflistung entfernt.
Implementieren von "IDataErrorInfo" in untergeordneten Klassen
Wir können der untergeordneten Klasse noch eine letzte optionale Funktion hinzufügen, und zwar die Möglichkeit, dem DataGrid zu sagen, wenn ein Zeilenelement gültig oder ungültig ist. Dies geschieht durch Implementieren der IDataErrorInfo-Schnittstelle in der untergeordneten LineItem-Klasse.
Die IDataErrorInfo-Schnittstelle definiert zwei Methoden:
Methode |
Beschreibung |
Error |
Gibt eine Beschreibung des Fehlers in diesem Objekt zurück (eine leere Zeichenfolge signalisiert Fehlerfreiheit). |
Item |
Gibt eine Beschreibung des Fehlers in einer bestimmten Eigenschaft oder einem bestimmten Feld des Objekts zurück (eine leere Zeichenfolge signalisiert Fehlerfreiheit). |
Die ausschlaggebende Methode ist die Error-Methode. Mit der Item-Methode können wir auch detailliertere Informationen bereitstellen, aber das DataGrid überschreibt den Text, der von der Error-Methode zurückgegeben wird, um festzustellen, ob die Zeile gültig oder ungültig ist. Im Codedownload sehen Sie, dass nur Code in der Error-Methode implementiert wurde:
Private ReadOnly Property [Error]() As String Implements _ System.ComponentModel.IDataErrorInfo.Error Get If Len(Product) = 0 Then Return "Product name required" If Quantity <= 0 Then Return "Quantity must be greater than zero" Return "" End Get End Property
Diese Methode prüft eine Reihe von Geschäftsregeln für das Zeilenelement. Wir geben vor, dass ein Zeilenelement einen Produktnamen und eine Menge größer Null (> 0) enthalten muss. Wird eine dieser Bedingungen nicht erfüllt, geben wir Text zurück, der die Art des Problems angibt. Ist das Objekt jedoch gültig, geben wir eine leere Zeichenfolge zurück.
Als Ergebnis zeigt das DataGrid grafisch an, welche Zeilen gültig sind. Siehe auch Abbildung 5 unten.
Abbildung 5. Das DataGrid zeigt die Gültigkeit von Zeilenelementen an
Beachten Sie, dass die Error-Eigenschaft als "private" gekennzeichnet ist. Wenn wir sie auf "public" setzen, wird der Fehlertext als Spalte im DataGrid angezeigt. Der Benutzer kann daran den Fehler im Zeilenelement erkennen.
Schlussfolgerung
Das Datenbindungsfeature ist endlich "erwachsen" geworden. Die Implementierung des Datenbindungsfeatures in Web Forms und in Windows Forms ist in vielen Fällen praktisch und nützlich. Als größten Nutzen können wir jetzt Daten an Objekte und Auflistungen und nicht mehr nur an das DataSet und die damit verbundenen ADO.NET-Objekte binden.
In diesem Artikel haben wir gelernt, dass wir mit relativ wenig zusätzlichem Code in Geschäfts- und Auflistungsklassen die Datenbindung in Windows Forms dazu bringen können, mit den Objekten zu interagieren. Wir müssen es nicht länger bei bloßer Datentechnologie in der RAD-Entwicklung bewenden lassen. Jetzt können wir gleichzeitig objekt- und RAD-orientiert arbeiten.