Καλώς ορίσατε στο dotNETZone.gr - Σύνδεση | Εγγραφή | Βοήθεια

Databinding και InotifyPropertyChanged - Μαμα, ο BindingManager κάνει του κεφαλιού του!

Στο dotNETZone.gr είχαμε μια ενδιαφέρουσα συζήτηση γύρω από ένα θέμα που προκύπτει με το databinding όταν χρησιμοποιούμε το INotifyPropertyChanged και σηκώνουμε το PropertyChanged event. Σκέφτηκα λοιπόν να παραθέσω το θέμα εδώ για όποιον συνάδελφο ενδιαφέρεται.

 

Το σενάριο (+ backgrounder):

Εχουμε ένα custom business object το οποίο έχει κάποια properties. Θέλουμε να "δέσουμε" αυτά τα properties με simple binding σε αντίστοιχα controls (π.χ. labels) μιας φόρμας, έτσι ώστε όταν αλλάζει κάποιο property να ενημερώνονται οι τιμές των controls της φόρμας χωρίς άλλες διαδικασίες. Εύκολα, γρήγορα, και απλά.

Για να το κάνουμε αυτό, εκτός από το γνωστό μακρινάρι:
MyLabel.Databindings.Add ("Text", myObjectInstance, "ΜyObjectProperty")
για κάθε property MyObject Property του MyInstance που θέλουμε να "δέσουμε" σε κάποιο Text property ενός label οπως το MyLabel, θα πρέπει να υλοποιήσουμε στο Business Object (έστω myObject) το interface INotifyPropertyChanged.

Ετσι, κάθε αλλαγή property θα σηκώνει ένα PropertyChanged event το οποίο θα "μιλάει" με τον υποβόσκων BindingManager και θα ενημερώνει αυτόματα την τιμή των labels στη φόρμα μας.

Σύμφωνα με το MSDN, πρέπει να κάνουμε Implements INotifyPropertyChanged στην κλάση μας και να προσθέσουμε κώδικα όπως ο παρακάτω:

Public Event PropertyChanged As PropertyChangedEventHandler _
Implements INotifyPropertyChanged.PropertyChanged


Private Sub NotifyPropertyChanged(ByVal info As String
   RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(info))
End Sub

Ετσι θα μπορούμε να καλούμε την Sub NotifyPropertyChanged όταν η τιμή κάποιου property αλλάζει, όπως παρακάτω:

Public Property CompanyName() As String
Get
   Return Me.companyNameValue
End Get

Set(ByVal value As String)
   If Not (value = companyNameValue) Then
      Me.companyNameValue = value
      NotifyPropertyChanged("CompanyName")
   End If
End Set
End Property

Οπως ισως προσέξατε, το propertyChanged event θέλει ως παράμετρο το όνομα του property που άλλαξε.

Από εδώ και πέρα, άν έχουμε "δέσει" την τιμή του property CompanyName στο label π.χ. lblCompanyName (κάπως έτσι: lblCompanyName.Databindings.Add ("Text", MyInstance, "CompanyName") οπου MyInstance το instance του class μας που περιέχει το property CompanyName, η τιμή του label θα ενημερώνεται αυτόματα όταν θα γίνονται αλλαγές στο property CompanyName.

Φυσικά, για όσα properties καλούμε την NotifyPropertyChanged θα γίνεται το ίδιο.

 

Το πρόβλημα

Εστω οτι υλοποιούμε αυτή τη λογική για ένα object που έχει π.χ. 10 properties, με ισάριθμα labels σε μια Windows Form. Πολύ γρήγορα θα διαπιστώσουμε οτι συμβαίνει το εξής:

Οταν σηκώνεται ένα PropertyChanged event ενημερώνονται ΟΛΑ τα databound labels της φόρμας με τις τιμές των properties του υποκείμενου business object instance, ανεξαρτήτως αν δεν αφορούν το property που άλλαξε ή άν έχουν αλλάξει ή οχι τιμή.

Αυτό σημαίνει οτι για κάθε PropertyChanged event που σηκώνεται θα έχουμε στο συγκεκριμένο σενάριο 10 ενημερώσεις! Και αυτό έχοντας αλλάξει μόνο την τιμή ενός property!

Αυτό μπορείτε να το επαληθεύσετε πολύ εύκολα αποφεύγοντας να "σηκώσετε" το PropertyChanged event σε κάποιο property του object σας. Αλλάζοντας την τιμή του property, φυσικά δεν ενημερώνεται το bound πεδίο μια και το PropertyChanged event δεν έχει σηκωθεί. Ομως, αν ενημερώσετε ΚΑΠΟΙΟ ΑΛΛΟ property που ΣΗΚΩΝΕΙ το PropertyChanged event, θα δείτε με έκπληξη οτι θα ενημερωθεί και η τιμή του control που είναι bound στο πρώτο property!

Σύμφωνα με τη γνώμη ορισμένων όπως εδώ, συμβαίνει το εξής:

"Any property changed event will result in
BindingManagerBase.PushData which causes a PushData on *all* its Bindings."

Ομως, αυτό μας οδηγεί στο εξής συμπέρασμα:

Αν έχω 10 properties bound σε αντίστοιχα controls και ενημερώσω και τα 10, τότε ο συνολικός αριθμός ενημερώσεων που θα γίνει στα controls θα είναι 10 x 10 = 100! Γιατί για κάθε property που σηκώνει ένα PropertyChanged event γίνεται ενημέρωση σε ΟΛΑ τα bound controls που έχω!

Και φυσικά στην εξής απορία:

Τότε ΤΙ το θέλουμε το όνομα του property στο event;

Για τη δεύτερη απορία είχαν την απάντηση στο DevelopersDex:

> What's the point of specifying a property name in the

When it comes to WinForms databinding there is no point afaik.    But
remember that INotifyPropertyChanged can be used elsewhere too, eg. for the
PropertyGrid (dunno if it refreshes all properties on change there) and also
for WPF (where it doesn't refresh all properties on change).

Το θέμα είναι όμως οτι έχουμε ένα πρόβλημα. Τις 100 ενημερώσεις! Πώς θα μπορούσαμε να αποφύγουμε αυτό το πρόβλημα; Για να το κάνω ακόμα πιό σοβαρό, φανταστείτε οτι έχουμε ένα ΜΕΓΑΛΟ object με 100 properties bound σε αντίστοιχα controls μιας φόρμας. Αν αλλάξουμε ΟΛΑ τα properties (π.χ. κάνοντας ένα fetch από τη βάση δεδομένων) τότε θα έχουμε 100 x 100 = 10000 ενημερώσεις!

 

Η λύση (στο σημείο που μας ενδιαφέρει)

Αφού δεν μπορούμε να το αποφύγουμε, μαλλον πρέπει να καθήσουμε και να το απολαύσουμε. Ο κ. Κελαιδίτης (kelman) όμως στο dotNETZone.gr έχει διαφορετική άποψη. Μπορούμε, τουλάχιστον, να αποφύγουμε τις πολλαπλές ενημερώσεις ως εξής, σύμφωνα με τον κώδικα που παραθέτει:

Είναι αναπόφευκτο ωστόσο, όταν δεν ξέρεις ποιό από τα 1000 properties θα αλλάξει. Αν ξέρεις, βάζεις το PropeprtyChanged μόνο στο property που σε ενδιαφέρει. Από την άλλη, μπορείς να αποφύγεις έναν τέτοιο σενάριο υλοποιώντας τον κατάλληλο μηχανισμό. Πρόσεξε: Ο designer όταν σχεδιάζει τη φόρμα βάζει στην αρχή Me.SuspendLayout και αφού τοποθετήσει όλα τα controls, στο τέλος Me.ResumeLayout(False) ώστε να αποφύγει τον τμηματικό σχεδιασμό της φόρμας. Το ίδιο κάνει και το DataRow object όπου με DataRow.BeginEdit κάνεις suspend την ενεργοποίηση των events που προκύπτουν από τα validation rules και στο τέλος με DataRow.ΕndEdit κάνεις ένα συνολικό validate. Αντίστοιχα κι εσύ μπορείς να φτιάξεις ένα ζευγάρι Begin/End ή Suspend/Resume για τη business κλάση σου. Πχ:

Imports System.ComponentModel

Public Class Customer
Implements INotifyPropertyChanged

Public Event PropertyChanged(ByVal sender As Object, ByVal e As System.ComponentModel.PropertyChangedEventArgs) Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged

Private _CustomerID As String
Private _CustomerName As String

Private notificationSuspended As Boolean = False

Public Property CustomerID() As String
Get
   Return _CustomerID
End Get
Set(ByVal value As String)
   _CustomerID = value
   RaisePropertyChanged("CustomerID")
End Set
End Property

Public Property CustomerName() As String
Get
   Return _CustomerName
End Get
Set(ByVal value As String)
   _CustomerName = value
   RaisePropertyChanged("CustomerName")
End Set
End Property

Public Sub SuspendNotification()
   notificationSuspended = True
End Sub

Public Sub ResumeNotification(ByVal notifyChanges As Boolean)
   notificationSuspended = False
   If notifyChanges Then
      RaisePropertyChanged(String.Empty)
   End If
End Sub

Private Sub RaisePropertyChanged(ByVal propertyName As String)
   If Not notificationSuspended Then
      RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(propertyName))
   End If
End Sub
End Class


Έτσι, αν πεις

x.CustomerID = "a"
x.CustomerName = "b"

τρέχουν δύο events. Αν πεις

x.SuspendNotification()

x.CustomerID &= "2"
x.CustomerName &= "2"

x.ResumeNotification(True)

τρέχει ένα event. Και αν πεις

x.SuspendNotification()

x.CustomerID &= "3"
x.CustomerName &= "3"

x.ResumeNotification(False)

 

 

 

Έχουν δημοσιευτεί Τετάρτη, 7 Φεβρουαρίου 2007 9:30 πμ από το μέλος cap
Δημοσίευση στην κατηγορία:

Ενημέρωση για Σχόλια

Αν θα θέλατε να λαμβάνετε ένα e-mail όταν γίνονται ανανεώσεις στο περιεχόμενο αυτής της δημοσίευσης, παρακαλούμε γίνετε συνδρομητής εδώ

Παραμείνετε ενήμεροι στα τελευταία σχόλια με την χρήση του αγαπημένου σας RSS Aggregator και συνδρομή στη Τροφοδοσία RSS με σχόλια

Σχόλια:

Χωρίς Σχόλια

Ποιά είναι η άποψή σας για την παραπάνω δημοσίευση;

(απαιτούμενο)
απαιτούμενο
(απαιτούμενο)
ÅéóÜãåôå ôïí êùäéêü:
CAPTCHA Image