Chi usa ASP.NET conosce il metodo FindControl() che consente di trovare un controllo all'interno del proprio genitore. Sa sicuramente anche che questo metodo non è ricorsivo, ma si limita al primo livello e di solito si usa scrivere un metodo apposito che implementa questa ricorsività. In rete se ne trovano parecchi esempi ma voglio proporvene uno che ho scritto stamane per eseguire ricerche mirate ad uno specifico tipo di controlli:

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

    
foreach (Control child in root.Controls)
    {
        T foundControl = FindControlRecursive<T>(child, id);

        
if (foundControl != null)
            
return foundControl;
    }

    
return default(T);
}

L'uso di un generics in questo caso consente di specificare il tipo di controllo da ricercare nella gerarchia, semplificando di molto la stesura del codice. Se poi avete l'ardire di togliere il constraint allora scoprirete che è possibile anche cercare un controllo che implementa una determinata interfaccia...

Lo confesso, mi sto innamorando. Un amore un po' generics, ma sempre di amore si tratta...

powered by IMHO 1.3


Nel framework 2.0 è presente una nuova interfaccia ITextControl, il cui compito è molto semplice, almeno quanto utile per chi sviluppa webcontrols. ITextControl identifica i controlli che espongono una proprietà Text e che quindi sono utilizzabili per "mostrare un output all'utente".

Implementano quindi ITextControl tutti quei controlli che avendo una proprietà Text possono consentire di mostrare qualcosa all'utente. Literal, Label, TextBox sono tutti controlli ITextControl. Tuttavia vorrei che qualcuno mi spiegasse perchè LinkButton e Button non implementano questa interfaccia. Questi ultimi implementano IButtonControl, che a sua volta espone la proprietà Text, ma era cos' difficile fare in modo che IButtonControl derivasse da ITextControl?

Pare una stupidaggine, ma forse non avete idea della quantità e qualità di codice che questo avrebbe consentito:

Control textControl = 
    FindTextControl(container, "HeaderText");

if (textControl is ITextControl)
    ((ITextControl)textControl).Text = 
this.Columns[i].HeaderText;
else if (textControl is IButtonControl)
    ((IButtonControl)textControl).Text = 
this.Columns[i].HeaderText;
else
    throw new 
InvalidOperationException("...");

sarebbe diventato:

ITextControl textControl = 
    FindTextControl(container, "HeaderText");

if (textControl != null)
    textControl).Text = 
this.Columns[i].HeaderText;
else
    throw new 
InvalidOperationException("...");

A volte basta talmente poco.

powered by IMHO 1.3


Se si prova a dare alle webparts un aspetto un po' più accattivante rispetto a quello che hanno di default ci si scontra con un piccolo ma fastidioso bug, riconosciuto da Microsoft come tale.  Le webpart, volenti o nolenti hanno sempre un padding di 5 pixel attorno al contenuto e di 2 pixel attorno al titolo. Tale padding non è impostabile in alcun modo, ma per stessa ammissione del team di ASP.NET 2.0 è hard-coded all'interno del framework.

Esistono un paio di workaround suggeriti da Microsoft in ladybug, ma la loro applicazione è del tutto limitata e difficoltosa. L'unico modo veramente valido che ho trovato - suggerito da un utente - per superare questo problema è lo sfruttare la natura dei css. Chiunque usi i css sa che in cascata vengono applicati prima i fogli di style .css, poi il contenuto di <style> e infine il valore dell'attributo style di ogni tag con il risultato che quest'ultimo ha la precedenza sugli altri e risulta insuperabile. Detto questo è evidente che l'attributo style="padding:5px" che si trova sul <td> che incapsula il contenuto della webpart è pressochè impossibile da aggirare.

Esiste però una clausola nei fogli di stile che consente di sovrascrivere questo comportamento di default. Si tratta dello switch !important che se fatto seguire ad uno stile fa si che esso diventi preponderante rispetto a quelli di livello superiore.

.webPart
{
   padding: 0px !important;
}

Questo breve pezzetto di stile risolverà definitivamente il problema. Speriamo però che Microsoft ci metta una pezza al più presto.

powered by IMHO 1.3


Le nuove capacità di Profiling di ASP.NET 2.0 sono una feature molto apprezzabile anche in considerazione del fatto che sono in grado di gestire anche i profili anonimi, cioè consentono di persistere le impostazioni di profilazione anche per gli utenti che ancora non sono autenticati. Questo ad esempio torna utile nel momento in cui è necessario consentire la selezione della lingua anche ad un navigatore che non si è ancora registrato. Esiste però un piccolo problema per capire il quale vi farò un semplice esempio: immaginate di utilizzare il profilo anonimo appunto per persistere la lingua selezionata in un portale. Ora è evidente che qualora un utente anonimo imposti la lingua e in seguito si autentichi al portale, l'impostazione selezionata dovrà essere trasportata verso il profile autenticato. Per compiere questa migrazione esiste un evento apposito, gestibile nel global.asax in questo modo:

public void Profile_OnMigrateAnonymous(
    
object sender, 
    ProfileMigrateEventArgs args)
{
    ProfileCommon anonymousProfile = 
        Profile.GetProfile(args.AnonymousID);
        
    Profile.Culture = anonymousProfile.Culture;
    
    ProfileManager.DeleteProfile(args.AnonymousID);
    AnonymousIdentificationModule.ClearAnonymousIdentifier(); 
}

L'esempio è reperibile anche nella documentazione MSDN ma non è del tutto evidente.

powered by IMHO 1.3


Quest'oggi mi sono scontrato con un problema che pareva di banale soluzione ma che si è rivelato essere decisamente subdolo. L'obbiettivo da raggiungere era di caricare le webparts di una pagina dinamicamente da un database invece che inserirle nel markup come di consueto. A prima vista può sembrare che il framework offra quanto necessario per compiere questo compito. Il WebPartManager ad esempio espone il metodo AddWebPart() che sembra fatto a posta per quello, ma dopo un po' di tentativi si intuisce che c'è qualcosa che non va.

Il problema non è che il metodo in questione non funzioni, ma anzi, che funziona troppo. Infatti il difetto che si evidenzia quasi subito è che ad ogni postback le webpart vengono nuovamente aggiunte causando una crescita smisurata e inarrestabile. Qualunque tentativo di gestire questo comportamento infausto non ha i risultati sperati. A partire dal consueto "IsPostBack", fino alla verifica controllo per controllo che non si stiano inserendo duplicati tutti i tentativi falliscono meramente.

Il fatto è che, quando si aggiungono le webpart queste vengono persistite nel database di personalizzazioni. Perciò la volta successiva che la pagina viene costruita le personalizzazioni salvate causano la duplicazione delle webpart. Naturalmente a questopunto verrà persistita anche la duplicazione, con l'ovvio risultato che la volta successiva ci si ritrova con una triplicazione... e così via.

L'unica soluzione a questo problema l'ho trovata spulciando tra le sessioni della PDC'05. Il trucco sta nel creare un WebPartManager derivato da quello consueto e gestire al suo interno l'aggiunta delle webparts dinamiche nell'OnInit della pagina. In questo modo sarà possibile sfruttare la classe WebPartManagerInternals esposta per mezzo della proprietà Internals per settare le proprietà delle webpart aggiunte e farle per così dire "rientrare" nel normale ciclo di vita delle webpart.

Ecco un breve spezzone di codice:

// metodo chiamato nel Page.OnInit()
private void AddExternalWebParts()
{
    
// legge le webpart dallo storage
     
List<WebPartData> parts = 
         WebPartProvider.GetWebParts(
this.Page);

    
// cicla le webpart
    
foreach (WebPartData data in parts) 
    {
        
// istanzia lo usercontrol
        
Control uc = 
            
this.Page.LoadControl(data.AscxVirtualPath);
        
        
// crea la webPart che incapsula lo usercontrol
        
GenericWebPart webPart = 
            WebPartManager.CreateWebPart(uc);

        
// setta le proprietà        
        
Internals.SetZoneID(webPart, data.ZoneID);
        Internals.SetZoneIndex(webPart, data.ZoneIndex);
        Internals.SetIsShared(webPart, 
true);

        
// Aggiunge una WebPart statica
        
Internals.AddWebPart(webPart);
    }
}

Nell'esempio che ho linkato, nel folder ExternalWebPart è presente anche il codice per creare le connessioni tra webpart automaticamente. Ma questa è un'altra storia.

powered by IMHO 1.3


L'esperienza con le WebParts di questi giorni è stata l'occasione per scrivere un breve articolo per UgiDotNet che unitamente ad un utile Tip per aggirare un comportamento che potrebbe sembrare anomalo, descrive il meccanismo per cui ogni WebControl o UserControl può diventare una WebPart.

Buona lettura a tutti.

Link: http://www.ugidotnet.org/articles/articles_read.aspx?ID=108

powered by IMHO 1.3


Il web.config di ASP.NET 2.0 si sta rivelando una vera e propria miniera. Stamane, per puro caso ho scoperto un'altra chicca che mi accingo a proporvi. Celata, nei meandri del file di configurazione si trova una nuova sezione tagMapping che consente di sostituire tag all'interno di un Templated Control quale può essere una pagina, uno usercontrol o un qualunque controllo che esponga un template.

Per chiarire il concetto provate a pensare di aver utilizzato un WebControl all'interno di una vostra applicazione. Dopo che avete effettuato il deploy dell'applicazione in produzione vi rendete conto che una versione successiva dello stesso controllo utilizzata nell'applicazione gli darebbe una serie di feature importanti. Senza dover modificare il codice sarà sufficiente andare nel web.config e scrivere la seguente linea:

<pages>
    <tagMapping>
        <add 
            
mappedTagType="MyApp.Controls.NewControl" 
            
tagType="MyApp.Controls.OldControl"/>
    <
/tagMapping>
<
/pages>

Come risultato otterrete che l'applicazione istanzierà il nuovo webcontrol al posto di quello vecchio. La sostituzione del controllo avviene durante la fase di parsing del template per cui non introduce alcun overhead al momento dell'esecuzione.
Unico limite di questa tecnica è che il controllo da sostituire deve derivare da quello di partenza. In effetti più che comprensibile.

powered by IMHO 1.3


Se create un WebControl e poi decidete di applicare ad esso l'utilizzo dei temi è necessario avere un particolare accorgimento per le proprietà che questo espone se debbono contenere un url relativo alla root del tema.

Ad esempio, se create una proprietà del WebControl, che espone l'url di una immagine che utilizzate per il rendering (ButtonImageUrl) e poi impostate questa proprietà nel file .skin vi renderete immediatamente conto che l'url che viene passato a runtime al WebControl è relativo la root dell'applicazione e non a quella del tema. Per ottenere la trasformazione relativa al tema occorre applicare alla property un attributo di tipo UrlProperty:

[UrlProperty]
public string ButtonImageUrl
{
    
get return this.buttonImageUrl;  }
    
set this.buttonImageUrl = value; }
}

Interessante notare che tale attributo dispone anche di una proprietà Filter, che consente di discriminare il tipo di file che devono essere soggetti alla trasformazione. Ad esempio se scrivere l'attributo come segue, solo le immagini gif saranno ricalcolate sulla base della root del tema.

[UrlProperty(Filter="*.gif")]

powered by IMHO 1.3


Se provare a creare uno UserControl con ASP.NET 2.0 e poi tentate di registrarlo nel file di skin e applicando ad esso uno SkinId di settare alcune proprietà, vi troverete di fronte al seguente errore che ieri mi ha fatto perdere ben mezza giornata di lavoro:

Error 1 The control type 'ASP.MyControl_ascx' cannot be themed. C:\DEV\Test\Portal\App_Themes\my_theme\my_theme.skin 42 

Per risolvere questo problema occorre applicare un attributo alla classe che rappresenta lo UserControl in questo modo:

[Themeable(true)]
public partial class MyControl : UserControl
{
}

Chissa poi perchè di default gli UserControl non dovrebbero essere skinnabili? Ma questo è un altro paio di maniche...

powered by IMHO 1.3


Curiosamente stamane Simone mi ha preceduto di un soffio nel parlarvi di Url Rewriting. In questi giorni infatti mi sono trovato nella necessità di soddisfare una particolare esigenza di rewriting che mi ha fatto approfondire l'argomento e avevo in mente di scrivere questo post da un po'. Preso atto che la scelta di Simone è ricaduta sull'implementazine di un IHttpModule, è evidente che il mio post non è una ripetizione perchè in realtà il metodo che ho usato è basato su una tecnica che nell'articolo di MSDN che Simone ha citato viene appena sfiorata. Eccomi quindi a proporvi il metodo che ho recentemente utilizzato per il rewriting dell'url in una applicazione che vedeva le proprie pagine riportate parzialmente nel database e parzialmente sul filesystem.

La mia implementazione di rewrite è basata sulla creazione di un HttpHandlerFactory. Se consideriamo un HttpModule come un filtro che intercetta tutte le chiamate alle pagine di una applicazione, e un HttpHandler un gestore per particolari tipi di chiamate che si sostituisce a quello delle normali pagine, la HttpHandlerFactory invece è una classe che viene usata dal runtime per istanziare un HttpHandler specifico in base alla chiamata che esso riceve. Implementando l'interfaccia IHttpHandlerFactory, in sostanza ci si pone nella HttpPipeline al livello più alto possibile e si è in grado di decidere quale handler istanziare in base all'url che si riceve ed in seguito passare ad esso la chiamata.

Questa condizione nel mio caso era davvero esemplificativa. Si trattava infatti di effettuare una semplice query nel database - query che con l'implementazione di un meccanismo di caching è diventata unica in tutta la vita dell'applicazione - e in base al risultato decidere se istanziare l'handler delle pagine aspx normalmente oppure variando leggermente l'input per ottenere oltre al rewrite, il caricamento di una specifica pagina in base a quanto specificato nel database. Vedo di spiegarmi meglio; tanto per cominciare vediamo come fa il runtime ad istanziare l'handler per le pagine aspx.

IhttpHandler page = PageParser.GetCompiledPageInstance(url, pathTranslated, context);

Per mezzo di questa semplice riga, è possibile indicare al runtime di produrre una istanza di IHttpHandler del tutto uguale a quella che produce esso stesso in condizioni normali. I parametri indicano il path virtuale, il path fisico della pagina e l'HttpContext della chiamata. Ecco quindi che in realtà utilizzando tale metodo si può "imbrogliare" il runtime facendo caricare ad esso un file che si trovi in un percorso diverso da quello reale che esso ha calcolato sulla base della chiamata. Il trucco per fare il rewriting in questo caso è di avere una singola pagina "dbpage.aspx", nella root dell'applicazione cui redirigere tutte le chiamate che fanno riferimento al database e - nel mio caso - rifilare al runtime una pagina "fisica" scelta fra una rosa di possibili layout per mezzo di un campo nel database stesso. Ecco il codice:

public IHttpHandler GetHandler(HttpContext context, string requestType, string url, string pathTranslated)
{
    PortalPage page = 
this.Indexer.FindPage(url);

    
if (page != null)
    {
        context.Items["CurrentPage"] = page;
        
return PageParser.GetCompiledPageInstance("~/dbpage.aspx", this.MapLayout(page.Layout), context);
    }

    
return PageParser.GetCompiledPageInstance(url, pathTranslated, context);
}

public string MapLayout(string layoutName)
{
    
return string.Format("~/layouts/{0}.aspx", layoutName);
}

Il codice parla da solo, ma vediamo lo stesso di spendere due parole. La prima riga reperisce l'istanza della PortalPage da un oggetto tenuto in cache, che rappresenta la struttura caricata dal database una volta per tutte. A questo punto, se la pagina viene trovata essa viene inserita nel contesto della chiamata e poi viene effettuato il rewrite modificando l'url in ~/dbpage.aspx e passando come path fisico una diversa pagina aspx - che per inciso contiene una serie di WebPartZones in un ContentPlaceHolder. Nel caso in cui la pagina non esista nel database, i parametri vengono passati così come sono ricevuti al PageParser che quindi costruirà l'IHttpHandler consueto.

Risultato di questo giochetto è che parte delle pagine dell'applicazione sono definite in una tabella del database e l'utente potrà navigare tale struttura come se fosse fusa con quella sul filesystem stesso.

Non mi rimane di segnalarvi che questa tecnica è utilizzabile anche con il framework 1.1, praticamente senza alcuna modifica.

powered by IMHO 1.3


Prosegue con questo post la saga, iniziata quando ho parlato degli ExpressionBuilders, dedicata alle risorse tipicamente utilizzate nelle applicazioni ASP.NET per depositare stringhe, e in genere proprietà da utilizzare nel rendering dell'interfaccia. In quel post spiegavo come realizzare un ExpressionBuilder che permettesse facilmente dei leggere le risorse da un database anziche da un più consueto file resx. Esiste tuttavia un altro metodo, per fare la medesima cosa, che in realtà è quello che il framework indica come quello corretto. Chi avesse provato a realizzare l'ExpressionBuilder forse si sarà reso conto che pur funzionando egregiamente esso ha delle limitazioni fastidiose. Giusto per indicarne una per tutte, quella che a me ha fatto approfondire ulteriormente l'argomento, qualora vi trovaste a localizzare un SiteMap ben presto vi renderete conto che in questo l'ExpressionBuilder non vi è di alcun aiuto. Il motivo è presto detto: la funzione degli ExpressionBuilder non è quella di leggere risorse, ma semplicemente quello che tipicamente viene usato per questa operazione, il ResourceExpressionBuilder è solo una minima sfaccettatura all'interno dell'architettura del framework dedicata all'argomento.

Se come ho fatto io imbracciate il vostro reflector, e lo puntate sulla classe System.Web.SiteMapNode, vi accorgerete che la proprietà Title (una di quelle che è passibile di localizzazione), racchiude al suo interno molta più logica di quella che ragionevolmente ci si potrebbe aspettare. Ecco un estratto da Reflector:

public virtual string Title
{
  
get
  
{
    
if (this._provider.EnableLocalization)
    {
      
string text1 = this.GetImplicitResourceString("title");
                 
      
if (text1 != null)
        
return text1;

      text1 = 
this.GetExplicitResourceString("title", this._title, true);

      
if (text1 != null)
        
return text1;
    }

    
if (this._title != null)
        
return this._title;

    
return string.Empty;
  }
}

Procedendo ulteriormente nell'esplorazione in breve si scoprira che il codice porta ad istanziare il medesimo ResourceExpressionBuilder e quindi in definitiva tentare di localizzare una proprietà della SiteMap corrisponde ad usare l'ExpressionBuilder "Resources". Di primo acchito questo mi è parso davvero strano. L'idea che mi ero fatto era che a quel punto si potessero usare esclusivamente i consueti file resx per localizzare una SiteMap.

In realtà nel framework esiste una interfaccia IResourceProvider, che è il punto giusto cui agganciarsi per spostare la fonte delle risorse dai normali file al database. Un po' celato all'interno del DOM del web.config si trova un attributo dell'elemento <globalization> denominato resourceProviderFactoryType. In tale attributo si dovrà immettere il tipo, derivato da ResourceProviderFactory, che ha il compito di creare il ResourceProvider deputato a recuperare le risorse. Mi rendo conto che detto così pare un po' complesso, ma questo meccanismo è molto efficace perchè consente di avere diversi ResourceProvider che lavorano assieme, istanziati alla bisogna in base al tipo di risorse che si desidera prelevare. In particolare il ResourceProviderFactory dispone di due metodi, uno per le risorse cosidette "locali" e uno per quelle "globali". Per comprendere la distinzione si dovra pensare alle risorse locali come quelle dedicate ad una singola pagina, infatti il relativo metodo richiede in ingresso il path della pagina, mentre per globali si intendono quello usate da tutte le pagine dell'applicazione. In quest'ultimo caso il metodo richiede una "classKey" cioè un qualificatore che permette di distinguere dei gruppi di risorse.

Non è finita però. Dopo aver implementato il ResourceProviderFactory dovremmo procedere all'implementazione di IResourceProvider che richiede la realizzazione di due metodi. Il primo per la lettura di una singola risorsa, e il secondo che restituisce un IResourceReader, che serve al runtime per enumerare le risorse presenti. La classe che implementa IResourceReader sarà lanostra terza ed ultima fatica. Ecco di seguito il codice di entrambe le classi:

public class SqlResourceProvider : IResourceProvider
{
    
public object GetObject(string key, CultureInfo culture)
    {
        
// leggi una singola risorsa 
        return
GetResourceFromDB(key, culture);
    }

    
public System.Resources.IResourceReader ResourceReader
    {
        
get
        
{
            
// leggi un set di risorse
            
return new SqlResourceReader(
                GetResourcesFromDB(CultureInfo.CurrentUICulture));
        }
    }
}
        
public class SqlResourceReader : IResourceReader
{
    
private Dictionary<stringobject> resources;

    
public SqlResourceReader(Dictionary<stringobject> resources)
    {
        
this.resources = resources;
    }

    
public IDictionaryEnumerator GetEnumerator()
    {
        
return resources.GetEnumerator();
    }

    
public void Close()
    { }
        
    
public IEnumerator IEnumerable.GetEnumerator()
    {
        
return resources.GetEnumerator();
    }

    
public void Dispose()
    {
        Close();
    }
}

L'istanza del ResourceProvider andrà creata nel ResourceProviderFactory che registreremo in configurazione. In questo modo avremo interposto il nostro provider a tutti i tentativi di lettura di risorse di localizzazione effettuati da parte dei componenti del framework, ed in questo modo potremmo facilmente localizzarli. Anche la SiteMap di cui ho detto poco fa.
 

powered by IMHO 1.3


Chi volesse creare un provider custom per sitemap, non avrà che da implementare i due metodi astratti della classe StaticSiteMapProvider, da cui già eredita la consueta XmlSiteMapProvider. In questo articoletto di Jeff Prosise, in breve viene spiegato come fare, con un piccolo esempio di codice nel quale viene realizzato un SiteMapProvider per Access.

Link: http://msdn.microsoft.com/msdnmag/issues/05/06/WickedCode/