Άνοιξες μεγάλο θέμα...
Λοιπόν, ας υποθέσουμε ότι ξεκινάς με αυτήν την κλάση:
Public Class Customer1
Private mName As String
Private mOriginalName As String
Private mDirty As Boolean
Public Sub New()
mName = ""
mOriginalName = ""
mDirty = False
End Sub
Public Property Name() As String
Get
Return mName
End Get
Set(ByVal Value As String)
mName = Value
End Set
End Property
Public Property OriginalName() As String
Get
Return mOriginalName
End Get
Set(ByVal Value As String)
mOriginalName = Value
End Set
End Property
Public ReadOnly Property Dirty() As Boolean
Get
Return mDirty
End Get
End Property
End Class
Υποθέτω ότι θα έχεις βάλει και λίγο κώδικα για να αλλάζεις το OriginalName ανάλογα, ωστόσο προς το παρόν αυτό δεν μας απασχολεί.
Όταν τρέχεις το
Dim b As Binding = TextBox1.DataBindings.Add("Text", c, "Name")
συνδέεις το text property του TextBox1 με το Name property του c object (instance του Customer). Οι αλλαγές που κάνεις στο textbox περνάνε "μαγικά" στο αντίστοιχο property, ωστόσο αυτό συμβαίνει πριν το validate event, δηλαδή στην περίπτωση του textbox, μετά το lost focus του. Άρα το θέμα είναι πως θα προκαλέσουμε το update του datasource νωρίτερα. Και φυσικά, τη λύση έρχονται να δώσουν (τα-νταααααν!!!) τα interfaces. Πριν όμως φτάσουμε εκεί, θα πρέπει να λύσουμε ένα άλλο μικρό πρόβλημα.
Το data binding μέχρι στιγμής είναι μονόδρομο, δηλαδή το μαγικό δεν συμβαίνει όταν θα πειράξουμε μέσω κώδικα την τιμή του property. To textbox θα παραμείνει απαθές και θα συνεχίσει να δείχνει την παλιά τιμή. Για να διορθώσουμε αυτή τη συμπεριφορά, θα πρέπει να ακολουθήσουμε ένα design pattern που αναγνωρίζει το binding object. Αυτό το design pattern λέει ότι θα πρέπει το datasource να κάνει expose ένα public event τύπου EventHandler που θα πρέπει επίσης να ονομάζεται "property"Changed (δηλαδή εδώ εφόσον μας ενδιαφέρει το Name property, θα πρέπει να λέγεται NameChanged). Αυτό το event θα πρέπει να γίνεται raise όταν αλλάζει το bounded property. Έτσι λοιπόν, η κλάση σου θα γίνει κάπως έτσι:
Public Class Customer2
Private mName As String
Private mOriginalName As String
Private mDirty As Boolean
Public Event NameChanged As EventHandler
Private Sub OnNameChanged()
RaiseEvent NameChanged(Me, EventArgs.Empty)
End Sub
Public Sub New()
mName = ""
mOriginalName = ""
mDirty = False
End Sub
Public Property Name() As String
Get
Return mName
End Get
Set(ByVal Value As String)
mName = Value
OnNameChanged()
End Set
End Property
Public Property OriginalName() As String
Get
Return mOriginalName
End Get
Set(ByVal Value As String)
mOriginalName = Value
End Set
End Property
Public ReadOnly Property Dirty() As Boolean
Get
Return mDirty
End Get
End Property
End Class
Τώρα, αν δοκιμάσεις να αλλάξεις το property μέσα από κώδικα, θα αλλάξει και το text του textbox. Θα έχεις δηλαδή "two-way databinding".
Ας δούμε λοιπόν τι χρειάζεται για να φτάσουμε στο τελικό αποτέλεσμα. Υπάρχει ένα interface που λέγεται IEditableObject το οποίο μας δίνει έλεγχο στην διαδικασία εισαγωγής τιμών στα properties ενός object όταν αυτό χρησιμοποιείται ως datasource. To interface αυτό ορίζει τρία methods, τα BeginEdit, EndEdit, και CancelEdit. Οπότε η κλάση σου, γίνεται κάπως σαν την παρακάτω:
Public Class Customer
Implements IEditableObject
Private mName As String
Private mOriginalName As String
Private mDirty As Boolean
Public Event NameChanged As EventHandler
Private Sub OnNameChanged()
RaiseEvent NameChanged(Me, EventArgs.Empty)
End Sub
Public Sub New()
mName = ""
mOriginalName = ""
mDirty = False
End Sub
Public Property Name() As String
Get
Return mName
End Get
Set(ByVal Value As String)
mName = Value
If Not mDirty Then
mOriginalName = mName
End If
OnNameChanged()
End Set
End Property
Public Property OriginalName() As String
Get
Return mOriginalName
End Get
Set(ByVal Value As String)
mOriginalName = Value
End Set
End Property
Public ReadOnly Property Dirty() As Boolean
Get
Return mDirty
End Get
End Property
Sub BeginEdit() Implements IEditableObject.BeginEdit
If Not mDirty Then
mOriginalName = mName
mDirty = True
End If
End Sub
Sub CancelEdit() Implements IEditableObject.CancelEdit
If mDirty Then
mName = mOriginalName
mDirty = False
End If
End Sub
Sub EndEdit() Implements IEditableObject.EndEdit
OnNameChanged()
End Sub
End Class
Η προσθήκη αυτού του interface στην κλάση, μας δίνει τη δυνατότητα να χρησιμοποιήσουμε τις μεθόδους BindingManagerBase.EndCurrentEdit() και BindingManagerBase.CancelCurrentEdit(). Που βρίσκεται το BindingManagerBase; Φυσικά στο b (Dim b As Binding ) που ορίσαμε στην αρχή. Έτσι λοιπόν, μπορείς να πεις
Private Sub TextBox1_KeyUp(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles TextBox1.KeyUp
b.BindingManagerBase.EndCurrentEdit()
End Sub
' Just a button to cancel the editing
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button3.Click
b.BindingManagerBase.CancelCurrentEdit()
End Sub
Και έχεις την επιθυμητή συμπεριφορά! Ενδεχομένως να χρειάζεται λίγο ραφινάρισμα στο συγχρονισμό Name - OriginalName, εγώ έκανα μια υποθετική υλοποίηση... Όταν υλοποιήσεις σε μία κλάση το IEditableObject, πρέπει υποχρεωτικά να παρέχεις το δικό σου μηχανισμό για να κρατάς τις προηγούμενες τιμές (ώστε να μπορεί να γίνει rollback σε αυτές, κατά το CancelEdit). Όπως καταλαβαίνεις, με το OriginalName και το Dirty πέτυχες ακριβώς αυτό το requirement
. H κλάση που κατεξοχήν υλοποιεί το IEditableObject είναι η DataRow και με αυτόν τον τρόπο επιτυγχάνει στο να κρατάει τις προηγούμενες εκδόσεις.
Τέλος, τα καλά νέα είναι ότι στο ΝΕΤ 2.0 η DataBindings.Add υποστηρίζει μία έξτρα παράμετρο που καθορίζει ακριβώς αυτό, το πότε θα περνάνε οι αλλαγές στο datasource.
Vir prudens non contra ventum mingit