Se avete provato ad usare i nuovi strumenti per la gestione della configurazione in .NET 2.0, sicuramente ne sarete rimasti affascinati come è successo a me, che oramai cerco di usarli ovunque possibile. Vi sarà forse capitato anche di dover gestire delle configurazioni dinamiche sul tipo di quelle che adottano i vari provider di ASP.NET. Il MembershipProvider ad esempio - ma anche tutti gli altri in realtà - tipicamente espongono una serie di parametri di configurazione che si possono suddividere in parametri "generali", cioè che si applicano indistintamente a tutti i MembershipProvider e in parametri "specifici" che invece variano in base a che tipo di provider dobbiamo configurare. Il SqlMembershipProvider ad esempio richiede una serie di parametri aggiuntivi - uno per tutti, il nome della stringa di connessione - che non necessariamente si applicano ad altri tipi di provider. In effetti qualunque provider deve derivare da ProviderBase che espone un metodo Initialize() tra i cui argomenti troviamo una NameValueCollection che contiene i parametri "sconosciuti" che verranno gestiti direttamente dall'implementazione che stiamo realizzando.

Se vi trovare perciò a dover creare un vostro sistema di provider, ma anche nel caso in cui vogliate scrivere un sistema pluggabile, in cui i moduli aggiuntivi possano godere di parametri di configurazione propri espressi all'interno dell'xml nel web.config dovrete gestire tali parametri in nel ConfigurationElement che li prevede. Ecco un esempio di come realizzare questo meccanismo:

public class ApplicationInfo : ConfigurationElement
{
    
private NameValueCollection attributes = new NameValueCollection();

    [ConfigurationProperty("name")]
    
public string Name
    {
        
get return (string)this["name"]; }
        
set this["name"] = value; }
    }

    
//
    // TODO: definire le altre proprietà di default
    //

    
public NameValueCollection Attributes
    {
        
get return attributes; }
    }

    
protected override bool OnDeserializeUnrecognizedAttribute(string name, string value)
    {
        attributes.Add(name, 
value);
        
return true;
    }
}

OnDeserializeUnrecognizedAttribute e il complementare OnDeserializeUnrecognizedElement sono una coppia di metodi virtuali che verranno chiamati dal runtime durante la fase di deserializzazione della configurazione e consentono di gestire eventuali attributi o elementi imprevisti. Nel caso concreto il metodo non fa altro che aggiungere i valori ad una NameValueCollection e ritornare "true" per ogni attributo sconosciuto, informando il runtime che non se ne deve ulteriormente occupare. Se invece volessimo gestire una serie specifica di attributi potremmo ritornare "false" qualora quello trovato non ci competa e in questo caso il runtime solleverà una eccezione che informa dell'errore di configurazione.

powered by IMHO 1.3


Sono certo che, più di qualcuno si sarà accorto che nonstante la flessibilità della GridView, questa soffre di qualche problema. In particolare mi riferisco alla impossibilità di recuperare il numero totale di righe presenti sulla fonte dati quando si utilizza una ObjectDataSource. Per intenderci quello che nella vecchia DataGrid si chiamava il VirtualItemCount. Vediamo di chiarire i motivi per cui questo accade. Ci sono sostanzialmente due modi di collaborazione tra una GridView e una ObjectDataSource. Nel primo caso la DataSource fornisce tutti gli oggetti presenti nel repository e la GridView si incarica di mostrare solo quelli richiesti dai parametri di paginazione. Nel secondo caso invece la DataSource fornisce solo i record necessari e la GridView è obbligata a chiamare il metodo SelectCountMethod della DataSource per sapere quanti record sono presenti.

Ora, parrà strano, ma la GridView - a meno che io non abbia preso un forte abbaglio - non espone alcuna proprietà che fornisca il numero totale di righe presenti. Questa proprietà, credetemi sulla parola, sarebbe di utilità estrema dato che il più delle volte al cliente non interessa sapere a che pagina è arrivato sul totale di pagine, ma vuole avere una indicazione precisa di quanti elementi sono presenti nel resultset che sta scorrendo. Pare impossibile, ma a fare le moltiplicazioni evidentemente si fa fatica...

In definitiva il dato che cerchiamo deve essere presente da qualche parte. Se ci si pensa bene, il calcolo del paginatore richiede che si conosca quante righe contiene la datasource. Armato del solito reflector mi sono perciò messo alla ricerca di esso e ho scoperto che è talmente facile arrivarvi da poter considerare una vera svista non averlo esposto. Ecco la classe che estende la GridView che pubblica una proprietà VirtualItemCount:

public class XGridView : GridView
{
    
public int VirtualItemCount
    {
        
get return (int)ViewState["VirtualItemCount"]; }
    }

    
protected override void PerformDataBinding(IEnumerable data)
    {
        
base.PerformDataBinding(data);
        
        
this.ViewState["VirtualItemCount"] = 
            
this.ViewState["_!ItemCount"];
    }
}

Davvero banale! In sostanza il metodo PerformDataBinding() è quello che viene chiamato subito dopo che la DataSource, qualunque essa sia ha restituito sotto forma di IEnumerable il suo risultato. Nel controllo System.Web.UI.WebControls.CompositeDataBoundControl - base anche della GridView - si può trovare il seguente codice:

// extracted with Reflector

protected internal override void PerformDataBinding(IEnumerable data)
{
      
base.PerformDataBinding(data);
      
this.Controls.Clear();
      
base.ClearChildViewState();
      
this.TrackViewState();
      
int num1 = this.CreateChildControls(data, true);
      
base.ChildControlsCreated = true;
      
this.ViewState["_!ItemCount"] = num1;
}

Ecco svelato l'arcano. un controllo che abbia come Base il CompositeDataBoundControl deve implementare un metodo CreateChildControls() che restituisca come parametro di ritorno un intero che è appunto il totale di righe presenti nel repository, in seguito utilizzato per calcolare eventualmente il paginatore. Che cosa costasse aggiungere questa proprietà alla GridView proprio non mi è dato sapere.

Giusto per non lasciare l'argomento a metà, vale la pena di spiegare come fa la GridView a paginare il resultset. Nell'implementazione di CreateChildControls() della GridView vine utilizzata una PagedDataSource che si comporta diversamente nei due casi che prima ho prospettato. Nel caso di utilizzo del SelectCountMethod non c'è alcun rilevo da fare, ci si affida semplicemente a questo risultato, ma nel caso opposto invece il resultset viene scorso __tutto__ una riga alla volta è una variabile viene incrementata di una unità ad ogni ciclo. Tutto ciò per il semplice motivo che una DataSource rende un IEnumerable e non una ICollection... quindi niente Count.

Morale? Pensateci bene quando dovete decidere che tipo di paginazione volete usare...

powered by IMHO 1.3


Ho scritto una articoletto su come ottenere dalla GridView un dato di cui è gelosa custode. Si tratta di VirtualItemCount che chi ha utilizzato la vecchia DataGrid conosce bene. L'articolo, che esplora mediante Reflector il meccanismo di paging della GridView è molto interessante anche per capire le limitazioni del controllo in alcuni casi.

link: ASP.NET 2.0: Ottenere il VirtualItemCount dalla GridView


Il mio ultimo sforzo letterario stavolta appare sulle "colonne" del sito di xe.net. Si tratta di un articolo - che segue a ruota il mio primo screencast pubblicato ieri - nel quale finalmente torno a parlare della parte del framework .NET che preferisco. Mi riferisco naturalmente ad ASP.NET sulla quale non scrivevo da un po' di tempo. L'inaugurazione della sezione articoli dello user group xe.net è stata l'occasione per tornare a scrivere un articolo dedicandomi stavolta alle datasource gerarchiche, una delle novità meno conosciute di ASP.NET 2.0.

Link: Creare una ObjectDataSource gerarchica

powered by IMHO 1.3


Ormai sono abituato ad evitare a tutti i costi i popup nelle applicazioni web. Tuttavia mi rendo conto che c'è ancora qualcuno che non riesce a farne a meno, a torto o a ragione non mi interessa sapere. Durante la track web dei CommunityDays sono stato molto incuriosito da una domanda che è venuta dal pubblico e che recitava più o meno così: "è possibile effettuare il cross-page postback in un popup impostando il PostBackUrl?". La risposta naturale è no, ma quest'oggi, per puro diletto ho provato a capire se questo è proprio vero. In effetti se ragioniamo bene sul problema è evidente che esiste almeno un modo di effettuare il postback in una nuova finestra del browser, cioè impostando l'attributo "target" della form. Però impostare questo attributo a runtime mediante javascript è fattibile ma espone ad un fastidioso problema. Il mio primo tentativo è stato quello "semplice" di usare l'evento "OnClientClick" come segue:

btnPopup.OnClientClick = "document.forms[0].target = '_blank';";

Impostare l target in questo modo funziona perfettamente, ma lascia la form in uno stato inconsistente che fa si che tutti i controlli presenti effettuino il postback nella nuova finestra dopo che il pulsante è stato azionato per la prima volta. In effetti la cosa migliore sarebbe quella di ripristinare il target originale dopo il postback, ma in questo l'evento OnClientClick non ci può essere di aiuto perchè il runtime quando fa il merge degli script aggiunge il codice per il postback subito dopo quello immesso in questo attributo. La soluzione perciò è solamente quella di estendere il controllo Button per modificarne il rendering:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.IO;
using System.Text.RegularExpressions;

namespace Elite.Web.UI.WebControls
{
    
/// <summary>
    /// 
Summary description for ButtonEx
    
/// </summary>
    
public class ButtonEx : Button
    {
        
private string postBackTarget;

        
public ButtonEx()
        { }

        
protected override void Render(HtmlTextWriter writer)
        {
            
if (!string.IsNullOrEmpty(this.PostBackUrl) && 
                !
string.IsNullOrEmpty(this.PostBackTarget))
            {
                
using (StringWriter tempWriter = new StringWriter())
                {
                    
using (HtmlTextWriter htmlWriter = new HtmlTextWriter(tempWriter))
                    {
                        
base.Render(htmlWriter);
                        
                        Regex rx = 
new Regex(
                            @"onclick=""javascript:(?<code>[^""]*)""", 
                            RegexOptions.IgnoreCase);

                        writer.Write(
                            rx.Replace(
                                tempWriter.ToString(), 
                                
new MatchEvaluator(Evaluator)));
                    }
                }
            }
            
else
                base
.Render(writer);
        }

        
private string Evaluator(Match match)
        {
            
return string.Format(
                @"onclick=""javascript:document.forms[0].target='{0}';{1};" + 
                 "setTimeout(&quot;document.forms[0].target=''&quot;, 100);""",
                
this.PostBackTarget, 
                match.Groups["code"].Value);
        }

        
public string PostBackTarget
        {
            
get return postBackTarget; }
            
set { postBackTarget = value; }
        }
    }
}

Il codice è in effetti un po' tirato, lo confesso, ma è davvero l'unico modo che sono riuscito a trovare, dopo aver analizzato la classe Button del framework, per iniettare il codice prima e dopo la generazione del postback. in sostanza nel metodo render viene fatto generare il codice del pulsante in una stringa, e tramite una regular expression si effettua la sostituzione all'interno dell'evento onclick. Questo codice viene poi immesso nuovamente nel flusso del rendering. In questo modo impostando la proprietò PostBackTarget del ButtonEx si otterrà il sospirato postback in popup. L'unica curiosità che rimane è il fatto che ho dovuto usare un timeout di 100 millisecondi per reimpostare il target. Infatti semplicemente impostando la proprietà subito dopo WebForm_DoPostBackWithOptions non si ottiene l'effetto desiderato.

powered by IMHO 1.3

 


Mi sono finalmente deciso ad investire qualche minuto del mio tempo per farmi una macro che mi aiuti a risparmiare le decine di secondi che di solito impiego per fare l'attach al processo di aspnet_wp.exe, per debuggare un website in IIS su XP. Di solito preferisco questo metodo perchè così posso tenere aperto un browser sulla pagina che sto realizzando e non debbo eseguire uno stesso path all'interno del sito per arrivarci ogni volta con il webserver integrato di Visual Studio 2005.

Ecco la macro scritta in puro VB.NET (arrggggggg!!!)

Imports System
Imports EnvDTE
Imports EnvDTE80
Imports System.Diagnostics
Imports System.IO

Public Module AspNetMacros

    
Sub AttachASPNET()

        
Dim aspnet As EnvDTE.Process = 
            FindASPWP(_ApplicationObjects.DTE)

        
If Not aspnet Is Nothing Then

            
aspnet.Attach()

            System.Threading.Thread.Sleep(2000)

        
Else

            MsgBox
("Cannot find aspnet_wp.exe")

        
End If

    End Sub


    Public Function 
FindASPWP(ByRef dte As EnvDTE.DTE) As EnvDTE.Process

        
For Each proc As EnvDTE.Process In dte.Debugger.LocalProcesses
        
            
If (Path.GetFileName(proc.Name) = "aspnet_wp.exe") Then
                Return 
proc
            
End If
        
        Next

        Return Nothing

    End Function

End Module

Naturalmente potete anche customizzarla per fare l'attache al processo che desiderate. Spero che faccia risparmiare qualche ora di lavoro anche a voi.

powered by IMHO 1.3


Il nuovo modello di accesso ai dati basato sui DataSourceControl consente con facilità di fare il binding di controlli complessi come al GridView, la DetailView e la FormView con una facilità impensabile. I controlli DataSourceControl hanno un design fortemente configurabile per adattarsi alle varie esigenze. Ad esempio è possibile con facilità parametrizzare il risultato della DataSource per mezzo di Parametri che possono essere passati come argomenti ai metodi Select, Insert, Update e Delete. I parametri vengono poi tradotti in modo conforme al tipo di DataSourceControl. Ad esempio nel caso della ObjectDataSource essi divengono parametri del metodo richiamato durante una operazione di accesso ai dati.

Esistono un buon numero di questi parametri, ad esempio Parameter, QueryStringParameter, SessionParameter, che hanno il compito di prelevare l'informazione da qualunque supporto sia necessario interfacciare. La struttura di questi parametri è molto semplice tanto che è possibile con semplicità crearne di personalizzati. Ecco ad esempio un tipo di parametro che legge il contenuto di Context.Items e lo passa alla DataSource:

public class ContextItemParameter : Parameter
{
    
private string itemName;

    
protected override object Evaluate(
        System.Web.HttpContext context, 
        System.Web.UI.Control control)
    {
        
if ((context != null) && (context.Items != null))
            
return context.Items[this.itemName];

        
return null;
    }

    
public string ItemName
    {
        
get return itemName; }
        
set { itemName = value; }
    }
}

Il nocciolo del lavoro si trova nel metodo Evaluate() che si occupa di trasformare il contenuto dell'item in un dato digeribile dai DataSourceControl

powered by IMHO 1.3

 


I Web User Controls, più spesso conosciuti con il diminutivo di "ascx" per l'estensione che contraddistingue il loro template, sono sovente sottovalutati per la loro intrinseca facilità di uso. Questo è un grave errore in cui si incorre nell'errata supposizione che la loro semplicità sia ottenuta sacrificando la l'estensibilità tipica dei Custom Web Controls. COn questo breve esempio vorrei dimostrarvi quanto sto dicendo; Poniamo di creare un semplice UserControl come segue:

<%@ Control 
    
Language="C#" 
    
CodeFile="TemplatedUserControl.ascx.cs" 
    
Inherits="TemplatedUserControl" %>
<asp:Table 
    
runat="server" 
    
ID="tblTris" 
    
CellPadding="5" 
    
CellSpacing="0" 
    
BorderStyle="None">
    <asp:TableRow>
        <asp:TableCell 
/>
        <asp:TableCell 
/>
        <asp:TableCell 
/>
    <
/asp:TableRow>
    <asp:TableRow>
        <asp:TableCell 
/>
        <asp:TableCell 
/>
        <asp:TableCell 
/>
    <
/asp:TableRow>
    <asp:TableRow>
        <asp:TableCell 
/>
        <asp:TableCell 
/>
        <asp:TableCell 
/>
    <
/asp:TableRow>
<
/asp:Table>

Come evidente si tratta di uno UserControl che contiene una semplice tabella con nove celle, distribuite su tre colonne per tre righe. Ora andiamo a modificare la classe che implementa la logica dello UserControl:

using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.ComponentModel;

public partial class TemplatedUserControl : System.Web.UI.UserControl
{
    
private ITemplate buttonTemplate;

    
protected override void OnInit(EventArgs e)
    {
        
int i = 0;
        
foreach (TableRow row in tblTris.Rows)
        {
            
foreach (TableCell cell in row.Cells)
            {
                ButtonTemplateContainer container = 
new ButtonTemplateContainer();

                ButtonTemplate.InstantiateIn(container);
                WebControl textControl = container.FindControl("TitleText") 
as WebControl;
                cell.Controls.Add(container);

                
if (textControl != null)
                {
                    textControl.Width = textControl.Height = Unit.Pixel(50);

                    
if (textControl is ITextControl)
                        ((ITextControl)textControl).Text = i.ToString();
                    
else if (textControl is IButtonControl)
                        ((IButtonControl)textControl).Text = i.ToString();
                }

                i++;
            }
        }

        
base.OnInit(e);
    }

    [PersistenceMode(PersistenceMode.InnerProperty)]
    [Browsable(
false)]
    [TemplateContainer(
typeof(ButtonTemplateContainer))]
    
public ITemplate ButtonTemplate
    {
        
get return buttonTemplate;  }
        
set { buttonTemplate = value; }
    }
}

public class ButtonTemplateContainer : 
    Control, 
    INamingContainer
{
}

Questo codice, trasforma lo UserControl appena creato in un semplice ed efficace TemplateControl. In pratica nelle nove celle viene iniettato il template generato sulla base di quanto richiesto nel markup. Ecco come è possibile usare lo UserControl:

<Elite:TemplatedUserControl runat="server" ID="ucTest">
    <ButtonTemplate>
        <asp:Button 
runat="server" ID="TitleText" />
    <
/ButtonTemplate>
<
/Elite:TemplatedUserControl>

In breve sarà possibile configurare un template in ogni istanza configurata dello UserControl allo stesso modo di quello che si farebbe con una GridView. Come evidente le potenzialità degli UserControls vanno ben aldilà di quallo che si può di primo acchito pensare.

powered by IMHO 1.3

 


MaintainScrollPositionOnPostback è un nuovo attributo della direttiva @Page che definirei senza ombra di dubbio impagabile. Per comprenderne il significato basta pensare a cosa succede alla pagina quando invochiamo un postback. Se ad esempio la pagina è molto lunga e scrolliamo verso il basso nel momento in cui interviene il postback normalmente il browser torna a posizionare la pagina all'inizio costringendoci ad un "lavoro di rotella" un po' scomodo. Impostando a true questa proprietà questo comportamento verrà modificato riportando sempre la pagina nella posizione in cui si trovava al momento del postback.

Un grazie a Davide Vernole che stamane mi ha segnalato questa novità...

powered by IMHO 1.3


L'accesso alla ViewState nell pagine ASP.NET, ma anche a Session, Cache ed Application tipicamente soffre della mancanza di type-safety che costringe a scrivere dei cast ripetuti. Con il breve e semplice metodo generico qui riportato è possibile semplificare la gestione di questi utili strumenti

public string MyValue
{
    
get return GetViewState<string>("MyValue", string.Empty); }
    
set { SetViewState<string>("MyValue", value); }
}

private T GetViewState<T>(string name, T defaultValue)
{
    
if (ViewState[name] == null)
        SetViewState<T>(name, defaultValue);

    
return (T)ViewState[name];
}

private void SetViewState<T>(string name, T value)
{
    ViewState[name] = 
value;
}

Il metodo descritto ha il vantaggio di consentire anche la lazy initialization del valore, così da evitare di avere dei fastidiosi null di ritorno.

powered by IMHO 1.3


Nel Framework 2.0 sono presenti una serie di controlli che sono in gradi di sfruttare un nuovo tipo di binding ai dati. Mi riferisco ai controlli gerarchici, come ad esempio il TreeView, il quale ricevendo una datasouce che implementi una determinata interfaccia è in grado di visualizzare dati che non siano delle semplici tabelle. L'implementazione della datasource gerarchica, richiede la realizzazione di una serie di classi a supporto che a loro volta si basano su interfacce e classi astratte.

Quest'oggi, nel districarmi in mezzo ad esse ho scoperto mio malgrado che quello che si trova in MSDN non è tutto oro colato. Per farla breve, dopo aver replicato esattamente un esempio della documentazione ed aver cercato per ore di capire dove stavo sbagliando mi sono deciso a compilare il codice dell'esempio per poi rendermi conto che esso sofriva dello stesso problema che stavo riscontrando nel mio. Niente meno che una StackOverflowException causata da una ricorsione infinita.

Mi chiedo quanti altri di questi esempi non testati esistano su MSDN.

powered by IMHO 1.3


Stamane ho fatto una scoperta che definirei sconcertante. La semplice lettura della proprietà Controls, in alcuni WebControl di ASP.NET causa notevoli malfunzionamenti al ciclo di vita della pagina. In particolare stavo utilizzando il metodo di cui ho parlato in un post precedente, per cercare dei controlli all'interno della gerarchia della pagina. Questa ricerca, se eseguita all'interno dell'OnInit, fa si che alcuni controlli non manifestino più gli eventi tipici. Ad esempio, un LinkButton non notificava più il "Click", e una FormView non era più in grado di gestire il postback correttamente.

Dopo una lunga estenuante ricerca, sono riuscito ad individuare la riga che era origine del problema, e con mio stupore mi sono reso contro che in quel punto non facevo altro che leggere la proprietà Controls per enumerare i controlli figlio. C'è voluta una breve indagine on il Reflector per arrivare ad una possibile spiegazione. La proprietà Controls non legge semplicemente il valore di un field, ma si occupa anche di inizializzare la collection nel caso in cui essa non sia già popolata; ecco il codice estratto da reflector:

public virtual ControlCollection Controls
{
      
get
      
{
            
if ((this._occasionalFields == null) || 
                (
this._occasionalFields.Controls == null))
            {
                  
this.EnsureOccasionalFields();
                  
this._occasionalFields.Controls = 
                      
this.CreateControlCollection();
            }
            
return this._occasionalFields.Controls;
      }
}

Come appare chiaro questo codice si occupa di istanziare una ControlCollection di default qualora essa non sia già presente. A quanto pare questa inizializzazione causa problemi se effettuata esternamente. Suppongo che da qualche parte, all'interno del ciclo di vita di un chiamata, esista una porzione di codice che verifica se tale proprietà è già inizializzata e si comporta di conseguenza in modi del tutto diversi. In effetti, a ben guardare la classe Control espone un metodo che compie questa verifica e che torna utile per aggirare questo malfunzionamento. Si tratta di HasControls(), un metodo che restituisce false se la proprietà non è inizializzata. Evidentemente in Microsoft il problema ho avevano previsto, e quindi hanno fornito un metodo per aggirarlo. Ecco quindi come dovrebbe essere modificato il codice dell'esempio precedente:

public static T FindControlRecursive<T>(Control root, string id)
    where T : Control
{
    
if (root.ID == id && root is T)
        
return root as T;

    
if (root.HasControls())
    {
        
foreach (Control child in root.Controls)
        {
            T foundControl = FindControlRecursive<T>(child, id);
    
            
if (foundControl != null)
                
return foundControl;
        }
    }

    
return default(T);
}

In questo modo non si modificherà lo stato del controllo. Rimane semplicemente da rilevare che si tratta di un comportamento decisamente subdolo, che andrebbe affrontato e corretto. Probabilmente sollevare un'eccezione sarebbe stato preferibile. A me è costato 3 ore di lavoro. Spero che a voi vada meglio.

powered by IMHO 1.3