Windows Forms Databinding - PowerPoint

Document Sample
Windows Forms Databinding - PowerPoint Powered By Docstoc
					Windows Forms Databinding
Raffaele Rialdi MVP C#
malta@vevy.com
http://mvp.support.microsoft.com
Cos'è il binding?
• Letteralmente significa legame e permette di stabilire
  automaticamente lo scambio di valori tra controllo e una
  sorgente dati.
   – Il databinding evita codice noioso e ripetitivo nel quale
     assegnamo i dati al controllo e successivamente li riassegnamo
     alla sorgente dati.
• Il binding con DataSource 'poveri' implica l'uso di
  reflection e quindi impoverisce le performance.
  Se invece il DataSource implementa le interfacce giuste
  il discorso è molto diverso.
Big picture ...
• In ADO.NET le sorgenti dati disconnesse non hanno più
  il concetto di record corrente.
• Il binding di dotnet è gestito da un intermediario tra
  controllo e sorgente dati
• La presenza di un unico intermediario per bindare più
  controlli garantisce il sync tra questi



          Sync              BindingManagerBase   Data
Sincronizzazione tra controlli

• E se non volessimo i due controlli sincronizzati?
• E se volessimo scorrere i dati indipendentemente sui
  due controlli?
• Risposta: bisogna avere due intermediari




                            BindingManagerBase


          Sync
                                                  Data

                            BindingManagerBase
BindingContext
• BindingContext è semplicemente una lista di BindingManagerBase
  mantenuta tramite Hashtable
• Poiché il nome del datasource è parte della chiave della Hashtable,
  non più di un BindingManagerBase con lo stesso datasource può
  esistere in un BindingContext
• Perció per non avere sync tra due controlli, i due
  BindingManagerBase devono appartenere a due BindingContext
  diversi                                            BindingContext
                                                        (Hashtable)

                                  BindingManagerBase


            Sync
                                                                      Data

                                  BindingManagerBase

                                                       BindingContext
                                                         (Hashtable)
Esempio SimpleBinding
Text, Font, BindingContext
Binding Manager
•   Il binding manager è l'intermediario
•   Per ogni datasource esiste un solo binding manager
•   Per ogni binding manager ci sono uno o più controlli
•   Questo serve per avere più controlli sincronizzati durante
    la navigazione dei dati.

• La classe base del binding manager è
  BindingManagerBase (astratta) e ha due classi derivate:
    – PropertyManager gestisce il binding con singoli elementi
    – CurrencyManager gestisce il binding con liste di elementi
Simple binding
• Associa un qualsiasi tipo ad un controllo in modo da semplificare la
  presentazione di un valore e poterlo aggiornare
                      Proprietà                DataMember
                      controllo   DataSource

   int i=5;
   myLabel.DataBindings.Add("Text", i, null);

                                   se è null viene usato
                                         ToString()


• Il binding con un singolo elemento implica l'uso di PropertyManager
    – La proprietà Position sarà sempre 0
• Il binding con una lista di elementi implica l'uso di CurrencyManager
  che ha il concetto di 'record corrente'.
    – Si usa Position per navigare le righe mostrate
    – Non si usa Position per leggere la posizione perchè la lista potrebbe
      contenere elementi che non vengono mostrati (es. filtro sulla dataview)
    – Si usa Current per leggere l'elemento nella lista sottostante (datasource)
Tip
• FAQ!
  Ricavare l'elemento del datasource data la riga
  selezionata nella datagrid.

• In caso di DataSet/DataTable ritorna una DataRowView

       DataRowView drv = GetCurrentBindedObject(DataGrid1) as DataRowView;

       private object GetCurrentBindedObject(DataGrid dg)
       {
           if(dg == null || dg.DataSource == null) return null;
           BindingManagerBase bmb = dg.BindingContext[dg.DataSource];
           if(bmb == null || bmb.Count == 0) return null;
           return bmb.Current;
       }
Sincronia dei dati
• Le variazioni del controllo vengono messe nel
  datasource quando:
   – Il datasource è un oggetto che espone proprietà
   – Il datamember è specificato esplicitamente nel binding


• Le variazioni del datasource vengono messe nel
  controllo quando:
   – Sono vere le condizioni di prima
   – Esiste un evento [NomeProprietà]Changed (ValChanged) che
     segnala le modifiche al datasource
     oppure
   – La classe supporta IBindingList (lo vedremo più avanti)
Simple e Complex Binding
                                CurrencyManager
 BindingContext                      oppure
                                PropertyManager
                   BindingManagerBase

                   BindingManagerBase                      DataSource     Data
                                        Simple Binding
                                                           DataMember
                    Bindings               Binding
                                                           Control
           BindingsCollection             Binding
                     oppure                                PropertyName
    ControlBindingsCollection             Binding




 BindingContext                              Complex Binding

                   BindingManagerBase

                                                                          Data
                                                           DataSource
                                        Simple Binding
                                                           DataMember
                                          Binding
                                                           Control
                                                           PropertyName
Esempio SingleElement
Simple binding
sul singolo elemento
Simple Binding: format e parse

• La classe binding (disponibile solo nel simple binding)
  offre due eventi importanti:
   – format. Intercetta il dato proveniente dal DataSource prima che
     venga impostato nel controllo.
   – parse. Intercetta il dato che dal controllo sta per essere trasferito
     al DataSource
• È un ottimo metodo per migliorare la qualità della
  visualizzazione dei dati
• Non è utile alla validazione che deve essere effettuata
  dal controllo
Esempio FormatParse
eventi format e parse
Complex binding
usando gli oggetti ado.net
•   IListSource permette di 'scoprire' qual'è la collection (IList) che contiene i veri
    elementi con cui effettuare il binding.
•   La collection è la DataView
•   Gli elementi sono le DataRowView                   DataSource

                                                          DataMember




                               IList IListSource.GetList()
       DataSet                 {
                                    return this.DefaultView;
                               }

                 DataTable                          DataView


                              DataRow                          DataRowView

                              DataRow                          DataRowView

                              DataRow                          DataRowView
Tips
• Cosa specificare in DataSource e DataMember?
   – DataSource = myDataSet
   – DataMember = myTable.myColumn
   Non specificare mai nel DataSource myDataSet.myTable perché:
   – il nome del DataSource funge da Key in BindingContext
   – il designer usa la convenzione in alto


• Quando si usa il CurrencyManager, può essere necessario usare il
  suo metodo Refresh affinché il controllo venga aggiornato.
  La Listbox ha bisogno di questo refresh mentre altri controlli no.

• Affinché il DataSource sia sicuramente aggiornato è necessario
  chiamare EndCurrentEdit
Perché rinunciare al dataset
• Non sempre abbiamo bisogno di tutto ciò che il dataset
  offre
• Il dataset è un contenitore generico e come tale è
  costretto a gestire in modo meno efficente i dati al suo
  interno
• Anche quando viene tipizzato l'uso a design time è
  farraginoso perchè ci sono troppe proprietà/metodi
• Ma soprattutto perchè non rappresenta esattamente i
  nostri dati ma siamo costretti ad adeguare i dati al
  dataset
Custom entities
1. Creare una collection dei nostri dati             Customer
2. Possibilmente tipizzarla         Customer         Customer
3. Implementare almeno:             Collection       Customer
   •   IEnumerable per il mondo web                  Customer
   •   IList per il mondo WinForms
4. Molte altre interfacce per renderla ricca di funzionalità al
   runtime e al design time (... le vedremo ...)
 Collection
• Strada più veloce: derivare da ArrayList e implementare gli overload
  tipizzati per rendere più piacevole l'uso della collection

• Strada più elegante: implementare IList, ICollection e IEnumerable
  usando per composition un ArrayList privato

• Derivando CollectionBase non otteniamo una collection ben
  tipizzata e ci 'brucia' la possibilità di derivare la collection da un'altra
  classe base visto che in dotnet non c'è multiple inheritance
 Collection
• Implementare IList (e quindi anche ICollection e
  IEnumerable)
• IList: metodi base come Add, Remove, Clear e Contains
• IList è l'unica interfaccia indispensabile per essere un
  datasource valido nelle Winform
• ICollection: proprietà Count, SyncRoot
• IEnumerable: GetEnumerator indispensabile per poter
  usare foreach
• IEnumerable è l'unica interfaccia indispensabile per
  essere un datasource valido nelle Webform
 Duplicabilità (opzionale)
• ICloneable. Unico metodo è Clone
• MSDN dice che è facoltà del programmatore scegliere
  se si vuole implementare Clone come:
   – Deep Copy: copia della collection e di tutti gli elementi
     referenziati [Implementazione complessa]
   – Shallow Copy: copia della sola collection che condividerà gli
     elementi con la collection di partenza [Implementazione
     semplice]
 Usabile dal designer
• IComponent oppure MarshalByValueComponent
• IComponent unito agli attributi permette l'uso nel
  designer. Si aggiunge la collection alla toolbox e si
  trascina sulla superficie del form
   – [DesignerCategory("")] oppure [DesignerCategory("component")]
   – [ToolboxItem(true)]
   – [DesignTimeVisible(true)]



• MarshalByValueComponent ci risparmia la fatica di
  implementare IComponent ma ci "brucia" la possibilità di
  derivare da un'altra classe
 IListSource: gerarchia a
design time
• Dato un DataSource vedo in DataMember l'elenco delle collection
  figlie

• Una possibilità è quella di usare un oggetto contenitore e segnare il
  getter delle collection con l'attributo:
  DesignerSerializationVisibility

• Se invece si vuole la gerarchia analoga a quella del DataSet devo
  costruire un oggetto analogo al DataViewManager:
    – il count riporta sempre 1 (così a design time c'è una sola riga)
    – ITypedList riporta le informazioni sulle inner-collection
    – costruire un PropertyDescriptor personalizzato


                                                                Implementazione omessa
                                                                 nell'esempio per brevità
 Serializzabile, usabile da
remoting
• [Serializable] oppure ISerializable
• La serializzazione permette di attraversare i remoting
  boundary
 Abilitazione alle notifiche, alle
ricerche, editabile e ordinabile
• IBindingList fornisce tutte quelle caratteristiche che
  troviamo nella DataView. È l'interfaccia più noiosa da
  implementare
• Proprietà Allowxxx, Supportxxx, Sortxxx
• Metodo AddNew che permette di inserire
• Metodo ApplySort che permette di ordinare
• Evento ListChanged per segnalare al controllo che i dati
  nella lista sono cambiati
   – Questo implica che l'oggetto child (collezionato) deve segnalare
     alla lista se e quando è cambiato
 Abilitazione alle notifiche, alle
ricerche, editabile e ordinabile
• E se non si implmenta IBindingList?
       – Per aggiornare i dati è necessario dare uno 'scrollone' al binding:
        private void RefreshGrid()
        {
            BindingManagerBase bmb = dataGrid1.BindingContext[dataGrid1.DataSource];
Tip!        bmb.SuspendBinding();
            bmb.ResumeBinding();
        }



       – Per notificare le modifiche è necessario creare un evento che
         abbia il nome della proprietà + "Changed"
         Esattamente quanto già visto in una slide iniziale e nell'esempio
         "SingleElement"
 Informazioni sullo schema
• ITypedList permette ai controlli di scoprire lo schema a
  runtime.
• Permette di conoscere il tipo che si 'nasconde' dietro una
  proprietà.
   – Metodo GetItemProperties. Se la proprietà è un tipo complesso
     (collection) bisogna fornirgli l'elenco delle proprietà del tipo che
     si nasconde dietro la collection.
   – Metodo GetListName. Se la proprietà è una collection, noi
     dovremo fornirgli il nome (stringa) del tipo collezionato
 Informazioni sullo schema
•       Con ITypedList possiamo fare a meno delle TableStyles:
         – cambiare nomi alle colonne (per esempio "NomeAzienda" in "Nome Azienda")
         – ordinare le colonne in modo arbitrario
         – nascondere le colonne non desiderate
    public PropertyDescriptorCollection GetItemProperties(PropertyDescriptor[] listAccessors)
    {
        // prende il nome dell'oggetto collezionato con una funzione custom
        Type t = GetPropertyType(listAccessors); // vedi esempio per l'implementazione

        PropertyDescriptorCollection pdc = TypeDescriptor.GetProperties(t);
        // dentro pdc ci sono tutte le informazioni sul tipo, compresi gli attributi

        pdc = pdc.Sort(Customer.PropertyNames);       // ordina le colonne
        int Len = Customer.PropertyNames.Length;
        PropertyDescriptor[] props = new PropertyDescriptor[Len];

        for(int i=0; i<Len; i++)
        {
            if(Array.IndexOf(Customer.PropertyNames, pdc[i].Name) == -1)
                continue;   // elimina le colonne non presenti nell'array
            props[i] = new BizPropertyDescriptor(pdc[i], null); // PropertyDescriptor custom
        }

        PropertyDescriptorCollection newpdc = new PropertyDescriptorCollection(props);
        return newpdc;
    }
 Gerarchica
• Nessuna interfaccia. Questo viene gratis quando si
  implementa bene la ITypedList e il nostro object model è
  costruito secondo i canoni classici
                                                               Si potrebbe scrivere una implementazione che nel
                        NWindSale                               binding mostri i dati sulla stessa riga mentre gli
                                                                            oggetti restano separati
                                                               Ma non ha forse più senso un business object che
                     Orders
                                             Order                            li inglobi entrambi?

                                             Order


                                  OrderDetails                              Products
                                                            OrderDetail                   Product
                                                                            Products
                                                            OrderDetail                   Product
                                                                            Products
                                                            OrderDetail                   Product




Nota: l'articolo: http://support.microsoft.com/kb/325682/EN-US/ mostra
    come creare una JoinView per mostrare dati di due DataTable
 Validazione ed errori
• Per gestire la validazione dei dati è necessario implementare
  IDataErrorInfo nella classe collezionata
• Metodo IDataErrorInfo.Item (indexer)
    – ci passa il nome della colonna (stringa)
    – restituiamo stringa vuota oppure l'errore da mostrare come tooltip
• Proprietà get di IDataErrorInfo.Error
    – restituiamo stringa vuota oppure l'errore da mostrare nell'header di riga
      della grid
    – Analogo a impostare la proprietà RowError della DataRow
 Modifiche transazionali
•   L'oggetto collezionato può decidere di gestire la transazionalità delle
    modifiche implementando IEditableObject
•   Bisogna implementare i 'famosi' metodi BeginEdit, EndEdit, CancelEdit
    tenendo da parte i valori temporanei delle proprietà fino al commit o rollback
    della transazione

•   Per evitare equivoci, stiamo parlando di modifiche sull'interfaccia grafica,
    non del database
•   Per esempio:                Utente sceglie la riga




          EndEdit                    BeginEdit                     CancelEdit




       Cambio di riga              Inizio modifiche            Pressione tasto ESC
625 righe di collection
Dov'è il trucco?
• Parola d'ordine: "case tools" cioè generatori di codice
   – Se usano CodeDom è meglio perchè possono generare codice
     VB.net, C#, ....
   – Se usano StringBuilder è facile realizzarli
• Generare cosa?
   – Il business object base a partire dalla tabella del db
     Attenzione! I business object spesso non mappano 1:1 con le
     tabelle quindi un lavoro manuale è sempre dovuto
   – La collection super-accessoriata. Questo è un task semplice:
     basta partire dall'esempio di questa sessione ed eseguire un
     banale replace con una regular expression
Alla fine ci siamo rifatti il
DataSet ...
•   [Serializable]
    public class DataSet :
            MarshalByValueComponent,
            IListSource, ISupportInitialize, ISerializable

•   [Serializable]
    public class DataTable :
            MarshalByValueComponent,
            IListSource, ISupportInitialize, ISerializable

•   public class DataView :                       // Analogo della collection di custom object
            MarshalByValueComponent,
            IBindingList, IList, ICollection, IEnumerable, ITypedList, ISupportInitialize

•   public class DataRowView :               // Analogo del custom object
            ICustomTypeDescriptor, IEditableObject, IDataErrorInfo

•   public class DataViewManager :                // Conserva una lista di impostazioni delle DefaultView
            MarshalByValueComponent,
            IBindingList, IList, ICollection, IEnumerable, ITypedList

•   [Serializable]
    public class DataRow
Ne vale la pena?
• Se si sviluppa RAD (vita dell'app molto corta), è meglio il
  DataSet
• Se il progetto è molto semplice può convenire usare il
  DataSet
• Negli altri casi io preferisco custom entities:
   – I business objects e le sue regole non si possono sempre
     deformare per farli assomigliare ad un database.
   – I business object non sono tabellari e non hanno
     necessariamente una semplice relazione 1:1 o 1:molti
   – Il db deve rimanere sempre solo un contenitore per la
     persistenza
La parola ai benchmark ...
... per quello che valgono
                                                public class CustomClass
•   CustomClass vs DataSet                      {
•   Solo operazioni in memoria (no binding)         private int _c0, _c1;
                                                    private decimal _c2, _c3;
•   CustomClass "full-optional"                     private double _c4, _c5;
                                                    private string _c6, _c7, _c8, _c9;
•   DataSet tipizzato o meno non cambia             // ...
                                                }

•   Risultato velocità:                         public TimeSpan DoTest(ITestBiz obj)
                                                {
                                                    DateTime d1 = DateTime.Now;
                                                    for(int i=0; i<1000000; i++)
                                                    {
                                                        obj.AddOne();
                                                        obj.DeleteOne();
                                                    }

•   Risultato memoria (bytes in all heaps):         for(int j=0; j<100; j++)
                                                    {
     – DataSet      231'891'952 byte   11,33%           for(int i=0; i<10000; i++)
     – Custom       208'285'852 byte   10,18%               obj.AddOne();
                                                        obj.Clear();
     (misure eseguite con perfmon)                  }
                                                    DateTime d2 = DateTime.Now;
                                                    return d2 - d1;
                                                }
Cosa ci manca ancora?
• Mantenimento dello stato pre-modifica (aggiornamento e
  cancellazione)
• Flag per marcare i nuovi oggetti nella collection
• Un analogo della DataView
• Un sistema di persistenza su DataBase ... ORM
Cosa ci porta VS2005?
• Gestione del DBNull nell'infrastruttura del binding!
  (niente più problemi con controlli come il datetimepicker)
• Proprietà NullMapping per la sostituzione del DBNull
• Gestione visuale delle "List" in controlli come la combo
• "Smart Captions". Se la colonna si chiama "NomeAzienda" il binding
  la cambierà automaticamente in "Nome Azienda: " con una regex
• Eventi "string-typed" dalle colonne del dataset tipizzato (oggi sono
  generiche)
• Uso delle partial classes per i DataSet tipizzati
• L'oggetto TableAdapter per i dataset (che supporta nativamente i
  DBNull)

• BindingSource e BindingNavigator (nella beta1 si chiamavano
  DataConnector e DataNavigator)
  ... comodo ma non può far altro che usare reflection e quindi è meno
  efficente della soluzione presentata
Domande?



     E poi non dite che è la solita sessione ...
        ... dove si racconta la solita storia ...

 ... che si fa tutto senza scrivere una riga di codice 