Non c'entra nulla con l'esame con cui sto bombardando
pacificamente il mio blog , però in questi giorni mi sono posto il
problema di far vivere un oggetto business all'interno di una WF, facendo in
modo di usare il data-binding
per riempire i controlli e di mantenere sincronizzato lo stato
dell'oggetto in questione in base alle operazioni fatte dall'utente attraverso
la UI.
Dedico questo post a mio fratello, se non altro per avermi ispirato con
un bell'oggetto HockeyPlayer da quando ha installato sul suo PC NHL
non-so-che-versione, dovuto all'euforia di Torino 2006
. Ho creato questo oggetto con qualche
semplice proprietà e con un'altra un po' più complessa. Vediamo parte del
codice.
using System;
using System.ComponentModel;
namespace DataBindingSample
{
public class HockeyPlayer
{
private string name;
private decimal height;
private decimal weight;
private FaultsCollection faults;
public string Name
{
get { return name; }
set { name = value; }
}
public decimal Height
{
get { return height; }
set { height = value; }
}
public decimal Weight
{
get { return weight; }
set { weight = value; }
}
public FaultsCollection Faults
{
get { return faults; }
}
public HockeyPlayer(string Name, decimal Height, decimal Weight)
{
name = Name;
height = Height;
weight = Weight;
faults = new FaultsCollection();
}
static public HockeyPlayer CreateHockeyPlayer()
{
return (new HockeyPlayer("Name", 0, 0));
}
}
}
La classe HockeyPlayer espone 4 proprietà: Name,
Height, Weight ed un'altra FaultsCollection così definita.
using System;
using System.Collections.ObjectModel;
namespace DataBindingSample
{
public class FaultsCollection : Collection<DateTime> { }
}
La classe ci permette di utilizzare un oggetto che rappresenta un giocatore
di hockey, con un elenco di tutti i falli che subìto durante la partita,
espressi tramite una banale Collection<DateTime>. In questo modo, posso
istanziare un oggetto in modo abbastanza semplice:
HockeyPlayer pl = HockeyPlayer.CreateHockeyPlayer();
pl.Name = "Brian Storm";
pl.Height = 182;
pl.Weight = 74;
pl.Faults.Add(new DateTime(2006, 2, 22, 12, 51, 00));
A questo punto, ho creato un WF con 3 TextBox ed 1 ListBox per
visualizzare i membri della classe. Questa WF si basa sui seguenti concetti:
- Espone una proprietà pubblica Player che mappa un oggetto
della classe HockeyPlayer. Tale proprietà fa ovviamente
riferimento ad un membro privato player.
- Ho creato un metodo internal chiamato SetupDataBindings() che si occupa di
inizializzare per ogni Control la sua datasource. Tale metodo viene chiamato
solo una volta durante il Load del form stesso
- Ho messo un pulsante di test che modifica alcuni membri dell'oggetto
privato player, aspettandomi che i controls bindati
a questo oggetto vengano refreshati automaticamente
Detto questo, vi mostro parte del codice relativo al form che ho creato.
// membro privato
private HockeyPlayer player;
// proprietà pubblica da esporre
public HockeyPlayer Player
{
get { return player; }
set { player = value; }
}
// handler dell'evento Load
private void FormHockeyPlayer_Load(object sender, EventArgs e)
{
SetupDataBindings();
}
// metodo privato che inizializza il data-binding
internal void SetupDataBindings()
{
txtName.DataBindings.Add("Text", player, "Name");
txtWeight.DataBindings.Add("Text", player, "Weight");
txtHeight.DataBindings.Add("Text", player, "Height");
lstFaults.DataSource = player.Faults;
}
Facciamo un ulteriore passo avanti: ho creato un
Main() che innanzitutto crea un oggetto
HockeyPlayer, e lo passa al form di cui sopra per poterne
mostrare i valori. Ecco il codice:
static void Main()
{
HockeyPlayer pl = HockeyPlayer.CreateHockeyPlayer();
pl.Name = "Brian Storm";
pl.Height = 182;
pl.Weight = 74;
pl.Faults.Add(new DateTime(2006, 2, 22, 12, 51, 00));
FormHockeyPlayer frm = new FormHockeyPlayer();
frm.Player = pl;
frm.ShowDialog();
}
Il form appare, i controlli sono valorizzati e posso sorridere compiaciuto.
Fin qua nulla di particolare, le magie
del data-binding le conosciamo bene, e non c'è alcun motivo per gioire in modo
particolare. Se non chè....ho avuto un problema: ho accennato prima nel punto
(3) di aver messo sul form un Button che va a modificare alcuni valori del mio
HockeyPlayer. Ad esempio, cambia il nome del giocatore, cambia
il suo peso e la sua altezza e - vero problema - aggiungo un elemento DateTime
alla sua proprietà Faults:
private void button1_Click(object sender, EventArgs e)
{
string text = string.Format("Matthew Werder Brema");
player.Name = text;
player.Weight = 98.5M;
player.Height = 190M;
player.Faults.Add(DateTime.Now);
}
Uno potrebbe
pensare che se modifico l'oggetto player privato, in realtà
vengono aggiornati anche i controls verso i quali questo oggetto è
bindato, ma non è così. Sì, è vero, il mio membro
player è il data-source per i miei controlli, ma la
cosa non è così automatica. Questa estate, quando ho sbranato "Windows
Forms Programming in C#" di Chris Sells, ho capito come risolvere il problema: basta
che nel mio oggetto business vado ad implementare l'interfaccia INotifyPropertyChanged che,
come dice il nome stesso, permette di notificare al FX che una proprietà è stata
modificata. Il FX di conseguenza gestisce la situazione e provoca un refresh dei
controlli bindati. Ecco un piccolo estratto del codice della classe
HockeyPlayer
(riporto
solo la proprietà Name):
public class HockeyPlayer : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set { name = value; NotifyPropertyChanged("Name"); }
}
public event PropertyChangedEventHandler PropertyChanged;
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
}
In pratica, nel costrutto set di ogni property che
vogliamo notificare, dobbiamo sollevare un evento di tipo PropertyChangedEventHandler.
Tale evento viene gestito in automatico dal FX, che si occupa di provocare un
refresh dei controlli che sono in binding con la classe stessa. Adesso, quando
eseguo il progetto e clicco sul Button di test, vedo le 3 TextBox che cambiano
valore in modo completamente trasparente, e senza scrivere codice apposta.
Ma....c'è sempre una ma!
Notare una cosa: nel codice button1_Click() che
ho incollato sopra, in realtà aggiungo un elemento DateTime alla proprietà
Faults del mio oggetto. Mi aspetto quindi che la ListBox venga
refreshata di conseguenza, ma non è così. Ci risiamo, dunque. Ieri sera
ho ripreso in mano il libro di Chris Sells alla ricerca di una soluzione, e l'ho
trovata solo a metà. Vi riporto quello che scrive Chris alla fine del
capitolo 13, pagina 516, del suo volume "Windows Forms Programming in C#":
A more full-featured integration with the data-grid including enablig the
datagrid to edit the data and the ability to keep the datagrid up-to-date as the
list data-source is modified - requires an implementation of the IBindingList interface,
something that is beyond the scope of this book.
Dal momento che Chris non ha avuto tempo di farvi vedere come fare 'sta
cosa, scriverò un post dedicato, ma ovviamente non è nulla di trascendentale:
come dice la sua nota qui sopra, è sufficiente implementare l'interfaccia IBindingList all'interno della nostra classe
FaultsCollection inclusa in HockeyPlayer.