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(null, new 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