Magazine

Creare una ObjectDataSource gerarchica

Andrea Boschin

14/05/2006

La objectdatasource di ASP.NET è uno dei controlli più apprezzati. Tuttavia non è in grado di soddisfare tutte le esigenze. Ad esempio non è in grado di effettuare il binding di treeview.

0%100%
per esprimere un voto è necessario registrarsi al sito

ASP.NET 2.0, ADO.NET

Elite.Web.DataSources.zip (39,00 Kb)

Con l'avvento del nuovo ASP.NET 2.0, è stata introdotta una nuova concezione di databinding basato sulle DataSource, delle classi il cui scopo è quello di comportarsi da ponte tra il controllo che richiede i dati e la fonte da cui questi vengono attinti. Questa nuova modalità, che ha semplificato enormemente la creazione di interfacce fortemente basate sul databinding, rivela tutta la sua potenza quando configurando esclusivamente alcuni attributi dei controlli DataSource, e talvolta senza nemmeno scrivere una riga di codice si riesce ad ottenere tutta una serie di feature, praticamente build-in, che prima richiedevano un dispendio in termini di tempo di sviluppo molto più oneroso.

Non tutti ne sono al corrente, ma il databinding del framework 2.0 offre delle possibilità ancora più potenti che sono accessibili però al prezzo stavolta della stesura di un po' di codice - nemmeno troppo a dire la verità - ma che possono regalare delle soddisfazioni ancora maggiori. Chi almeno una volta abbia provato ad utilizzare una SiteMapDataSource dovrebbe comprendere a cosa mi riferisco. A prima vista infatti la facilità con cui si fa uso di essa può far apparire che il suo comportamento sia del tutto analogo a quello delle altre datasource presenti nei namespace System.Web, ma un'analisi più approfondita dovrebbe rivelare che in realtà la fonte dati e l'output sono sostanzialmente diversi. Mentre nel caso di una SqlDataSource o di una ObjectDataSource il binding è per così dire piatto, con la SitemapDataSource invece ci troviamo di fronte ad un tipo di binding gerarchico. I dati ottenuti dalla fonte - ad esempio il file web.sitemap - non sono riconducibili direttamente ad una struttura sul tipo di una tabella di database, ma essa è una rappresentazione nidificata che pone un livello di complessità molto più pesante al consumer della datasource.

Quello che assolutamente manca nel framework è una versione gerarchica delle datasources di uso più comune, come ad esempio la ObjectDataSource che a tutti gli effetti non può assolutamente essere usata per questo scopo. Se infatti provare a collegare una TreeView - cioè un controllo che rappresenta dei dati gerarchici - ad una datasource "normale" otterrete una eccezione tanto chiara quanto disarmante:

The DataSourceID of 'TreeView1' must be the ID of a control of type IHierarchicalDataSource. 'ObjectDataSource1' is not an IHierarchicalDataSource.

Indagando ulteriomente sulla SiteMapDataSource si scopre che essa implementa l'interfaccia IHierarchicalDataSource che eredita da un curioso HierarchicalDataSourceControl. Quest'ultimo è un controllo astratto che implementa tutta una serie di funzionalità base atte a facilitare la presentazione di dati in forma gerarchica. E' evidente che la ObjectDataSource , sarà utilissima fintanto che dovremmo presentare dei dati in forma tabellare, ma non servirà sostanzialmente a nulla quando ci troveremo a presentare una struttura gerarchica come ad esempio una organizzazione tassonomica sul tipo di quelle che spesso si incontrano in tutti i progetti web.

Costruiamo una ObjectHierarchyDataSource

In un caso come quello descritto poco fa, ci troveremmo di fronte ad un bivio. O torniamo ad utilizzare i metodi arcaici di binding popolando a runtime il controllo TreeView con i TreeNode creati sulla base dei dati estratti, oppure dovremmo costruirci un controlo DataSource che ci faciliti ovunque ne abbiamo bisogno fornendo alla TreeView i dati nella forma che essa si aspetta. Dovremmo quindi crearci una ObjectHierchyDataSource che accetti degli oggetti strutturati gerarchicamente e li fornisca attraverso l'interfaccia IHierarchicalDataSource .

Il primo passo in questa direzione, che è sicuramente la più auspicabile, è stabilire quali e quante classi dovremmo creare. Implementare ex-novo un DataSourceControl non è una attività semplice, anzi richiede un lavoro molto attento ed espone ad errori potenzialmente gravi. L'idea migliore è quella di tentare l'estensione della ObjectDataSource per aggiungere ad essa le caratteristiche necessarie. Tuttavia anche questa non è una attività semplice, infatti estendendo un controllo già esistente ci troveremmo a non poter ereditare dalla classe HierarchicalDataSourceControl dato che in C# non esiste l'ereditarietà multipla, ma saremo costretti ad implementare uno ad uno i metodi e le proprietà imposti dall'interfaccia IHierarchicalDataSource .

Soffermarsi un attimo ad analizzare le due possibilità rivela che in definitiva la strada dell'estensione della ObjectDataSource è di gran lunga la più semplice, sia perchè i metodi e le proprietà dell'interfaccia IHierarchicalDataSource non sono molti e nemmeno complessi, ma soprattutto perchè tutta la parte di accesso ai dati sarà completamente riutilizzabile. Scegliamo quindi questa strada e descriviamo le classi che andranno create ipotizzando di voler visualizzare una tassonomia gerarchica:

Category: classe di business che conterrà i dati della categoria. Questa classe dovrà implementare una interfaccia particolare per poter collaborare con la IHierachicalDataSource . Si tratta dell'unica classe non riutilizzabile perchè specifica del dominio

ObjectHierarchicalCollection: collection specializzata per contenere le istanze di oggetti gerarchizzati. Anche questa classe che deriva direttamente da un ArrayList dovrà implementare una specifica interfaccia per il medesimo motivo.

ObjectHierarchyDataSourceView: Classe derivata direttamente da HierarchicalDataSourceView che avrà la responsabilità di mantenere la collection di oggetti gerarchici all'interno della datasource

ObjectHierarchyDataSource: Controllo da inserire nelle pagine che sfruttando la collaborazione delle classi precedenti fornisce gli oggetti richiesti al controllo che li richiede 

Iniziamo a vedere come è realizzata la classe che rappresenta una categoria:

[Serializable]
public class Category : IHierarchyData
{
    
private Category parentCategory;
    
    
#region IHierarchyData Members

    public 
IHierarchicalEnumerable GetChildren()
    {
        ObjectHierarchicalCollection collection =
            
new ObjectHierarchicalCollection();

        
foreach (IHierarchyData item in this.Children)
            collection.Add(item);

        
return collection;
    }

    
public IHierarchyData GetParent()
    {
        
return this.Parent;
    }

    
public bool HasChildren
    {
        
get return this.Children.Count > 0; }
    }

    
public object Item
    {
        
get return this; }
    }

    
public string Path
    {
        
get return MakePath(this); }
    }

    
private string MakePath(Category searchCategory)
    {
        StringBuilder builder = 
new StringBuilder();

        
for (Category node = searchCategory; node != null; node = node.Parent)
            builder.Insert(0, node.description  + "\\");

        
return builder.ToString();
    }

    
public string Type
    {
        
get return "Category"; }
    }

    
public Category Parent
    {
        
get return parentCategory; }
        
set { parentCategory = value; }
    }

    
#endregion
}

La porzione di codice qui riportata mostra l'implementazione dell'interfaccia IHierachyData . Come detto poco fa si tratta dell'unica interfaccia che dovrà essere implementata ogniqualvolta dovremmo utilizzare una classe diversa. In effetti sarebbe molto meglio creare una classe astratta che di volta in volta ci possa aiutare nell'utilizzo di questa interfaccia, ma così facendo ci bruceremmo la possibilità di fare capo ad una gerarchia di oggetti specifica del domino applicativo. Nel codice sorgente allegato si troverà la classe Category nella sua totalità. Quello che conta ora è dare uno sguardo soprattutto ai metodi GetChildren() che compone una ObjectHierarchicalCollection contenente i nodi figli del nodo corrente e la proprietà Path che deve restituire il percorso completo per giungere a questa categoria. Quest'ultima proprietà è molto importante perchè viene utilizzata dal runtime per determinare la posizione cui è giunto durante la visualizzazione. Se questa proprietà non restituisce un valore coerente si rischia di causare un ciclo infinito nell'esecuzione. 

Ora è giunto il momento di prendere visione delle altre classi. Nel riquadro che segue è possibile vedere l'implementazione di ObjectHierarchicalCollection e di ObjectHierarchyDataView

public class  ObjectHierarchicalCollection : ArrayList, IHierarchicalEnumerable
{
    
public  ObjectHierarchicalCollection()
    {}

    
#region IHierarchicalEnumerable Members

    public 
IHierarchyData GetHierarchyData( object  enumeratedItem)
    {
        
return  enumeratedItem  as  IHierarchyData;
    }

    
#endregion
}

// --------------

public class  ObjectHierarchicalDataSourceView : HierarchicalDataSourceView
{
    
private  ObjectHierarchicalCollection collection;

    
public  ObjectHierarchicalDataSourceView()
    {}

    
public  ObjectHierarchicalDataSourceView(IEnumerable data)
    {
        
this .collection =  new  ObjectHierarchicalCollection();

        
foreach  (IHierarchyData item  in  data)
            
this .collection.Add(item);
    }

    
public override  IHierarchicalEnumerable Select()
    {
        
return this .collection;
    }
}

L'implementazione di queste classi è molto semplice. La prima è richiesta per avere una collection che espone l'interfaccia IHierarchicalEnumerable, che è un nuovo membro del framework 2.0 tale interfaccia consente la enumerazione di strutture gerarchiche. Nella nostra classe il metodo GetHierarchyData() restituisce l'oggetto enumerato così com'è dato che esso implementa direttamente IHierarchyData. Nello stesso riquadro è presente anche la classe ObjectHierarchicalDataSourceView. La DataSourceView è una classe utilizzata da tutti i controlli di tipo Datasource per contenere i dati da visualizzare. Nel nostro caso semplicemente conterrà una collection gerarchica e ne restituirà un riferimento nel metodo Select().

public class  ObjectHierarchyDataSource : ObjectDataSource, IHierarchicalDataSource
{
    
public event  EventHandler DataSourceChanged;

    
#region IHierarchicalDataSource Members

    public 
HierarchicalDataSourceView GetHierarchicalView( string  viewPath)
    {
        IEnumerable children = FindChildData(
base .Select(), viewPath);

        
if  (children!= null )
            
return new  ObjectHierarchicalDataSourceView(children);

        
return new  ObjectHierarchicalDataSourceView();
    }

    
private  IEnumerable FindChildData(IEnumerable data,  string  path)
    {
        
if  ( string .IsNullOrEmpty(path))
            
return  data;

        foreach 
(IHierarchyData item  in  data)
        {
            
if  (item.Path == path)
                
return  item.GetChildren();

            
if  (path.StartsWith(item.Path))
                
return  FindChildData(item.GetChildren(), path);
        }

        
return null ;
    }

    
#endregion
}

Quest'ultima classe è la datasource vera e propria. L'interfaccia IHierarchicalDataSource in effetti è molto semplice ed impone esclusivamente la presenza dei metodi GetHierarchicalView() e FindChildData(). Questi metodi, aggiunti a quelli esposti dalla ObjectDataSource consentiranno ai controlli come la TreeView di estrarre gli oggetti annidati. In particolare il metodo FindChildData() come anticipato richiede che venga specificato il path dell'oggetto di cui si desiderano i figli. E' proprio in questo punto che il codice porterà ad un cliclo infinito se la proprietà path non è gestita correttamente.

Le classi presentate in questo articolo, sono facilmente riutilizzabili, a patto di rispettare i vincoli di cui ho parlato. E' probabile che con un po' di lavoro, sfruttanto le caratteristiche del metodo GetHierarchyData() della classe ObjectHierarchicalCollection si possa arrivare ad una ulteriore generalizzazione, ma questo esula dallo scopo dell'articolo e lo lascio al lettore come utile esercizio. Un'altra attività da considerare è quella di aggiungere i metodi per consentire il databinding bidirezionale, che nella mia implementazione è stato volutamente trascurato.

Commenti
Nome

Sito web
Commento


indietro