Crad's .NET Blog

L'UGIblog di Marco De Sanctis
posts - 190, comments - 457, trackbacks - 70

Ancora su custom attributes e validazione

In un post precedente ho iniziato un discorso su come utilizzare le potenzialità dei custom attributes per validare le nostre entity. Una volta capito il meccanismo, è estrememente semplice implementare tutta una serie di classi che permettano le più disparate tipologie di validazioni, magari anche combinandole tra di loro.

Il nostro nuovo ErrorMessageAttribute

Ma se la validazione fallisce come dobbiamo comportarci? Beh, l'ideale a questo punto sarebbe sollevare un'eccezione, magari potendo fornire un messaggio d'errore specifico per il membro che non è stato correttamente valorizzato. Siccome ci stiamo appassionando tantissimo alla programmazione dichiarativa , realizziamo un bell'attributo simile a questo (semplicissimo) che segue:

public class ErrorMessageAttribute: Attribute
{
    
private string errorMessage;
    
public string ErrorMessage
    {
        
get return errorMessage; }
    }
    
    
public ErrorMessageAttribute(string message)
    {
        
this.errorMessage = message;
    }
}

In questo modo possiamo scrivere qualcosa come:

public class AdultPerson: Person
{
    
//... more code ....
    
private int age;
    
    [MinValue(18), ErrorMessage("E' necessario che l'individuo sia maggiorenne!")]
    
public int Age
    {
        
get return age; }
        
set { age = value; }
    }
    
    
//... more code ....
}

e con una minima modifica al metodo di validazione, che non inserisco qui per non dilungarmi ulteriormente ma che è abbastanza banale (altrimenti chiedete pure lumi), si può recuperare il messaggio di errore da questo attributo. Benissimo, ma se volessimo supportare anche la localization in diverse lingue?

Facciamo diventare ErrorMessageAttribute anche localizzabile

Beh, intanto iniziamo con una premessa: quanto scritto poc'anzi non è una pratica correttissima, dato che le stringhe andrebbero sempre memorizzate in un resource file e richiamate tramite un resource manager (FxCop si arrabbia tantissimo per questo ). Allo scopo sono solito implementare, in ogni assembly che lo richieda, una classe Singleton che instanzia un ResourceManager e restituisce le stringhe tramite un metodo statico che legge il file di risorse della culture corrente (ah, dimenticavo, questo è esattamente quanto accade nelle classi del .NET Framework, parola di Reflector). Un piccolo snippet per spiegare quanto detto:

internal sealed class Res
{
    
private ResourceManager resource;      
    
private Res()
    { 
        
this.resource =
          
new ResourceManager("ResourcesName", Assembly.GetExecutingAssembly());   
    }   
        
    
// Singleton semplificato. Attenzione, NON E' THREAD-SAFE
    
private static Res _defaultInstance;
    
private static Res defaultInstance
    {
        
get
        
{
            
if (_defaultInstance == null)
              _defaultInstance = 
new Res();
            
return _defaultInstance;
        }
    }
    
    
// questo è il metodo che recupera la risorsa string
    
public static string GetString(string name)
    {
        Res instance = Res.getDefaultInstance();
        
        
return instance.resource.GetString(name);
    }
}

Con i custom attributes, però, la faccenda si complica un po', dato che i parametri che possiamo fornire ai costruttori possono essere solo costanti, type, o array degli stessi. In pratica il codice seguente

[MinValue(18), ErrorMessage(Res.GetString("InvalidAge"))]
public int Age
// ....

non compila. Una possibile soluzione, allora, è creare un nuovo costruttore di ErrorMessageAttribute a cui passare il type del nostro Res e il nome della risorsa string da cercare:

public ErrorMessageAttribute(Type staticResourceManager, string resourceName)
{
    MethodInfo method = staticResourceManager
        .GetMethod("GetString", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
    
    
if (method != null)
        
this.errorMessage = (string)
            method.Invoke(
nullnew object[] { resourceName });
}

Tramite Reflection, il costruttore ispeziona il tipo passato come parametro e cerca un metodo statico di nome GetString simile a quello che abbiamo scritto in precedenza. Per utilizzarlo basta scrivere

[MinValue(18), ErrorMessage(typeof(Res), "InvalidAge")]
public int Age
// ....

e i nostri messaggi d'errore divengono automaticamente localizzabili, a patto di creare un file di risorse per ogni culture che vogliamo supportare.  L'unico lato negativo di questa implementazione (e purtroppo non di poco conto) è che la chiamata a Res non è strong-typed e se commettiamo qualche errore, ad esempio rinominando GetString in GetStringa, ce ne accorgiamo solo a run-time. Sinceramente non mi è venuta in mente una soluzione migliore, quindi sono aperto a tutti i consigli possibili!

Ciao!

powered by IMHO 1.3

Print | posted on domenica 12 marzo 2006 03:23 | Filed Under [ .Net 2.0 ]

Powered by:
Powered By Subtext Powered By ASP.NET