Dopo aver chiesto ad i CDays2008 a Mauro cosa pensava dei mixin, dopo aver letto i vari post di Andrea, Raffaele e Gian Maria che parlano di POCO e dopo aver letto i due post (qui e qui) nel blog inglese di Gian Maria ho deciso di spiegare meglio cosa intendevo con il mio commento al post di Andrea.
Gian Maria mostra due modi diversi per rivolvere il "problema" dell'interfaccia INotifyPropertyChanged creando proxy a run-time, io avevo risolto il solito problema usando Spring.NET AOP. Questo dimostra che come al solito ci sono svariati modi per ottenere il solito risultato.
Finita questa piccola anticipazione arrivo al nocciolo del discorso e sparo la mia sentenza...nessuna di queste implementazioni è usabile!! Vi chiederete perchè ed io sono qui per dimostrarvelo. Piccola demo, provate a creare una semplice BindingList<Customer>, aggiungete ad essa un numero imprecisato di Customer-proxaty, agganciate l'evento ListChanged ed in fine provate a modificare la proprietà di un Customer della BindingList...et vualà l'evento non viene scatenato, cosa che dovrebbe accadere se Customer implementasse normalmente INotifyPropertyChanged.
Quando per la prima volta mi sono trovato davanti a questo problema armato di Reflector sono andato a sbirciare il codice della classe BindingList<T> ed ho trovato l'inghippo. Guardando il codice mostrato nell'immagine vediamo che la seconda linea di codice verifica se il tipo INotifyPropertyChanged è assegnabile dal tipo Customer, questa verifica nel caso di Customer in versione POCO è assolutamente falsa dato che il tipo Customer non sa niente di INotifyPropertyChanged. Per confermare questa teoria mi sono creato un po di test unitari che riproducono il problema, vediamoli. In entrambi i casi di test la creazione del Customer è stata "nascosta" dentro dei metodi Factory dato che non mi interessa mostrarne l'implementazione in questo post. Per prima cosa ho creato due classi Customer la prima implementa INotifyPropertyChanged e la seconda no, però ha le properties marcate come virtual per permettere la creazione del proxy a run-time.
1: public class Customer : INotifyPropertyChanged
2: {
3: public event PropertyChangedEventHandler PropertyChanged;
4: private Guid id = Guid.NewGuid();
5: private string name;
6:
7: public Guid Id {
8: get { return id; }
9: }
10: public string Name {
11: get { return name; }
12: set {
13: if (name != value) {
14: name = value;
15: RaisePropertyChanged("Name");}
16: }
17: }
18: private void RaisePropertyChanged(string propertyName)
19: {
20: if (PropertyChanged != null)
21: PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
22: }
23: }
1: public class CustomerPoco
2: {
3: private Guid id = Guid.NewGuid();
4:
5: public virtual Guid Id {
6: get { return id; }
7: }
8: public virtual string Name { get; set; }
9: }
Dopodiché ho creato due metodi di test, uno verifica se il Customer è castabile a INotifyPropertyChanged mentre l'altro verifica se è assegnabile.
1: [Fact]
2: public void ShouldBeCompatibleWithTypeINotifyPropertyChanged()
3: {
4: Customer customer = CustomerFactory.CreateCustomer();
5: (customer is INotifyPropertyChanged).ShouldBeTrue("Is not INotifyPropertyChanged");
6: }
7: [Fact]
8: public void ShouldBeAssignableToTypeINotifyPropertyChanged()
9: {
10: typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(Customer))
11: .ShouldBeTrue("INotifyPropertyChanged is not assignable from Customer");
12: }
In questo caso entrabi i test passano perfettamente, proseguiamo eseguendo i soliti test su CustomerPoco:
1: [Fact]
2: public void ShouldBeCompatibleWithTypeINotifyPropertyChanged()
3: {
4: CustomerPoco customer = CustomerFactory.CreateCustomerPoco();
5: (customer is INotifyPropertyChanged).ShouldBeTrue("Is not INotifyPropertyChanged");
6: }
7: [Fact]
8: public void ShouldBeAssignableToTypeINotifyPropertyChanged()
9: {
10: typeof(INotifyPropertyChanged).IsAssignableFrom(typeof(CustomerPoco))
11: .ShouldBeTrue("INotifyPropertyChanged is not assignable from CustomerPoco");
12: }
In quest'ultimo caso il primo test passa mentre il secondo no distruggendo i nostri sogni di sviluppatore lazy che non doveva più implementare INotifyPropertyChanged . Ecco perchè nel mio commento al post di Andrea ho detto: "...perchè il framework di per se non conosce i mixin quindi è stato scritto "bloccandone" l'uso". Problemi simili sono risolvibili con diversi hack, basta barare con un po di reflection dopo aver istanziato la collection:
1: private static void EditRaiseItemChangedEvents(BindingList<Customer> list)
2: {
3: FieldInfo _fi = list.GetType().GetField("raiseItemChangedEvents", BindingFlags.NonPublic | BindingFlags.Instance);
4: bool raiseItemChangedEvents = (bool)_fi.GetValue(list);
5: if (!raiseItemChangedEvents) _fi.SetValue(list, true);
6: }
ed il gioco è fatto ma in generale hanno un costo troppo alto, sia di implementazione che di manutenzione dato che il problema non si presenta solo per collection come la BindingList ma anche per controlli come il BindingSource delle WindowsForm.