Sabato sono stato alla IV UGI.Alt Conf tenutasi a Bologna, è stata un’esperiamza decisamente interessante e per la quale non posso far altro che ringraziare tutta l’organizzazione.
Dovevo tenere una sessione, come da agenda, ma alla fine ne ho tenute 2, la seconda (prima in ordine di apparizione) è stata una sessione OpenSpace su Model-View-ViewModel. E’ stata la prima volta come moderatore di OpenSpace e devo dire che è stata un’esperienza interessante di cui magari parlerò in futuro.
Alla fine sono riuscito a seguire una sola sessione per intero, quella su Mono tenuta da Massimiliano Mantione, sessione che avrei voluto seguire già al workshop UGI a Predappio ma che era in parallelo proprio con la mia. La sessione di Massi, imho, è stata perfetta. Il “ragazzo” poi è l’equilibrio fatto a persona, ha una capicità di giudizio scevra da pregiudizi (almeno in ambito informatico) degna di invidia, complimenti.
Sono inoltre decisamente lusingato, e in quanto decisamente orso anche un po’ “intimorito”, dai complimenti che ho letto riguardo le mie sessioni, semplicemente grazie!, è un ottimo stimolo per fare sempre meglio.
Infine, ma non per questo meno importante, anzi, un accenno alla serata pre-conf magnificamente piacevole grazie alla solita inpeccabile ospitalità di casa Scardova, quindi grazie Ale e Lucy!
Carnazza rulez!!! vero Matteo?
.m
P.S. il post contiene un solo link perchè sono in albergo, su gprs…, e la navigazione è semplicemente un delirio… mi perdonate? ;-)
The journey is the most important thing, not the destination. Find your next destination and start travelling again.
Monday, June 29, 2009
Thursday, June 25, 2009
Non sono un test addicted, ma…
Non sono mai stato un “test addicted” nel senso che non scrivo i test per tutto, scrivo i test per molte cose (ed in alcuni casi tendo maniacalmente ad un 100% di Code Coverage), alcune le faccio in TDD, e per altre ritengo semplicemente inutile scrivere test, se non a fronte di un bug dove allora prima scrivo il test, e se assenti, un set di test che mi parino da eventuali regressioni, che lo riproduce poi procedo con la fix.
Credo di avere una minima esperienza da garantirmi un buon deisgn anche se non è emergente, ne è riprova il fatto che tutte le volte che ho dovuto scrivere test su qualcosa di già disegnato non mi sono mai pentito del design; in questo senso c’è anche da dire che l’uso spinto di un tool per Inversion of Control obbliga ad un design di un certo tipo.
Resta il fatto che ci sono momenti, per tutto c’è sempre mastercard sia chiaro, in cui la scrittura di un paio di test toglie dalle grane mostruosamente più rapidamente dell’F5. Sto lavorando ad una cosa in cui la base dati sta in parte dietro un set di servizi quindi per poter “mixare” questi dati con quelli provenienti dal db “locale” mi sono dovuto parzialemnte reinventare la ruota (non ho trovato di meglio e sono aperto a suggerimenti) e mascherare completamente l’ORM dietro una mia “Unit of Work” che sia in grado di fare il dispatch delle chiamate “a destra e a manca” in base al tipo di entità che state chiedendo, simulando ove possibile anche le “join” e altri giochetti. Detto fatto.
Oggi pomeriggio mi sono dedicato alla gestione della IdentityMap e in questo caso gli Unit Test sono una vera manna dal cielo perchè vi permettono di simulare quello che volete senza fare il benchè minimo sforzo:
.m
Credo di avere una minima esperienza da garantirmi un buon deisgn anche se non è emergente, ne è riprova il fatto che tutte le volte che ho dovuto scrivere test su qualcosa di già disegnato non mi sono mai pentito del design; in questo senso c’è anche da dire che l’uso spinto di un tool per Inversion of Control obbliga ad un design di un certo tipo.
Resta il fatto che ci sono momenti, per tutto c’è sempre mastercard sia chiaro, in cui la scrittura di un paio di test toglie dalle grane mostruosamente più rapidamente dell’F5. Sto lavorando ad una cosa in cui la base dati sta in parte dietro un set di servizi quindi per poter “mixare” questi dati con quelli provenienti dal db “locale” mi sono dovuto parzialemnte reinventare la ruota (non ho trovato di meglio e sono aperto a suggerimenti) e mascherare completamente l’ORM dietro una mia “Unit of Work” che sia in grado di fare il dispatch delle chiamate “a destra e a manca” in base al tipo di entità che state chiedendo, simulando ove possibile anche le “join” e altri giochetti. Detto fatto.
Oggi pomeriggio mi sono dedicato alla gestione della IdentityMap e in questo caso gli Unit Test sono una vera manna dal cielo perchè vi permettono di simulare quello che volete senza fare il benchè minimo sforzo:
Altrimenti l’unica soluzione plausibile è una tonnellata di F5 (che fatti su una solution con 68 progetti non è certo una passeggiata ;-)) uniti a tanta fantasia per capire se nel complesso di quello che l’applicazione sta facendo il vostro componente si sta comportando nella maniera attesa… che equivale ad una soluzione non plausibile ;-)[TestMethod] public void dataContext_getByKey_using_identityMap_should_return_the_same_instance() { var paf = MockRepository.GenerateStub<IPersistanceActionFactory>(); var rp = MockRepository.GenerateMock<IRepositoryProvider>(); rp.Expect( obj => obj.Resolve<IRepository<StubEntity>>() ) .Return( new StubEntityRepository() ) .Repeat.Once(); var dc = new DataContext( new IdentityMap(), rp, paf ); var a = dc.GetByKey<StubEntity>( new Key<Int32>() ); var b = dc.GetByKey<StubEntity>( new Key<Int32>() ); a.ShouldBeEqualTo( b ); rp.VerifyAllExpectations(); } [TestMethod] public void dataContext_getByKey_then_getAll_using_identityMap_should_return_the_same_instance() { var paf = MockRepository.GenerateStub<IPersistanceActionFactory>(); var rp = MockRepository.GenerateMock<IRepositoryProvider>(); rp.Expect( obj => obj.Resolve<IRepository<StubEntity>>() ) .Return( new StubEntityRepository() ) .Repeat.Once(); var dc = new DataContext( new IdentityMap(), rp, paf ); var a = dc.GetByKey<StubEntity>( new Key<Int32>() ); var list = dc.GetAll<StubEntity>(); a.ShouldBeEqualTo( list.ElementAt( 0 ) ); rp.VerifyAllExpectations(); } [TestMethod] public void dataContext_getAll_then_getByKey_using_identityMap_should_return_the_same_instance() { var paf = MockRepository.GenerateStub<IPersistanceActionFactory>(); var rp = MockRepository.GenerateMock<IRepositoryProvider>(); rp.Expect( obj => obj.Resolve<IRepository<StubEntity>>() ) .Return( new StubEntityRepository() ) .Repeat.Once(); var dc = new DataContext( new IdentityMap(), rp, paf ); var list = dc.GetAll<StubEntity>(); var a = dc.GetByKey<StubEntity>( new Key<Int32>() ); list.ElementAt( 0 ).ShouldBeEqualTo( a ); rp.VerifyAllExpectations(); }
.m
Labels:
Software Mason
Friday, June 19, 2009
db4o: update
Qualche giorno fa ho parlato di db4o dal punto di vista dello sviluppatore, devo dire che nell’insieme ho trovato molto interessante il tutto e sto seguendo il feed del team di sviluppo.
Ci sono interessanti novità per quel che riguarda il supporto a linq/IQueryable il che non è niente male, inoltre con la versione 7.10, ancora in beta, viene distribuito gratuitamente anche il db4o ObjectManager (una sorta di Enterprise Manager per db4o) la cosa interessante è che questa versione:
Inoltre spulciano i vari post ho capito qualcosa in più, nel mio primo post ho parlato delle varie modalità di interrogazione (query) supportate da db4o, definendo la Native Query (NQ) come la modalità nativa, come si evince dalla documentazione allegata, in realtà le cose non sono proprio così:
Da quel che posso intuire dai post che ho letto internamente il motore di db4o utilizza SODA e non NQ, il nuovo supporto per IQueryable infatti adesso trasforma quasi tutto l’expression tree in query SODA, facendo solo in rari casi, per gli operatori non supportati, il fallback a NQ che sostanzialmente si traduce in Linq2Objects che è sempre meglio che una exception ;-).
.m
Ci sono interessanti novità per quel che riguarda il supporto a linq/IQueryable
- ha funzionato al primo colpo, a differenza del predecessore;
- è un add-in che si integra molto bene in Visual Studio;
Inoltre spulciano i vari post ho capito qualcosa in più, nel mio primo post ho parlato delle varie modalità di interrogazione (query) supportate da db4o, definendo la Native Query (NQ) come la modalità nativa, come si evince dalla documentazione allegata, in realtà le cose non sono proprio così:
Da quel che posso intuire dai post che ho letto internamente il motore di db4o utilizza SODA e non NQ, il nuovo supporto per IQueryable
.m
Thursday, June 18, 2009
*DataContext.Log –> log4net…
I vari flavour del DataContext offrono la possibilità di agganciare un “logger” per vedere ad esempio le query prodotte, ad esempio con Linq2Sql avete una cosa del tipo:
Adesso supponiamo che vogliate scrivere quel log su un vostro sistema di Log, sia esso log4net, come nel mio caso, o il motore di Tracing di .net, poco importa quale sia la destinazione; in tutti i casi l’inghippo è che da una parte avete un TextWriter mentre dall’altra avete un metodo da chiamare tipicamente passando un messaggio sottoforma di stringa.
Per log4net ho trovato questa soluzione abbastanza complessa, a mio modo di vedere, che per inciso non funziona… ;-) o meglio se usate log4net in maniera basilare si, ma se cominciate a fare cose un po’ strane allora ciccia, non da errori ma non scrive nulla. All’inizio ci ho ragionato sopra un po’ cercando di capire quale fosse il problema… poi “blink” :-)
che gli passate nel costruttore.
Per usarlo quindi, nel mio caso con log4net, mi limito a fare:
.m
Questo, se avete un debugger attaccato, o siete in un’applicazione Console, scrive sulla Console (Output Window per il debugger) i messaggi di Log del DataContext, tra cui tutte le query T-Sql con i parametri etc etc…using( var dc = new MyDataContext() ) { dc.Log = Console.Out;
}
Adesso supponiamo che vogliate scrivere quel log su un vostro sistema di Log, sia esso log4net, come nel mio caso, o il motore di Tracing di .net, poco importa quale sia la destinazione; in tutti i casi l’inghippo è che da una parte avete un TextWriter mentre dall’altra avete un metodo da chiamare tipicamente passando un messaggio sottoforma di stringa.
Per log4net ho trovato questa soluzione abbastanza complessa, a mio modo di vedere, che per inciso non funziona… ;-) o meglio se usate log4net in maniera basilare si, ma se cominciate a fare cose un po’ strane allora ciccia, non da errori ma non scrive nulla. All’inizio ci ho ragionato sopra un po’ cercando di capire quale fosse il problema… poi “blink” :-)
ActionTextWriter, di una semplicità disarmante: altro non fa che intercettare le chiamate a “Write” del TextWriter e redirigerle sul delegato di tipo Actionpublic sealed class ActionTextWriter : TextWriter { readonly Action<String> logger; public ActionTextWriter( Action<String> logger ) { Ensure.That( logger ).Named( "logger" ).IsNotNull(); this.logger = logger; } public override void Write( string value ) { this.logger( value ); } public override void Write( char[] buffer, int index, int count ) { if( buffer == null || index < 0 || count < 0 || buffer.Length - index < count ) { //let base class to throw exception base.Write( buffer, index, count ); } this.logger( new String( buffer, index, count ) ); } Encoding _encoding; public override Encoding Encoding { get { if( this._encoding == null ) { this._encoding = new UnicodeEncoding( false, false ); } return this._encoding; } } }
Per usarlo quindi, nel mio caso con log4net, mi limito a fare:
Semplice, funzionale e senza seg*e mentali di sorta ;-)static readonly ILog logger = LogManager.GetLogger( typeof( MyRepository ) );using( var dc = new MyDataContext( this.connectionString ) ) { dc.Log = new ActionTextWriter( s => logger.Debug( s ) ); }
.m
Labels:
Software Mason
Tuesday, June 16, 2009
Log4PostSharp: note di redazione
Ieri ho parlato di Log4PostSharp come ottimo compagno di viaggio per la scrittura del tracing. Un paio di note per districarsi meglio nella giungla.
Magagna…
Se scaricate Log4PostSharp e cercate di usarlo non va, vi beccate un simpatico errore di compilazione… per qualche strano motivo l’autore ha compilato i suoi assembly utilizzando una reference ad una versione specifica di PostSharp, la 1.0, mentre tipicamente voi potreste avere la 1.0sp1. Nulla di grave:
Non fatevi prendere dalla tentazione di ricompilare il tutto per PostSharp 1.5 RC1 perchè compilare compila ma poi non va un bel nulla… causa cambi radicali nel modello di reflection utilizzato da PostSharp. PostSharp infatti utilizza un suo sistema di Reflection al fine di evitare di caricare l’assembly che sta analizzando.
Tip & Tricks
Una necessità a cui potreste andare incontro è quella di avere accesso allo stesso “logger” che state usando con Log4PostSharp ad esempio perchè durante l’esecuzione di un metodo avete bisogno di loggare informazioni in più.
Nulla di più facile, anche grazie a come funziona log4net, avete sempre il vostro bel metodo:
Quello che potete fare è pressochè la stessa cosa che fa Log4PostSharp:
.m
Magagna…
Se scaricate Log4PostSharp e cercate di usarlo non va, vi beccate un simpatico errore di compilazione… per qualche strano motivo l’autore ha compilato i suoi assembly utilizzando una reference ad una versione specifica di PostSharp, la 1.0, mentre tipicamente voi potreste avere la 1.0sp1. Nulla di grave:
- Scaricatevi i sorgenti dal repository svn di Google (io uso RapidSVN)
- Aprite la solution;
- Rimuovete le reference a PostSharp.*.dll;
- Rimettetele con quelle che avete voi, di default Visual Studio mette Specific Version “false”;
- Ricompilate e va tutto che è un piacere;
Non fatevi prendere dalla tentazione di ricompilare il tutto per PostSharp 1.5 RC1 perchè compilare compila ma poi non va un bel nulla… causa cambi radicali nel modello di reflection utilizzato da PostSharp. PostSharp infatti utilizza un suo sistema di Reflection al fine di evitare di caricare l’assembly che sta analizzando.
Tip & Tricks
Una necessità a cui potreste andare incontro è quella di avere accesso allo stesso “logger” che state usando con Log4PostSharp ad esempio perchè durante l’esecuzione di un metodo avete bisogno di loggare informazioni in più.
Nulla di più facile, anche grazie a come funziona log4net, avete sempre il vostro bel metodo:
e il prodotto della compilazione è:static void Main( string[] args ) { var p = new Person(); p.FirstName = "Mauro"; p.LastName = "Servienti"; var x = p.Echo( p ); Console.WriteLine( x ); Console.Read(); }
Quello che potete fare è pressochè la stessa cosa che fa Log4PostSharp:
Recuperate una reference allo stesso logger utilizzato da Log4PostSharp, con il risultato atteso:var logger = LogManager.GetLogger( typeof( Program ) ); var p = new Person(); p.FirstName = "Mauro"; p.LastName = "Servienti"; logger.Debug( "My message..." ); var x = p.Echo( p ); Console.WriteLine( x ); Console.Read();
.m
Monday, June 15, 2009
Tracing… che barba che noia :-) <cit.>
Un paio di assiomi ;-):
Introducing AOP with PostSharp
Il tracing è uno di quegli aspetti della programmazione in cui AOP potrebbe fare da padrone, alla fine della fiera quel codice è generalizzabilissimo e varrebbe proprio la pena di farlo.
Ho già parlato più di una volta di PostSharp: qui, qui, qui e qui. Oggi voglio approfondire alcuni concetti e far vedere come lo sto usando con soddisfazione, molta soddisfazione.
AOP: Compile Time Code Injection
PostSharp ragiona in maniera completamente diversa rispetto a tutti i framework che fanno policy/behavior injection, in generale siamo abituati ad un qualcosa fatto a runtime utilizzando il concetto di transparent proxy, viene cioè creato a runtime un proxy che wrappa il vostro tipo e inietta codice e comportamento, è evidente che questo ha un costo in termini di performance; ha anche interessanti vantaggi sia chiaro.
PostSharp invece interviene a compile time, PostSharp incorpora un post-compilatore che interviene dopo la compilazione fatta da MSBuild, disassembla il vostro compilato, inietta il codice e ricompila. Vediamo un esempio proprio in materia di Tracing:
Torniamo al codice di esempio da cui siamo partiti e trasformiamolo così:
Urka :-) praticamente abbiamo “gratis” un trace di tutto quello che è successo nel codice, ma cosa è successo? Apriamo con Reflector l’assembly generato:
Il metodo Main() del nostro programma è diventato qualcosa di decisamente diverso, e così tutto il resto. PostSharp ha infatti iniettato una montagna di codice per far si che vengano effettuate le chiamate al codice del nostro attributo.
Non mi dilungo oltre perchè ho parlato a lungo di come funziona, tutto bello però c’è un problema. Se osserviamo bene il codice generato c’è un sacco di overhead che per quello che vogliamo fare è pressochè inutile (per altri usi di PostSharp è una manna ma per il tracing non serve proprio a nulla). L’overhead è in soldoni questo:
Ma non tutto è perso, anzi. PostSharp mette a disposizione anche un livello di intervento decisamente più basso, chiamato Weaver, che vi consete di inserirvi direttamente nel processo di Post-Compilazione al fine di emettere direttamente codice… IL…
Naturalmente lungi da me intraprendere questa impervia ed ardua strada, soprattutto se altri lo hanno già fatto ;-)
Log4PostSharp
Dopo aver seguito i passi per il setup di tutto l’ambaradan possiamo cambiare il nostro codice in questo modo:
Le differenze sono evidenti, non c’è più traccia di tutta l’infrastruttura di PostSharp (che infatti non è più una dipendenza a runtime per il nostro programma) ma troviamo solo del sano codice di tracing molto simle (naming a parte) a quello che potremmo scrivere noi. Ed ecco il risultato a runtime con la configurazione di log4net di cui sopra:
Di nuovo urka ;-)
Un paio di note:
Una cosa che ho fatto è stata però ricompilare il plugin Log4PostSharp per la versione 1.5 di PostSharp, la versione originale è per la 1.0, perchè il post-compiler della 1.5 è mostruosamente più veloce.
Per quello che rigarda il Logging continuo a farlo a mano, ma quello ha anche aspetti interessanti e problematiche curiose che val la pena affrontare.
.m
- Il Tracing è necessario;
- Il Tracing è di una noia mortale;
- Tracing: quello che fa il codice: il metodo tal dei tali è stato invocato con la lista di parametrei xyz;
- Logging: quello che fa l’utente: l’utente ‘mauro’ alla tal ora del tal giorno ha salvato la tal anagrafica cliente;
se pensiamo a quanto tempo investiamo nella scrittura, e manutenzione, di quel codice mi viene male, questo in generale a prescindere dallo strumento di Tracing che usiamo.class Program { static TraceSource tracer = new TraceSource( "Program" ); static void Main( string[] args ) { try { tracer.TraceEvent( TraceEventType.Verbose, 0, "Entering method..." ); var p = new Person(); p.FirstName = "Mauro"; p.LastName = "Servienti"; var x = p.Echo( p ); Console.WriteLine( x ); } catch( Exception e ) { tracer.TraceEvent( TraceEventType.Critical, 0, "Unhandled Exception: {0}", e ); throw; } finally { tracer.TraceEvent( TraceEventType.Verbose, 0, "Exiting method..." ); } } }
Introducing AOP with PostSharp
Il tracing è uno di quegli aspetti della programmazione in cui AOP potrebbe fare da padrone, alla fine della fiera quel codice è generalizzabilissimo e varrebbe proprio la pena di farlo.
Ho già parlato più di una volta di PostSharp: qui, qui, qui e qui. Oggi voglio approfondire alcuni concetti e far vedere come lo sto usando con soddisfazione, molta soddisfazione.
AOP: Compile Time Code Injection
PostSharp ragiona in maniera completamente diversa rispetto a tutti i framework che fanno policy/behavior injection, in generale siamo abituati ad un qualcosa fatto a runtime utilizzando il concetto di transparent proxy, viene cioè creato a runtime un proxy che wrappa il vostro tipo e inietta codice e comportamento, è evidente che questo ha un costo in termini di performance; ha anche interessanti vantaggi sia chiaro.
PostSharp invece interviene a compile time, PostSharp incorpora un post-compilatore che interviene dopo la compilazione fatta da MSBuild, disassembla il vostro compilato, inietta il codice e ricompila. Vediamo un esempio proprio in materia di Tracing:
Creiamo una classe che deriva da una classe base del framework di PostSharp, in questo caso OnMethodBoundaryAspect che, come lascia intuire il nome, è una sorta di wrapper intorno al codice di un metodo (ove per metodo vengono intesi anche costruttori, proprietà ed eventi).[Serializable] public class TraceAttribute : OnMethodBoundaryAspect { public override void OnEntry( MethodExecutionEventArgs eventArgs ) { eventArgs.MethodExecutionTag = Guid.NewGuid(); Console.WriteLine( "--> ctx: {1} - {0}", eventArgs.Method.Name, eventArgs.MethodExecutionTag ); } public override void OnExit( MethodExecutionEventArgs eventArgs ) { Console.WriteLine( "<-- ctx: {1} - {0}", eventArgs.Method.Name, eventArgs.MethodExecutionTag ); } public override void OnException( MethodExecutionEventArgs eventArgs ) { Console.WriteLine( "--- Exception at" ); Console.WriteLine( "--- ctx: {1} - {0}", eventArgs.Method.Name, eventArgs.MethodExecutionTag ); Console.WriteLine( "--- Begin of Stack Trace" ); Console.WriteLine( eventArgs.Exception.StackTrace ); Console.WriteLine( "--- End of Stack Trace" ); } }
Torniamo al codice di esempio da cui siamo partiti e trasformiamolo così:
Se lo compiliamo e manidamo in esecuzione l’output è questo:[assembly: TraceAttribute( AttributeTargetMembers = "*",
AttributeTargetAssemblies = "PostSharp Tracing" )] namespace PostSharp_Tracing { class Program { static void Main( string[] args ) { var p = new Person(); p.FirstName = "Mauro"; p.LastName = "Servienti"; var x = p.Echo( p ); Console.WriteLine( x ); Console.Read(); } } }
Urka :-) praticamente abbiamo “gratis” un trace di tutto quello che è successo nel codice, ma cosa è successo? Apriamo con Reflector l’assembly generato:
Il metodo Main() del nostro programma è diventato qualcosa di decisamente diverso, e così tutto il resto. PostSharp ha infatti iniettato una montagna di codice per far si che vengano effettuate le chiamate al codice del nostro attributo.
Non mi dilungo oltre perchè ho parlato a lungo di come funziona, tutto bello però c’è un problema. Se osserviamo bene il codice generato c’è un sacco di overhead che per quello che vogliamo fare è pressochè inutile (per altri usi di PostSharp è una manna ma per il tracing non serve proprio a nulla). L’overhead è in soldoni questo:
- MSBuild compila il vostro codice;
- PostSharp disassembla e deve iniettare le chiamate;
- Quello che però deve fare non è chiamare il vostro codice perchè non ha idea di come farlo non conoscendolo… deve quindi creare un generico e flessibile ponte: programma –> infrastruttura di PostSharp –> codice di tracing –> infrastruttura di PostSharp –> programma;
Ma non tutto è perso, anzi. PostSharp mette a disposizione anche un livello di intervento decisamente più basso, chiamato Weaver, che vi consete di inserirvi direttamente nel processo di Post-Compilazione al fine di emettere direttamente codice… IL…
Naturalmente lungi da me intraprendere questa impervia ed ardua strada, soprattutto se altri lo hanno già fatto ;-)
Log4PostSharp
Dopo aver seguito i passi per il setup di tutto l’ambaradan possiamo cambiare il nostro codice in questo modo:
L’unica differenza evidente sono le dichiarazioni in testa, aggiungiamo l’attributo XmlConfiguration che istruisce log4net (utilizzato come framework di logging da Log4PostSharp) su come gestire la configurazione e rimpiazziamo il nostro attributo TraceAttribute con LogAttribute incluso nella librearia Log4PostSharp, ricordiamoci anche di aggiungere la configurazione di log4net nel file app.config:[assembly: XmlConfigurator( Watch = true )] [assembly: Log( AttributeTargetMembers = "*", AttributeTargetAssemblies = "PostSharp Tracing", EntryLevel = LogLevel.Debug, ExitLevel = LogLevel.Debug )] namespace PostSharp_Tracing { class Program { static void Main( string[] args ) { var p = new Person(); p.FirstName = "Mauro"; p.LastName = "Servienti"; var x = p.Echo( p ); Console.WriteLine( x ); Console.Read(); } } }
Ecco il nuovo metodo Main dopo la Post-Compilazione:<configSections> <section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler, log4net" /> configSections> <log4net> <appender name="consoleAppender" type="log4net.Appender.ConsoleAppender"> <layout type="log4net.Layout.PatternLayout"> <conversionPattern value="%date [%thread] %-5level %logger %ndc - %message%newline" /> layout> appender> <root> <level value="ALL" /> <appender-ref ref="consoleAppender" /> root> log4net>
Le differenze sono evidenti, non c’è più traccia di tutta l’infrastruttura di PostSharp (che infatti non è più una dipendenza a runtime per il nostro programma) ma troviamo solo del sano codice di tracing molto simle (naming a parte) a quello che potremmo scrivere noi. Ed ecco il risultato a runtime con la configurazione di log4net di cui sopra:
Di nuovo urka ;-)
Un paio di note:
- il codice iniettato non interferisce in alcun modo con la debug-experience;
- Il codice di tracing dipende da log4net e vi obbliga all’uso di log4net; utilizzando la prima tecnica che abbiamo visto sarebbe abbastanza facile utilizzare IoC/DI nel TraceAttribute per iniettare a runtime il motore di tracing ma:
- IMVHO: non ne vale la pena neanche dipinti sui muri ;-)
- Generalizzare il sistema di Tracing vi porterebbe ad avere un’infrastruttura di Tracing veramente povera, vedasi come esempio quella di Castle Windsor, perchè le uniche feature che potreste esporre sono il minimo comun denominatore delle feature messe a disposizione dai motori di tracing che volete supportare;
- Generalizzare/cambiare il motore di tracing con la seconda tecnica ha invece un costo altissimo perchè dovete essere dei mezzi guru di IL e della generazione di codice a compile-time… quindi viva log4net ;-)
Una cosa che ho fatto è stata però ricompilare il plugin Log4PostSharp per la versione 1.5 di PostSharp, la versione originale è per la 1.0, perchè il post-compiler della 1.5 è mostruosamente più veloce.
Per quello che rigarda il Logging continuo a farlo a mano, ma quello ha anche aspetti interessanti e problematiche curiose che val la pena affrontare.
.m
Labels:
AOP,
PostSharp,
Software Mason
Spettacolo!
dopo quello che ho visto ieri in Moto GP per quel che mi riguarda la Formula 1 può tranquillamente chiudere i battenti.
Semplicemente fantastico
.m
Semplicemente fantastico
.m
Labels:
Why not...
Sunday, June 14, 2009
Curiosity killed the cat. <cit.>
Io sinceramente ogni tanto mi pento di essere così “goloso” di conoscenza… perchè già il nostro lavoro è, se vogliamo, una fonte infinita di possibilità se poi ci mettiamo ad ascoltare le pulci nell’orecchio allora siamo proprio rovinati.
In viaggio verso Predappio per il workshop UGIdotNet il capo butta li una frase del tipo: Mi chiedo come mai i database ad oggetti non prendano piede. (citazione molto vaga ma la memoria alla mia età ormai fa acqua…).
Quindi… è domenica mattina presto, l’ufficio è aperto, c’è una giornata meravigliosa e la dolce metà è ancora a pelle d’orso nel letto… quindi quale momento migliore per soddisfare un po’ di sana curiosità?
db4o
E’ da anni che questo nome mi gira in testa, direi sin dall’epoca in cui per la prima volta ho sentito parlare di Tamino…
Che cosa è: db4o è un database ad oggetti, dove il paradigma relazionale scompare per essere completamente sostituito dal concetto di domain model anche nel “db”; La definizione è certamente molto casareccia e su Wikipedia ne trovate una molto più seria ;-)
Prima di dare uno sguardo a che cosa fa e come lo fa ci sono le domande, come ogni cosa che vuole rivoluzionare il modello a cui siamo fortemente abituati. In questo caso le prime cose che mi sono chiesto sono:
n.d.r.: Direi non da poco… pienamente supportato sul Compact Framework 3.5.
Quindi… essenziale per partire è la sola class library di db4o; db4o è OpenSource e rilasciato con licenza GPL, c’è anche una versione commerciale con supporto etc, etc.. ci sono poi i tool di amministrazione in 2 versioni:
Questo ci porta a introdurre le modalità di query supportate da db4o:
QBE: QueryByExample
Una primissima, e direi per la nostra mentalità inusuale, modalità di query è la QueryByExample:
Native Query (NQ)
Un’altra modalità di query è la Native Query, quella nativa da quel che ho capito… Il concetto di Native Query per noi dotnettinani è di una semplicità disarmante:
, Predicate non Expression, al metodo generico Query specificando con T quale è il tipo che stiamo interrogando.
SODA: Simple Object Database Access
Tutto diventa gasato… :-) la documentazione dice che SODA è l’API interna del motore di query di db4o, è presente per 2 motivi:
Complichiamoci un attimo la vita, altrimenti che gusto c’è ;-) questa è la classe Person nel suo insieme:
L’oggetto/istanza Person viene infatti salvata 2 volte… male direte voi, ma è evidente che non è possibile che non ci abbiano pensato… :-)
Cosa succede? stiamo semplicemente simulando il concetto di “disconnesso”, che nota di redazione non è pienamente supportato da db4o, nello snippet di cui sopra facciamo alcune cose interessanti:
Ok, funziona… ma è pericoloso, ecco perchè non è consigliato. Immaginiamo uno scenario disconnesso un po’ più complesso:
E’ comunque un problema aperto e vi invito a non fidarvi assolutamente delle conclusioni che ho tratto io perchè sono frutto di una mezz’ora di esperimenti e tentativi, nulla di più.
Direi che per ora vi ho tediato fin troppo, credo di non aver risposto a nessuna delle domande iniziali, ma credo anche che sarebbe stato impossibile farlo in solo 4 ore di prove dal primo download.
Giusto un hint… questo funziona perfettamente:
L’intenziona adesso sarebbe quella di provarlo sul campo in un piccolo progetto personale che è partito (è lontano ancora dall’essere un embrione) con Sql Compact e che ha anche la necessità di girare su un device Windows Mobile, quindi necessita della replica… vedremo.
.m
In viaggio verso Predappio per il workshop UGIdotNet il capo butta li una frase del tipo: Mi chiedo come mai i database ad oggetti non prendano piede. (citazione molto vaga ma la memoria alla mia età ormai fa acqua…).
Quindi… è domenica mattina presto, l’ufficio è aperto, c’è una giornata meravigliosa e la dolce metà è ancora a pelle d’orso nel letto… quindi quale momento migliore per soddisfare un po’ di sana curiosità?
db4o
E’ da anni che questo nome mi gira in testa, direi sin dall’epoca in cui per la prima volta ho sentito parlare di Tamino…
Che cosa è: db4o è un database ad oggetti, dove il paradigma relazionale scompare per essere completamente sostituito dal concetto di domain model anche nel “db”; La definizione è certamente molto casareccia e su Wikipedia ne trovate una molto più seria ;-)
Prima di dare uno sguardo a che cosa fa e come lo fa ci sono le domande, come ogni cosa che vuole rivoluzionare il modello a cui siamo fortemente abituati. In questo caso le prime cose che mi sono chiesto sono:
- Prestazioni?
- Scalabilità orizzontale?
- Replica?
- Variazioni dello schema?
- Indicizzazione e ricerca?
- Constraint sui dati?
- Gestione della concorrenza?
- Transazioni?
- Amministrazione?
n.d.r.: Direi non da poco… pienamente supportato sul Compact Framework 3.5.
Quindi… essenziale per partire è la sola class library di db4o; db4o è OpenSource e rilasciato con licenza GPL, c’è anche una versione commerciale con supporto etc, etc.. ci sono poi i tool di amministrazione in 2 versioni:
- commerciale con integrazione nell’ambiente di sviluppo;
- free e stand alone, completamente scritto in java. Una chiavica… le cose che non vanno al primo colpo a me personalmente fanno roteare i ma*oni al volo :-)
- Db4objects.Db4o.dll;
- Db4objects.Db4o.Linq.dll (e qui slurp…);
“Person” è una classe tradizionalissima, completamente POCO, senza nessun requisito di sorta, proprio nessuno. Per uso avanzato e per sfruttare tutte le caratteristiche di db4o in realtà POCO si perde un po’ per strada, ma non lo vedo come un problema. Db4oFactory è l’entry point per accedere ad un database di db4o, nello specifico OpenFile se non trova il file lo crea. Le modalità di accesso sono essenzialmente 2:var person = new Person(); person.FirstName = "Mauro"; person.LastName = "Servienti"; person.BornDate = new DateTime( 1973, 1, 10 ); Console.WriteLine( person ); using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person ); var query = db.Cast<Person>() .Where( p => p.FirstName.StartsWith( "M" ) );foreach( var p in query ) { Console.WriteLine( p ); }
}
- Single User, quella dell’esempio, che non è realmente single user ma va poco oltre;
- Client-Server: simile al tradizionale accesso che siamo abituati a fare ad un rdbms in rete;
- Il più evidente: pieno supporto per Linq e per gli Expression tree: quell’extension method Cast
(pessima scelta di naming) infatti non è il Cast di IEnumerable ma bensì l’entry point per dire al motore di costruzione delle query quale è la “tabella master” su cui volete operare, nei miei esempi mi sono già fatto un extension method decisamente più parlante:
static class ObjectContainerExtensions { public static IDb4oLinqQueryPossiamo quindi fare un paragone molto spannometrico e dire che l’istanza di IObjectContainer ritornata dalla OpenFile() è paragonabile al concetto di DataContext di Linq2Sql/EF ed in effetti ogni IObjectContainer rappresenta in tutto e per tutto una Unit of Work con, secondo concetto importante:From ( this IObjectContainer db ) { return db.Cast (); } }
- Identity Map: un IObjectContainer implementa quindi i pattern UnitOfWork e Identity Map;
Questo ci porta a introdurre le modalità di query supportate da db4o:
QBE: QueryByExample
Una primissima, e direi per la nostra mentalità inusuale, modalità di query è la QueryByExample:
In soldoni db4o ci permette di passargli un “template” dell’oggetto che stiamo cercando e se valorizziamo alcune proprietà dell’oggetto questi valori verranno usati come filtro per la ricerca, se la proprietà non sono valorizzare, o sono valorizzate al valore di default per il tipo della proprietà, allora db4o le considererà come “wild card”.using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { IObjectSet result = db.QueryByExample( new Person() { FirstName = "Mauro" } ); foreach( Object obj in result ) { Console.WriteLine( obj ); } }
Native Query (NQ)
Un’altra modalità di query è la Native Query, quella nativa da quel che ho capito… Il concetto di Native Query per noi dotnettinani è di una semplicità disarmante:
altro non facciamo che passare un Predicateusing( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { var result = db.Query<Person>( p => p.FirstName == "Mauro" ); foreach( Object obj in result ) { Console.WriteLine( obj ); } }
SODA: Simple Object Database Access
Tutto diventa gasato… :-) la documentazione dice che SODA è l’API interna del motore di query di db4o, è presente per 2 motivi:
- Retrocompatibilità;
- Costruzione dinamica delle query: problema direi superato con gli ExpressionTree, per i linguaggi che li supportano;
Chiediamo all’engine di creare una nuova query, impostiamo un Constrain sul tipo che stiamo cercando, quindi gli diciamo cosa stiamo cercando e con quale valore, infine eseguiamo la query. La nota interessante qui è la sintassi usata per riferirsi alla proprietà FirstName: quella strana sintassi è dovuta al fatto che nella mia classe Person sto utilizzando le Auto Implemented Property di C# 3.0, se avessi avuto una proprietà “tradizionale” implementata quindi con il backing field li avrei dovuto specificare il nome del backing field. Questo ci fa scoprire un interessante internal su come db4o memorizzi gli oggetti.using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { var query = db.Query(); query.Constrain( typeof( Person ) ); query.Descend( "k__BackingField" ) .Constrain( "Mauro" ); var result = query.Execute(); foreach( Object obj in result ) { Console.WriteLine( obj ); } }
Complichiamoci un attimo la vita, altrimenti che gusto c’è ;-) questa è la classe Person nel suo insieme:
Ci sono alcune cose interessanti:sealed class Person { public String FirstName { get; set; } public String LastName { get; set; } public DateTime BornDate { get; set; } public Int32 Age { get { return ( ( Int32 )( DateTime.Now.Date - this.BornDate ).TotalDays / 365 ); } } public override string ToString() { return String.Format( "{0} {1}, {2}", this.FirstName, this.LastName, this.Age ); } }
- è “sealed”;
- Non ha nessun membro virtual, e non potrebbe è sealed ;-);
- Non è pubblica;
- E’ effettivamente POCO, non c’è proprio nulla che ci faccia pensare che abbia qualcosa a che fare con un db;
Notate le chiamate un po’ barbine al metodo Store()? L’output di questo snippet è curioso, in realtà se ci pensiamo è più che giusto, ma al primo colpo lascia un po’ così:var person = new Person(); person.FirstName = "Mauro"; person.LastName = "Servienti"; person.BornDate = new DateTime( 1973, 1, 10 ); using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person ); db.Store( person );
Console.WriteLine( "Double Store, Count: {0}", db.From<Person>().Count() ); } using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person );
Console.WriteLine( "Single Store, Count: {0}", db.From<Person>().Count() ); } using( var db = Db4oFactory.OpenFile( @"c:\temp\db4o_Sample.db4o" ) ) { Console.WriteLine( "Count: {0}", db.From<Person>().Count() ); var query = db.From<Person>() .Where( p => p.FirstName.StartsWith( "M" ) ); foreach( var p in query ) { Console.WriteLine( p ); } }
L’oggetto/istanza Person viene infatti salvata 2 volte… male direte voi, ma è evidente che non è possibile che non ci abbiano pensato… :-)
Cosa succede? stiamo semplicemente simulando il concetto di “disconnesso”, che nota di redazione non è pienamente supportato da db4o, nello snippet di cui sopra facciamo alcune cose interessanti:
- Creiamo una singola istanza della classe Person;
- Apriamo un “data context” e salviamo la stessa istanza 2 volte: questo porta, giustamente ad una singola “insert”, la doppia Store viene semplicemente ignorata in questo esempio perchè l’istanza di Person è “known” ergo il data context si rendo conto che la doppia store è un “non sense”
- Chiudiamo il data context e ne apriamo un altro rifacciamo la store e “boom” a questo punto abbiamo 2 “record” nel nostro database… siamo in modalità disconnessa a questo punto e giustamente l’engine non ha la più pallida idea che Person sia la stessa di prima; cosa possiamo fare?
All’interno del primo data context recuperiamo un l’id univoco che db4o genera per ogni “record” quando apriamo il secondo data context chiamiamo il metodo Bind() passando la “nuova” reference e l’id, internamente l’engine “carica” l’istanza presente nel db e la rimpiazza in memoria (nell’Identity Map) con quello che gli stiamo dando noi.long id; using( var db = Db4oFactory.OpenFile( config, @"c:\temp\db4o_Sample.db4o" ) ) { db.Store( person ); db.Store( person ); Console.WriteLine( "Double Store, Count: {0}", db.From<Person>().Count() ); id = db.Ext().GetID( person ); db.Close(); } using( var db = Db4oFactory.OpenFile( config, @"c:\temp\db4o_Sample.db4o" ) ) { db.Ext().Bind( person, id ); db.Store( person ); Console.WriteLine( "Single Store, Count: {0}", db.From<Person>().Count() ); db.Close(); }
Ok, funziona… ma è pericoloso, ecco perchè non è consigliato. Immaginiamo uno scenario disconnesso un po’ più complesso:
- Servizio WCF che maschera lo storage;
- Applicazione client;
- Il client recupera un grafo complesso di oggetti;
- Vengono modificati solo alcuni oggetti;
- Siccome il client è intelligente rimanda al servizio solo la parte modificata;
- Il servizio chiama Bind() e… solo una parte viene rimpiazzata in memoria con la “fastidiosa” conseguenza che quando chiamate Store() all’engine arriva un grafo parziale, con un id esistente e il buon db4o, giustamente, cancella dal db quello che non gli arriva… ooops…
E’ comunque un problema aperto e vi invito a non fidarvi assolutamente delle conclusioni che ho tratto io perchè sono frutto di una mezz’ora di esperimenti e tentativi, nulla di più.
Direi che per ora vi ho tediato fin troppo, credo di non aver risposto a nessuna delle domande iniziali, ma credo anche che sarebbe stato impossibile farlo in solo 4 ore di prove dal primo download.
Giusto un hint… questo funziona perfettamente:
dimostrando che anche query complesse su grafi complessi funzionano, naturalmente okkio che la query (l’ExpressionTree) deve essere trasformabile dall’engine pena non una exception ma piuttosto il caricamento di tutto il grafo e l’analisi dello stesso in memoria per risolvere la query.var query = db.From<Person>() .Where( p => p.FirstName.StartsWith( "M" ) && p.Addresses.Count > 2 );
L’intenziona adesso sarebbe quella di provarlo sul campo in un piccolo progetto personale che è partito (è lontano ancora dall’essere un embrione) con Sql Compact e che ha anche la necessità di girare su un device Windows Mobile, quindi necessita della replica… vedremo.
.m
Labels:
Software Mason
Friday, June 12, 2009
Utente, perchè sei tu… utente!
Una “mailing list” spontanea tra svariati partecipanti mi porta verso questo post.
Il problema è la validazione dell’input dell’utente, vediamo da dove sono partito:
La situazione è decisamente banale, classicissima Window (Wpf) per l’editing di una Entity (Person), il tutto basato strettamente su M-V-VM, quindi è un “requisito” che nel code-behind della Window non ci finisca nulla, o xaml o ViewModel. Il problema è la vadilazione dell’input, diamo un paio di regole di business che “incriccano” la cosa:
ValidationRule
Il motore di Data Binding di Wpf ha un suo motore di validazioni dei dati che generalmente utilizziamo senza rendercene conto; quando scriviamo una cosa del tipo:
Exception Driven Validation
La nostra TextBox resta invariata e nel ViewModel possiamo scrivere qualcosa del tipo:
Ci sono però una serie di effetti collaterali:
IDataErrorInfo
Il framework 3.5 aggiunge a Wpf la nozione di IDataErrorInfo, interfaccia storica, offrendo quello che secondo me è il miglior compromesso in termini di validazione dei dati inseriti dell’utente. Sia chiaro si poteva fare anche prima dell’introduzione del fx 3.5 perchè quello che è stato aggiunto è:
Tutto ciò ha però un “difetto” la validazione viene fatta, decisamente comodo e vedremo tra poco perchè, in maniera “bidirezionale” quindi il motore di binding chiama il vostro sistema (la bruttura) di validazione dopo aver fatto il push del dato dalla UI alla entity e viceversa dopo aver fatto anche il pull del dato dalla entity alla UI. Questo ha quindi un effetto collaterale:
Prima di capire come soddisfare l’ultimo requisito, la visualizzazione di un “sommario” degli errori, introduciamo rapidamente il sistema di validazione usato:
Enterprise Library: Validation Application Block
Perchè reinventare la ruota? abbiamo un motore di validazione che funziona egregiamente, quindi perchè non usare quello?
Il motore di validazione (la bruttura) nel ViewModel diventa:
Intermezzo
Nel frattempo vediamo anche come visualizzare l’errore all’utente: con Wpf nulla di più semplice! Ogni controllo ha una serie di attached property che servono per stabilire cosa e come fare in caso di errori di validazione:
Validation.ErrorTemplate
L’ErrorTemplate di un controllo è quella proprietà che ci permette di personalizzare il template che vogliamo assegnare ad un controllo quando il suo stato è “sono in errore”, possiamo quindi definire nelle risorse un ControlTemplate:
ErrorSummary
Ci restano ancora un paio di nodi da scogliere:
Sulla Window mi sono limitato ad aggiungere una ListBox in binding con la collection Errors.
la form all’avvio… e:
la form dopo aver solamente premuto il pulsante “Salva”.
N.D.R.
Se il form che state utilizzando è pregno di controlli attenzione che le validazioni che vengono triggherate possono diventare migliaia nel giro di pochissimo tempo e quindi le performance della UI diventare un problema percepibile anche dall’utente.
Il codice di esempio è semplicemente un esempio volutamente semplice, offre parecchi (s)punti di ottimizzazione per cercare di limare il numero di validazioni che vengono eseguite.
E adesso?
Il prossimo passaggio, materia di un altro post, è quello di avere un sistema che in automatico analizzi le regole di validazione del modello con cui siamo in binding e visualizzi all’utente, di fianco ad ogni controllo, i requisiti per cui quel controllo sia valido. Visualizzando ad esempio, per impostazione predefinita, i controlli obbligatori e a richiesta le regole a cui ogni singolo controllo verrà assoggettato in fase di validazione.
.m
* mi piacerebbe guardare in faccia quello che ha pensato una proprietà indicizzata invece che un metodo…
Il problema è la validazione dell’input dell’utente, vediamo da dove sono partito:
La situazione è decisamente banale, classicissima Window (Wpf) per l’editing di una Entity (Person), il tutto basato strettamente su M-V-VM, quindi è un “requisito” che nel code-behind della Window non ci finisca nulla, o xaml o ViewModel. Il problema è la vadilazione dell’input, diamo un paio di regole di business che “incriccano” la cosa:
- Rule A: Person.FirstName non deve essere vuoto;
- Rule B: Person.LastName non deve essere vuoto;
- Evidenziare ogni singolo controllo in uno stato non valido;
- All’avvio della Window non visualizzare i controlli come invalidi anche se lo sono;
- Visualizzare l’elenco dei problemi solo ed esclusivamente nel momento in cui l’utente cerca di salvare;
ValidationRule
Il motore di Data Binding di Wpf ha un suo motore di validazioni dei dati che generalmente utilizziamo senza rendercene conto; quando scriviamo una cosa del tipo:
Chiediamo al motore di binding di intercettare le exception e visualizzare il controllo in stato di errore. La sintassi è esattamente equivalente alla seguente:<TextBox Text="{Binding Path=MyProperty, ValidatesOnExceptions=True, NotifyOnValidationError=True}" />
che mette ben in evidenza il concetto di ValidationRule. Una validation rule è una regola che viene valutata per decidere se il dato che sta viaggiando dal controllo verso la entity, o viceversa, debba essere considerato valido, scrivere una ValidationRule è un’operazione banale e rende il vostro processo di validazione decisamente flessibile perchè avete la possibilità di istruire il motore di binding su dove (durante la pipeline di binding) utilizzare la vostra validation rule: potete scegliere in quale direzione intervenire e se intervenire prima o dopo il set del valore. Tutto molto interessante ma:<TextBox> <TextBox.Text> <Binding Path="MyProperty"> <Binding.ValidationRules> <ExceptionValidationRule /> Binding.ValidationRules> Binding> TextBox.Text> TextBox>
- In un’applicazione, dal mediamente grande in su, avete l’inghippo che decentralizzate la validazione su ogni singola Window/Controllo, se quindi lo stesso dato viene visualizzato/editato in punti diversi l’onere della manutenzione comincia a diventare pesante;
- Spostare la logica di validazione sulla View secondo me è, in ottica M-V-VM, sbagliato perchè da responsabilità, alla View, che non dovrebbe avere;
- Se nel team c’è anche un designer (solo designer, che usa Blend) obbligate il designer a conoscere le regole di business/validazione del dato;
- Infine, per come è pensata una ValidationRule avete il problema che non siete in grado in maniera semplice, dall’interno della rule, di accedere al contesto generale: quello che succede è che la rule ha in mano il dato/valore ma nulla di più quindi si deve limitare a valutare il dato e non lo può fare contestualizzandolo;
Exception Driven Validation
La nostra TextBox resta invariata e nel ViewModel possiamo scrivere qualcosa del tipo:
Il tutto funziona, producendo questo a runtime senza far schiantare l’applicazione (il che è già qualcosa :-D):public String FirstName { get { return this.dataSource.FirstName; } set { if( value != this.FirstName ) { if( String.IsNullOrEmpty(value) ) { throw new ArgumentException( "Il nome non può essere vuoto." ); } this.dataSource.FirstName = value; } } }
Ci sono però una serie di effetti collaterali:
- Utilizzate le eccezioni per “guidare” la logica di business e non sarebbe cosa buona e giusta;
- Dovete inventarvi un sistema per estrarre la vera eccezione perchè come si vede dall’immagine la vostra ArgumentException diventa la InnerException di una TargetInvocationException;
- Non avete la possibilità di valutare il dato nel suo insieme: se le regole per un singolo dato sono più di una la prima che fallisce genera un’eccezione e verrà visualizzata solo quella;
IDataErrorInfo
Il framework 3.5 aggiunge a Wpf la nozione di IDataErrorInfo, interfaccia storica, offrendo quello che secondo me è il miglior compromesso in termini di validazione dei dati inseriti dell’utente. Sia chiaro si poteva fare anche prima dell’introduzione del fx 3.5 perchè quello che è stato aggiunto è:
- una ValidationRule: DataErrorValidationRule, che altro non fa che controllare se il target del data binding è IDataErrorInfo e nel caso delegare la validazione del dato dopo averlo scritto;
- Una nuova versione della classe/markup extension Binding con una proprietà ValidatesOnDataErrors;
<TextBox Text="{Binding Path=MyProperty, ValidatesOnDataErrors=True, NotifyOnValidationError=True}" />Nel nostro ViewModel avremo invece una cosa del tipo:
Il motore di binding chiamerà, dopo il set della proprietà, quella bruttura* passandovi il nome della proprietà che deve essere validata. La bruttura deve tornare o un messaggio di errore o una stringa nulla per identificare che non ci sono errori.public String this[ String propertyName ] { get { /* * Perform validation and return the * error message for the given property, * otherwise return null. */ return null; } }
Tutto ciò ha però un “difetto” la validazione viene fatta, decisamente comodo e vedremo tra poco perchè, in maniera “bidirezionale” quindi il motore di binding chiama il vostro sistema (la bruttura) di validazione dopo aver fatto il push del dato dalla UI alla entity e viceversa dopo aver fatto anche il pull del dato dalla entity alla UI. Questo ha quindi un effetto collaterale:
La validazione viene effettuata anche la prima volta che viene inizializzato il binding di ogni singolo controllo; il rischio è quindi:la cosa è facilmente aggirabile, fortunatamente:
Il tutto in termini di UX non è proprio consistente in quanto l’utente non ha ancora fatto nulla, quindi è poco sensato dirgli che ha già sbagliato.
- l’utente chiede di creare una nuova Person;
- la Window per l’inserimento dei dati appare e purtroppo appare con già visualizzati tutti gli errori;
Ci limitiamo a tener traccia di tutte le proprietà che sono state validate almeno una volta, supponendo che la prima volta sia quella che del binding iniziale.readonly HashSet<String> propertiesValidatedAtLeastOnce = new HashSet<String>(); public String this[ String propertyName ] { get { if( !this.propertiesValidatedAtLeastOnce.Contains( propertyName ) ) { this.propertiesValidatedAtLeastOnce.Add( propertyName ); return null; } /* * Validation for the given property * has been already requested once, * proceed with validation */ return null; } }
Prima di capire come soddisfare l’ultimo requisito, la visualizzazione di un “sommario” degli errori, introduciamo rapidamente il sistema di validazione usato:
Enterprise Library: Validation Application Block
Perchè reinventare la ruota? abbiamo un motore di validazione che funziona egregiamente, quindi perchè non usare quello?
Passando oltre… quello che faccio è una cosa del tipo, sulla entity (con la benedizione del capo):
il capo direbbe che ci sono fior di motivi per inventarne uno nuovo… :-D
Definisco le regole di validazione a cui la entity deve sempre sottostare, quindi non quelle contestuali all’uso.class Person { [NotNullValidator( MessageTemplate = "FirstName non può essere nullo." )]
[StringLengthValidator( 0, RangeBoundaryType.Exclusive, 0, RangeBoundaryType.Ignore, MessageTemplate="FirstName è obbligatorio." )] public String FirstName { get; set; } [StringLengthValidator( 0, RangeBoundaryType.Exclusive, 0, RangeBoundaryType.Ignore, MessageTemplate = "LastName è obbligatorio." )] public String LastName { get; set; } }
Il motore di validazione (la bruttura) nel ViewModel diventa:
Creo un validatore “statico”, la creazione di un validatore è la parte veramente onerosa in termini prestazionali della validazione, ad ogni richiesta eseguo la validazione dell’intera entity e mi salvo il risultato, quindi cerco all’interno dei risultati se ci sono degli errori per la proprietà che stiamo validando, nel caso aggrego i messaggi di errore: in questo modo al client tornano tutti gli errori per quella proprietà e non solo il primo.ValidationResults results; readonly Validator<Person> validator = ValidationFactory.CreateValidator<Person>(); readonly HashSet<String> propertiesValidatedAtLeastOnce = new HashSet<String>(); public String this[ String propertyName ] { get { if( !this.propertiesValidatedAtLeastOnce.Contains( propertyName ) ) { this.propertiesValidatedAtLeastOnce.Add( propertyName ); return null; } this.results = this.validator.Validate( this.dataSource ); if( !results.IsValid ) { var error = results.Where( err => err.Key == propertyName ) .Select( err => err.Message ) .Aggregate( String.Empty, ( a, e ) => { a += e + Environment.NewLine; return a; } ) .TrimEnd( Environment.NewLine.ToCharArray() ); return error; } return null; } }
Intermezzo
Nel frattempo vediamo anche come visualizzare l’errore all’utente: con Wpf nulla di più semplice! Ogni controllo ha una serie di attached property che servono per stabilire cosa e come fare in caso di errori di validazione:
Validation.ErrorTemplate
L’ErrorTemplate di un controllo è quella proprietà che ci permette di personalizzare il template che vogliamo assegnare ad un controllo quando il suo stato è “sono in errore”, possiamo quindi definire nelle risorse un ControlTemplate:
che definisce come vogliamo visualizzare l’errore, ed infine associarlo al nostro controllo:<ControlTemplate x:Key="errorTemplate"> <DockPanel> <AdornedElementPlaceholder x:Name="elementPlaceHolder" /> <TextBlock Foreground="Red" ToolTip="{Binding ElementName=elementPlaceHolder, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}" FontSize="20">*TextBlock> DockPanel> ControlTemplate>
Possiamo anche definire uno stile per cambiare, ad esempio il background:<TextBox Validation.ErrorTemplate="{StaticResource errorTemplate}" ... />
e “triggherare” il colore di background quando l’altra proprietà findamentale del motore di validazione cambia di valore: Validation.HasError è l’attached property che ci fornisce lo stato di un controllo.<Style x:Key="textBoxInError" TargetType="{x:Type TextBox}"> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="#EFB8B8" /> Trigger> Style.Triggers> Style>
ErrorSummary
Ci restano ancora un paio di nodi da scogliere:
- Vogliamo visualizzare, ad esempio al tentativo di salvataggio, un elenco di tutti gli errori;
- Vogliamo, quando viene visualizzato il sommario, anche triggherare lo stato invalido sugli eventuali controlli che non sono in stato invalido ma sono invalidi… un esempio chiarificatore:
- l’utente avvia la creazione di una nuova Person;
- la Window non è ancora in stato non valido perchè l’utente non ha fatto nulla per ora;
- l’utente cerca di pigiare il bottone Salva;
- Visualizziamo l’ErrorSummary;
- Vogliamo fare in modo che tutti i controlli sulla Window, se non validi, visualizzino il loro stato;
Abbiamo una semplice lista di stringhe che rappresenta tutti gli errori, al tentativo di salvataggio:public ObservableCollection<String> Errors { get; private set; }void TrySave() { this.Errors.Clear(); this.results = this.validator.Validate( this.dataSource ); if( !this.results.IsValid ) { foreach( var err in results ) { if( this.PropertyChanged != null ) { this.PropertyChanged( this, new PropertyChangedEventArgs( err.Key ) ); } this.Errors.Add( err.Message ); } } }
- Ci liberiamo di ogni errore precedente;
- Eseguiamo nuovamente la validazione della entity;
- Se non è valida:
- Aggiungiamo ogni errore alla lista degli errori;
- Scateniamo un evento PropertyChanged per ognuna delle proprietà non valide;
Sulla Window mi sono limitato ad aggiungere una ListBox in binding con la collection Errors.
la form all’avvio… e:
la form dopo aver solamente premuto il pulsante “Salva”.
N.D.R.
Se il form che state utilizzando è pregno di controlli attenzione che le validazioni che vengono triggherate possono diventare migliaia nel giro di pochissimo tempo e quindi le performance della UI diventare un problema percepibile anche dall’utente.
Il codice di esempio è semplicemente un esempio volutamente semplice, offre parecchi (s)punti di ottimizzazione per cercare di limare il numero di validazioni che vengono eseguite.
E adesso?
Il prossimo passaggio, materia di un altro post, è quello di avere un sistema che in automatico analizzi le regole di validazione del modello con cui siamo in binding e visualizzi all’utente, di fianco ad ogni controllo, i requisiti per cui quel controllo sia valido. Visualizzando ad esempio, per impostazione predefinita, i controlli obbligatori e a richiesta le regole a cui ogni singolo controllo verrà assoggettato in fase di validazione.
.m
* mi piacerebbe guardare in faccia quello che ha pensato una proprietà indicizzata invece che un metodo…
Tuesday, June 9, 2009
UGI @ Predappio: the day after
Igor chiama, Mauro risponde :-)
L’inghippo è che oggi praticamente è “Lunedì”… ma:
Questa è l’unica foto che ho io, per le altre aspettiamo Nicolò che è diventato per uso capione il fotografo ufficiale. La foto qui sopra è ralativa alla superba serata pre-conf che i ragazzi di Dental Trey, che ha ospitato il workshop, ci hanno regalato.
Il workshop, per come la vedo io, è stato un successo, primo workshop indipendente, staccato dalla mamma Milano, in una zona non certo rinomata, in termini di convention informatiche, argomenti di vario genere e livello, dall’introduttivo al Raff-level… :-), passando per Visual Studio 2010 Beta1, Silverlight, Mono, MVC e architettura e project management.
Ho visto tanta gente che non vedevo da tempo, e ho visto anche tante facce nuove.
Da non dimenticare, per me e il genio del male, la capatina di ieri sera a piacenza durante il viaggio di ritorno…
.m
L’inghippo è che oggi praticamente è “Lunedì”… ma:
Questa è l’unica foto che ho io, per le altre aspettiamo Nicolò che è diventato per uso capione il fotografo ufficiale. La foto qui sopra è ralativa alla superba serata pre-conf che i ragazzi di Dental Trey, che ha ospitato il workshop, ci hanno regalato.
- Posto incantevole;
- Cena, home made, perfetta:
- Aperitivo in giardino (con Piscina…);
- Tagliatelle all’uovo fatte in casa con ragù e piselli;
- “Grigliatina” mista con patate al forno, il tutto fatto su quel superbo “camino” che fa da sfondo alla sala da pranzo;
- Ospitalità che difficilmente potrà avere rivali.
Il workshop, per come la vedo io, è stato un successo, primo workshop indipendente, staccato dalla mamma Milano, in una zona non certo rinomata, in termini di convention informatiche, argomenti di vario genere e livello, dall’introduttivo al Raff-level… :-), passando per Visual Studio 2010 Beta1, Silverlight, Mono, MVC e architettura e project management.
Ho visto tanta gente che non vedevo da tempo, e ho visto anche tante facce nuove.
Da non dimenticare, per me e il genio del male, la capatina di ieri sera a piacenza durante il viaggio di ritorno…
.m
Labels:
Why not...
Friday, June 5, 2009
[OT] La mia città fa schifo e non è colpa sua
Questo è OT, ma oggi sono incazzato.
Giro per la mia città, felicemente, o a piedi o in bicicletta perchè Treviglio è ancora un paese a misura d’uomo e non a misura di “auto con imbelle che è convinto di essere dio” e vedo sporcizia ovunque, ma proprio tanta, derivante da alcuni fattori:
Se il 30% della monnezza pubblica per le strade è generata da volantini/pubblicità/inutilità/confenizioniridondanti/quellochevoletevoi perchè i signori che sono causa prima di tutto ciò non si pagano il 30% della mia tassa sui rifiuti? perchè ricevo letteralmente chili di carta da:
Poi, mazziate i cafoni! il posacenere della vostra macchina svuotatevelo nel vostro letto e non sul nostro marciapiede.
C’è una legge che impone di raccogliere i “prodotti” dei propri cani… perchè quando esco la sera a mettere fuori la spazzatura devo fare la Parigi-Dakar sull’aiuola davanti a casa?
L’amministrazione comunale secondo me il suo lavoro lo fa, ma se tutti remano contro…
.m
Giro per la mia città, felicemente, o a piedi o in bicicletta perchè Treviglio è ancora un paese a misura d’uomo e non a misura di “auto con imbelle che è convinto di essere dio” e vedo sporcizia ovunque, ma proprio tanta, derivante da alcuni fattori:
- il cittadino è un cafone di *erda che se ha in mano qualcosa che non gli serve più semplicemente lo lascia nel posto più vicino: per terra;
- siamo invasi dall’inutilità: a occhio almeno il 30% della spazzatura che deturpa le strade sono volantini pubblicitari;
Se il 30% della monnezza pubblica per le strade è generata da volantini/pubblicità/inutilità/confenizioniridondanti/quellochevoletevoi perchè i signori che sono causa prima di tutto ciò non si pagano il 30% della mia tassa sui rifiuti? perchè ricevo letteralmente chili di carta da:
- Lidl;
- Pellicano;
- Semearo;
- Iper di Orio Center;
- Ikea;
- Euronics;
- Trony;
- Gentaglia che vuole farsi votare per le elezioni europee;
- Chi più ne ha più ne metta..
Poi, mazziate i cafoni! il posacenere della vostra macchina svuotatevelo nel vostro letto e non sul nostro marciapiede.
C’è una legge che impone di raccogliere i “prodotti” dei propri cani… perchè quando esco la sera a mettere fuori la spazzatura devo fare la Parigi-Dakar sull’aiuola davanti a casa?
L’amministrazione comunale secondo me il suo lavoro lo fa, ma se tutti remano contro…
.m
Labels:
Why not...
Mi sono un po’ rotto…
… di non fare mai i nomi e limitarsi a dire una famosa banca, quella grande anzienda, etc..
La mia banca, Banca di Credito Cooperativo, utilizza un sistema di home banking che si chiama Relax Banking, o meglio se siete un azienda ve ne dovrebbe dare un’altro, di cui non ricordo il nome… veramente. Tempo fa quando ero andato per fare il contratto scopro che:
Oggi ne ho trovata un’altra:
Devo pagare quell’effetto nell’immagine ma non riesco perchè uno criptico messaggio di errore mi dice:
Segue quindi una telefonata al Call Center, che devo dirlo ha risposto al volo, dalla quale scopro che semplicemente l’effetto è scaduto, nulla di più… la signorina candidamente mi dice anche che il sistema tratta i tempi di scadenza per gli effetti on-line in maniera diversa da quelli off-line (cartacei) quindi mi devo attaccare e andare in banca di persona… ok… ma dove sta scritta questa differenza?
Tralascio ogni considerazione sul fatto che un mio commento sulla qualità/inutilità degli errori ha portato ad una repentina conclusione della telefonata con un: si, vabbbheee, grazie e arrivederci.
Detto questo la considerazione è: se non vado errato… pago… e neanche poco…
Dimenticavo… il coso fa largo uso di popup e di finestre non ridimensionabili, per quale motivo lo sanno solo loro, e naturalmente non funziona un gran che bene con IE8, lasciamo perdere gli altri che proprio non sono neanche presenti sulla faccia della terra.
.m
La mia banca, Banca di Credito Cooperativo, utilizza un sistema di home banking che si chiama Relax Banking, o meglio se siete un azienda ve ne dovrebbe dare un’altro, di cui non ricordo il nome… veramente. Tempo fa quando ero andato per fare il contratto scopro che:
- Ci voleva per forza un floppy disk, adesso so che è stato aggiornato a chiavetta USB, per metterci i certificati digitali… tristezza, già al tempo un portatile con un floppy era un miraggio;
- Ci voleva per forza installata la JVM di Microsoft, illegalissima… senza contare che sulla mia macchina del tempo con Windows 2003 non avrei avuto modo di installarla
- Fatta qualche rimostranza la “signorina” mi dice di scrivere che io ho un Mac e quindi mi avrebbero dato un versione diversa: Relax Banking appunto;
Oggi ne ho trovata un’altra:
Devo pagare quell’effetto nell’immagine ma non riesco perchè uno criptico messaggio di errore mi dice:
La disposizioni non ha superato i controlli su date e/o importi… etc…Mi domando.. che vuol dire? A parte la grammatica che non è un’opinione, mi armo di pazienza e mi scarico il manuale in pdf… naturalmente il manuale è aggiornato alla versione 0.0.0.x del software mentre quella attuale è la 10000.x quindi non ci azzecca una mazza, inoltre sul manuale non c’è traccia di una benchè minima reference degli errori possibili…
Segue quindi una telefonata al Call Center, che devo dirlo ha risposto al volo, dalla quale scopro che semplicemente l’effetto è scaduto, nulla di più… la signorina candidamente mi dice anche che il sistema tratta i tempi di scadenza per gli effetti on-line in maniera diversa da quelli off-line (cartacei) quindi mi devo attaccare e andare in banca di persona… ok… ma dove sta scritta questa differenza?
Tralascio ogni considerazione sul fatto che un mio commento sulla qualità/inutilità degli errori ha portato ad una repentina conclusione della telefonata con un: si, vabbbheee, grazie e arrivederci.
Detto questo la considerazione è: se non vado errato… pago… e neanche poco…
Dimenticavo… il coso fa largo uso di popup e di finestre non ridimensionabili, per quale motivo lo sanno solo loro, e naturalmente non funziona un gran che bene con IE8, lasciamo perdere gli altri che proprio non sono neanche presenti sulla faccia della terra.
.m
Labels:
Why not...
Visual Studio “silent” crash e i progetti Wpf
Su entrambe le macchine di sviluppo, da tempo immemore, ho questo problema: http://karlshifflett.wordpress.com/2009/03/13/clr-hotfix-available-for-visual-studio-2008-rapid-crashing/, la sfiga vuole che entrambe le macchine di sviluppo siano la RC di Windows 7, quindi:
La cosa curiosa è che sulla macchina a 32Bit il problema affligge Visual Studio 2008 mentre su quella a 64Bit Visual Studio 2010B1 e non Visual Studio 2008: “misteri della bug”… :-)
.m
Sorry we do not have a version of this patch for Windows 7 at present…poco male, nel mio caso il workaround è abbastanza banale e indolore: basta ricordarsi di chiudere ogni file xaml prima di chiudere Visual Studio e poi alla riapertua del progetto/solution far “girare” almeno una volta il compilatore, anche su un progetto non Wpf.
La cosa curiosa è che sulla macchina a 32Bit il problema affligge Visual Studio 2008 mentre su quella a 64Bit Visual Studio 2010B1 e non Visual Studio 2008: “misteri della bug”… :-)
.m
Labels:
Why not...
Subscribe to:
Posts (Atom)