Magazine

Un HeaderTemplate per la GridView

Andrea Boschin

30/05/2006

I controlli standard del framework non sono n grado di soddisfare le esigenze più raffinate. Non fa eccezione la gridview che in questo aticolo sarà estesa per aggiungere una importante proprietà

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

ASP.NET 2.0

Elite.Web.Controls.zip ()

Sono certo che tutti voi come me, guardate con rispetto ai WebControls che espongono uno o più template. Non fosse altro che per la loro estrema versatilità è evidente che la presenza di un template in un WebControl gli da una marcia in più. A partire dallla WebForm, passando per Panel, e View fino a giungere ai più evoluti FormView e GridView, il template è uno degli elementi fondamentali che percorrono in lungo e in largo ASP.NET. Tuttavia succede talvolta che i template non bastino, che li dove logica vorrebbe ci dovrebbe essere un template, in realtà si ha a che fare con una semplice proprietà che non può soddisfare appieno le nostre esigenze. Un caso esemplare di quanto sto dicendo è riscontrabile nella GridView. Questo spendido pezzo di alta "gioielleria", espone un bel numero di template, che consentono di personalizzare praticamente tutti gli elementi che la compongono salvo uno. Lo Header della GridView è sostanzialmente un elemento intoccabile la cui personalizzazione così come succedeva per la DataGrid in ASP.NET 1.1 richiede uno sforzo di codice abbastanza importante.

Ora, come consueto non è facile che io mi accontenti e quando ho avuto la necessità di utilizzare ampiamente un controllo GridView mi è balenata l'idea di estenderla per aggiungere il template di cui avevo bisogno. L'esercizio però è tutt'altro che semplice ed immediato, perchè richiede oltre alla conoscenza del meccanismo di costruzione di un template, anche e soprattutto il punto giusto in cui intervenire per farlo funzionare a dovere. Quest'ultimo particolare è il più difficile da capire, perchè il flusso di funzionamento della GridView non è documentato, ma risulta nascosto all'interno del controllo stesso e solo svariati test lo possono rivelare. Perciò ancora una volta la soluzione al problema ha preso il nome di Reflector, per mezzo del quale ho inseguito metodi e proprietà fino ad arrivare ad una possibile soluzione.

Cos'è un template?

Suppongo che chiuque sappia qual'è il significato di questo termine inglese. Il Template altro non è che un modello da seguire durante la costruzione di qualcosa, nel nostro caso una GridView, e la sua potenza si cela nella forte capacità di personalizzazione che racchiude. Per comprendere l'importanza dei Template in ASP.NET vi propongo un interessante esercizio. Aprite MSDN, e cercate la documentazione della classe Page. in breve vi renderete conto che essa eredita direttamente dalla classe TemplateControl, che altro non è che la base dei controlli - e la pagina è a tutti gli effetti un controllo - che dispongono di una template editabile come succede per Page e UserControl. Questo però è solo un modo di imporre un template ad un controllo, che in effetti è limitato a Page e UserControl, ma esiste tutta una serie di WebControls che consente di specificare quasi ogni tipo di aspetto. La stessa GridView ad esempio espone una proprietà PagerTemplate che consente in modo molto semplice di personalizzare i controlli usati per la paginazione dei record. Se analizziamo questa proprietà con il reflector noteremo immediatamente che il suo tipo è ITemplate. Questa interfaccia espone un solo metodo la cui funzione è di istanziare i controlli che corrispondono al template all'interno di un contenitore. Ecco l'interfaccia:

public interface ITemplate
{
    
void InstantiateIn(Control container);
}

In buona sostanza, quando si attribuisce una proprietà di tipo ITemplate ad un WebControls, questa verrà convertita in una particolare assegnazione nel codice autogenerato dal runtime. In questa assegnazione viene creata una istanza di una classe CompiledTemplateBuilder, che ha l'incarico di interpretare i tag immessi nel template e convertirli in una serie di WebControls che di volta in volta verranno inseriti nel container richiesto. Nella riga successiva ho riportato il codice che viene generato dal runtime:

@__ctrl.HeaderTemplate = 
    
new System.Web.UI.CompiledTemplateBuilder(
        
new System.Web.UI.BuildTemplateMethod(
            
this.@__BuildControl__control4));

CompiledTemplateBuilder è naturalmente una classe che implementa ITemplate e infatti per utilizzarne l'output è sufficiente chiamare al momento giusto il metodo InstantiateIn(), passando come argomento il controllo Container che dovrà ricevere nella sua collezione di controlli quelli prodotti dal Template. Una volta compreso questo meccanismo è molto semplice sfruttarlo per le proprie esigenze più disparate, infatti l'unica accortezza da avere è che il controllo passato come argomento implementi l'interfaccia INamingContainer. Questo perchè il runtime dovrà basarsi sul controllo contenitore per generare degli ID validi e univoci per i controlli contenuti.

Dove intervenire?

Ora che è un po' più chiaro come funziona il meccanismo di istanziazione di un ITemplate, è giunto il momento di approfondire la GridView per trovare il punto giusto in cui inserire il nostro codice. La costruzione della GridView avviene in due stadi principali. Il primo stadio è quello in cui avviene la selezione delle righe dalla DataSource, mentre il secondo è quello in cui vengono generate le righe rispettando le colonne che sono state espresse nel markup. Se analizzaimo il flusso di funzionamento della GridView ci renderemo presto conto dell'esistenza di un metodo CreateChildControls(), cui viene passato il risultato dell'interrogazione della datasource, è ha il compito di creare la tabella sulla cui base è strutturata la GridView, attingendo direttamente ai dati ottenuti. L'idea quindi è di effettuare l'override di questo metodo ed inserire il nostro template al suo posto subito dopo che il metodo base non abbia fatto il suo lavoro. E' importante intervenire dopo la creazione delle righe proprio perchè solo allora avremo una grid costituita interamente e potremmo trovare al suo interno tutti gli elementi che ci servono. Il codice a quel punto dovrà creare un'istanza del template, trovare al suo interno degli elementi utili e infine inizializzare i vari controlli con il titolo ottenuto dalla proprietà HeaderText, in modo che l'intervento sia del tutto trasparente alla GridView. Ecco il codice così come è riportato nell'esempio allegato a questo articolo:

/// <summary>
/// 
costruisce i controlli dopo il databinding
/// </summary>
/// <param name="dataSource">
dati ottenuti dalla datasource</param>
/// <param name="dataBinding">
flag che indica se il binding è avvenuto</param>
/// <returns>
numero di righe prese in causa</returns>
protected override int CreateChildControls(IEnumerable dataSource, bool dataBinding)
{
    
// costruisce la grid normalmente
    
int result = base.CreateChildControls(dataSource, dataBinding);

    
// esegue il codice solo se c'è un template
    
if (HeaderTemplate != null && HeaderRow != null)
    {
        
// scorre le celle presenti nel template
        
for (int idx = 0; idx < HeaderRow.Cells.Count; idx++)
        {
            
// per ogni cella istanzia il template usando il container e lo aggiunge alla cella
            
TemplateContainer container = new TemplateContainer(this);
            HeaderTemplate.InstantiateIn(container);
            HeaderRow.Cells[idx].Controls.Clear();
            HeaderRow.Cells[idx].Controls.Add(container);

            
if (Columns.Count>idx)
            {
                
// cerca il controllo del testo
                
Control textControl = FindTextControl(container, "HeaderText");

                
if (textControl is ITextControl)
                    
// se si tratta di un controllo testuale inserisce il testo
                    
((ITextControl)textControl).Text = Columns[idx].HeaderText;
                
else if (textControl is IButtonControl)
                {
                    
// se si tratta di un pulsante imposta il testo il commandname
                    // in modo che provochi l'ordinamento della colonna
                    
IButtonControl button = textControl as IButtonControl;

                    button.Text = Columns[idx].HeaderText;
                    button.CommandName = "Sort";

                    
if (!string.IsNullOrEmpty(Columns[idx].SortBLOCKED EXPRESSION
                        button.CommandArgument = Columns[idx].SortExpression;
                    
else if (textControl is WebControl)
                        
// disabilito il click delle colonne se non c'è sort expression
                        
((WebControl)textControl).Attributes["onclick"] = "return false;";        
                }
                
else
                    throw new 
InvalidOperationException("HeaderTemplate does not contains control 'HeaderText'");

                
// gesisce la posizione dell'immagine
                
if (!string.IsNullOrEmpty(Columns[idx].SortBLOCKED EXPRESSION
                {
                    
// trova il controllo immagine
                    
Image image = ControlUtil.FindControlRecursive<Image>(container, "SortImage");

                    
if (image != null)
                    {
                        
// imposta l'immagine in base all'ordinamento corrente
                        
if (!string.IsNullOrEmpty(SortExpression) &&
                            SortExpression == Columns[idx].SortExpression &&
                            !(
string.IsNullOrEmpty(SortAscendingImageUrl) ||
                              
string.IsNullOrEmpty(SortDescendingImageUrl)))
                        {
                            image.ImageUrl =
                                (SortDirection == SortDirection.Ascending) ? sortAscendingImageUrl : SortDescendingImageUrl;
                        }
                        
else
                            
image.Visible = false;
                    }
                }
            }
        }
    }

    
return result;
}

Il controllo così esteso dovrà esporre una nuova proprietà HeaderTemplate di tipo ITemplate che raccolga la definizione data dall'utente. Questa proprietà dovrà essere decorata con alcuni attributi per fare in modo che un tool quale Visual Studio riesca a leggerli e a comportarsi di conseguenza. L'attributo più importante per il nostro intento è PersistenceMode il cui valore indica al CompiledTemplateBuilder in che modo affrontare il campo. Tale attributo dice come devono essere interpretate le informazioni che provengono DataSource. Ecco la proprietà come viene dichiarata:

/// <summary>
/// 
espone il template
/// </summary>
[Browsable(false)]
[PersistenceMode(PersistenceMode.InnerProperty)]
[DefaultValue((
string)null)]
[TemplateContainer(
typeof(TemplateContainer))]
public ITemplate HeaderTemplate
{
    
get return headerTemplate; }
    
set { headerTemplate = value; }
}

L'inserimento di questa proprietà rende ora possibile scrivere il seguente markup, che è importante notare sarà disponibile a livello di intellisense grazie all'applicazione degli attributi alla proprietà HeaderContainer, e soprattutto potrà essere espresso anche in un file .skin:

<elite:ExGridView
    
id="exGrid"
    
runat="server">
    <HeaderTemplate>
        <
table class="gridHeader" cellpadding="0" cellspacing="0">
            <
tr>
                <
td><class="gridHeaderLTerm"></p></td>
                <
td class="gridHeaderSort">
                    <asp:Image 
runat="server" ID="SortImage" /></td>
                <
td class="gridHeaderTitle">
                    <asp:LinkButton 
runat="server" ID="HeaderText" /></td>
                <
td><class="gridHeaderRTerm"></p></td>
            <
/tr>
        <
/table>
    <
/HeaderTemplate>
    
...
</elite:ExGridView>

Il template così inserito verrà ora iniettatto in tutti gli header di colonna e preparato in modo che reagisca correttamente al click dei controlli in esso contenuti. In particolare esso verifica la presenza di un IButtonControl oppure di un ITextControl e conseguentemente attribuisce il testo, il CommandName e il CommandArgument, poi sposta la propria attenzione all'immagine e vi attribuisce la giusta direzione in base all'ordinamento corrente. In questo modo l'Header risulta facilmente personalizzabile nonostante il suo funzionamento rientri perfettamente nel ciclo di vita del controllo GridView.Il vantaggio per chi sviluppa è del tutto evidente; Semplicemente inserendo opportunamente questo template si potrà gestire la visualizzazione nell'Header del classico indicatore dell'ordinamento, personalizzadolo come meglio di crede, con l'unico obbligo di dover inserire un controllo denominato HeaderText e una Image denominata SortImage. Il controllo testuale potrà essere indistintamente una label, oppure un hyperlink o un button. Dato che vengono utilizzate le proprietà ITextControl e IButtonControl n realtà qualunque controllo che si adegui ad esse è ben accetto, al limite anche uno implementato da noi come UserControl o Web Custom Control.

Commenti
Nome

Sito web
Commento


indietro