Freeteo


Pensieri e C#dice di Matteo Raumer

Silverlight: implementare un DataContext offline basato su XML

Nello sviluppo di applicazioni Silverlight, ho trovato molto bella la possibilità di installare l'applicazione OOB (Out Of the browser), il che permette tra l'altro, di lanciare l'applicazione anche se si è "fisicamente" scollegati dal server dove risiede una parte dell'applicazione.
Ragionando su una struttura "classica" dell'applicazione Silverlight + RiaServices + EntityFramework + SqlExpress 2008, se mi trovassi completamente offline ed accedessi all'applicazione di fatto essendo Silverlight un Runtime client a tutti gli effetti, l'applicazione partirebbe ma la parte di RiaServices aspettandosi di raggiungere un servizio WCF, non sarebbe funzionante.

Per questo scenario quindi di applicazione "completamente offline" ma che possa funzionare ugualmente, sarebbe utile potere in qualche modo implementare un DomainContext che punti a dei dati in locale senza riscrivere l'applicazione...semplicemente commutando da On/Offline quando necessario in maniera trasparente per l'utente.

Attualmente in RiaServices non c'è una feature integrata per questo tipo di scenario, però andando a guardare i vari overload del costruttore della classe DomainContext, è possibile specificare un DomainContext diverso da quello standard che Visual Studio ha generato automaticamente (un WebDomainClient(serviceUri) della dove "I" è l'interfaccia che WCF si aspetta etc... Perciò ho pensato di provare ad implementare un DomainClient custom, che operi sulle collection salvate/lette sul pc locale in XML (la strada più veloce è la serializzazione in XML degli oggetti del Framework...) tramite IsolateStorage.

La struttura di questa funzionalità Offline, si traduce sostanzialmente in questi oggetti:
1) Un OfflineProvider che serializzi/deserializzi le collection sul disco del client, tramite IsolateStorage
2) Un QueryEngine che si occupi di ricevere una query sotto forma di Expression<> e la applichi direttamente su una collection in memoria
3) Un OfflineDomainClient, che erediti da DomainClient e andando in overload dei metodi di Query e di SubmitChanges (passato nel costruttore del DomainContext) che riceva la query

[1: OfflineProvider]
Questa classe senza tante ottimizzazioni carica in memoria la lista delle entità (Entity) ed esegue la CRUD sulle collection in memoria:

public class OfflineDataProvider
{ 
   private Dictionary<Type, List<Entity>> database = new Dictionary<Type, List<Entity>>();

   public
void Persist()
   {
     
foreach (var tipo in database.Keys)
      {
        
IList<Entity> lista = database[tipo];
         
if (lista == null)
           
continue;

        
//--- parametri per poterlo serializzare
        
List<Type> tipi = new List<Type>();
        
tipi.Add(tipo);

         using (Stream stream = StorageManager.GetStreamForWrite(tipo))
         {
           
DataContractSerializer ser = new DataContractSerializer(lista.GetType(), tipi);
   
            using (var w = XmlWriter.Create(stream, settings))
            {
               ser.WriteObject(w, lista);
            }
         }
      }
   }


   public void ReadFile(Type tipo)
   {
      //--- controllo se c'è in memoria
      if (!database.ContainsKey(tipo))
        
database.Add(tipo, new List<Entity>());

      Type[] tipi = new Type[] { tipo };
     
DataContractSerializer ser = new DataContractSerializer(typeof(List<Entity>), tipi);
     
     
using (Stream stream = StorageManager.GetStreamForRead(tipo))
     
{
         
        
try  {
             database[tipo] = ser.ReadObject(stream)
as List<Entity>;
         

         
catch { }
      }
   }


  
public IEnumerable<Entity> GetAll(Type tipo)
   {
      //--- controllo se c'è in memoria
      if (!database.ContainsKey(tipo))
        
ReadFile(tipo);

      return database[tipo];
   }


   public void Delete(Entity item)
   {
      //--- safety code
     
if (item == null)
       
return;

     
PropertyInfo idProperty = //<--- qui è necessario sapere qual'è la proprietà ID dell'oggetto
      Type tipo = item.GetType();

     
//--- cerco nella collection in memoria se c'è
     
if (database.ContainsKey(tipo))
     
{
        
if (database[tipo].Count > 0)
        
{
           
var id = idProperty.GetValue(item, null);
           
Entity trovato = database[tipo].FirstOrDefault(i => idProperty.GetValue(i, null).Equals(id));
           
if (trovato != null)
                database[tipo].Remove(trovato);
         }
     
}
  
}


   public bool Salva(Entity item)
  
{
     
//--- mi setto le cose per sotto
     
PropertyInfo idProperty = //<--- qui è necessario sapere qual'è la proprietà ID dell'oggetto
     
Type tipo = item.GetType();
     
     
//--- cerco nella collection in memoria se c'è
     
List<Entity> lista = new List<Entity>();

      if
(database.ContainsKey(tipo))
         lista = database[tipo];
     
else
        
database[tipo] = lista; 

      var id = idProperty.GetValue(item, null);
      Entity trovato = lista.FirstOrDefault(i => idProperty.GetValue(i, null).Equals(id));

      switch
(item.EntityState)
     
{
       
  
case EntityState.Modified: //---- UPDATE
           
trovato = item;
           
break;

         case EntityState.Deleted: //--- c'è il metodo apposito
            break;
            

         default:
            lista.Add(item); //---- INSERT
            break;
     
}

     
return true;
   }
}

 

[3: OfflineDomainClient]
Il "core" della soluzione, usando gli altri 2 oggetti fa lavorare il DomainContext di RiaServices in maniera "trasparente" su dati locali, senza bisogno di avere un servizio WCF.
Di fatto basta andare a fare l'override dei metodi principali come "BeginQueryCore" ed il relativo "EndQueryCore" e "BeginSubmitCore" ed il relativo "EndSubmitCore", come si intuisce facilmente sono Begin../End... perchè si da sempre il supporto alle chiamate Async con Silverlight, anche se in questo caso l'esecuzione è istantanea, senza "latenza" diciamo.

public
class OfflineDomainClient : DomainClient
{
   //--- i vari provider per gli oggetti
   OfflineDataProvider provider = new OfflineDataProvider();


   //--- inizio della query sulla collection
   protected
override IAsyncResult BeginQueryCore(EntityQuery query, AsyncCallback callback, object userState)
   {
      OfflineAsyncResult result = new OfflineAsyncResult() { AsyncState = userState };

      //--- collection in memoria
      IEnumerable coll = provider.GetAll(query.EntityType);

      //--- esegue la query
      result.Entities = OfflineQueryEngine.ExecuteQuery<Entity>(coll,query.Query)


      //--- chiamo subito la fine dell'operazione perchè si aspetta async
      if (callback != null)
         callback.DynamicInvoke(result);

      return result;
   }

   
   //--- fine della query
   protected override QueryCompletedResult EndQueryCore(IAsyncResult asyncResult)
   {
      var result = asyncResult as OfflineAsyncResult;

      return
new QueryCompletedResult(result.Entities, 
                     new List<Entity>(),
                     result.Entities.Count(),
                     new ValidationResult[0]);
   }


   //--- quando viene definita la fine dell'operazione Submit devo inserire/update/delete
   //--- di quello che è stato fatto lato Silverlight
   protected override SubmitCompletedResult EndSubmitCore(IAsyncResult asyncResult)
   {
      var result = asyncResult as OfflineAsyncResult;
      var entities = result.Entities as EntityChangeSet;

      //--- aggiunti
     
foreach (var item in entities.AddedEntities)
         provider.Salva(item);

     
//--- modificati
     
foreach (var item in entities.ModifiedEntities)
         provider.Salva(item);

      //--- cancellati
      foreach (var item in entities.RemovedEntities)
         provider.Delete(item);


      //--- finora ho ancora tutto in memoria, adesso salvo su disco
      //--- con il metodo apposito
      provider.Persist();


      return new SubmitCompletedResult(entities,
                     new List<ChangeSetEntry>() //--- questo fa si che non vengano ricontrollate
                                                //--- vengono prese per buone nella collection, altrimenti ho
                                                /
/--- conflitto di EntityState (es: le trova già aggiunte)
               );

   }


   protected override IAsyncResult BeginSubmitCore(EntityChangeSet changeSet, AsyncCallback callback, object userState)
   {
      OfflineAsyncResult result = new OfflineAsyncResult() { AsyncState = userState };
      result.Entities = changeSet;

      //--- chiamo subito
      if (callback != null)
         callback.DynamicInvoke(result);

      return result;
   }


   //--- classe di appoggio perchè si aspetta tutto asyncrono
   public class OfflineAsyncResult : IAsyncResult
   {
      //--- mi server per parcheggiare i dati tra la chiamata e il callback
      public IEnumerable<Entity> Entities { get; set; }
      public object AsyncState { get; set; }

      public
WaitHandle AsyncWaitHandle { get { retun null; }

      public bool CompletedSynchronously{ get { return false; } }

      public bool IsCompleted { get { return true; } } 
   }
}

[2 : QueryEngine]
Questa classe applica un'Expression ad una collection, senza sapere i tipi precisi, in maniera del tutto dinamica così da poter essere slegata dal model specifico dell'applicazione e riciclabile anche per altri progetti (infatti si sono usati IList e IQueryable) :

public class OfflineQueryEngine
{
   public static IEnumerable ExecuteQuery(IList lista, IQueryable query) 
   {
      Expression expression = query.Expression;

     
//--- safety code
     
if (!(expression is MethodCallExpression))
        
return null;


      //--- mi faccio una collection del tipo corretto per cui è stata scritta
      //--- la Lambda Expression così posso applicarla cambiando l'ExpressionTree
      MethodInfo method = typeof(System.Linq.Enumerable).GetMethod("Cast", new[] { typeof(System.Collections.IEnumerable) });
      MethodInfo generic = method.MakeGenericMethod(query.ElementType);
      IQueryable q = (generic.Invoke(lista, new object[] { lista }) as IEnumerable).AsQueryable();

      ExpressionModifier treeCopier = new ExpressionModifier(q);
      Expression newExpressionTree = treeCopier.Visit(expression);

      return q.Provider.CreateQuery(newExpressionTree).OfType();
   }
   

   //--- con questa classe posso far eseguire un'expression su un mio oggetto passato
   //--- nel costrutture e cambiandolo "sotto il naso" nel metodo VisitConstant
  
class
ExpressionModifier : ExpressionVisitor
   {
      object collection;
      internal ExpressionModifier(object collection)
      {
         this.collection = collection;
      }

      protected override Expression VisitConstant(ConstantExpression c)
      {
         var
val = c.Value;

         //--- questo metodo viene chiamato per vari nodia me interessa quando viene applicato su una collection
         if (c.Type == typeof(EnumerableQuery) || c.Type.BaseType == typeof(EnumerableQuery))
           
val = collection;
         
         return
Expression.Constant(val);
      }
   }
}

Al di la di questa "grezza" mia implementazione del supporto offline, penso che sarebbe utile ci fosse un'implementazione da parte di Microsoft direttamente in RiaServices per queste situazioni, soprattutto in Italia dove la connessione ad internet non è così "assicurata" per non parlare della banda...
Al momento comunque non si sa se verrà tenuta in considerazione per le prossime release del pacchetto, anche se più di qualcuno ho visto ha votato per features di questo tipo:
http://dotnet.uservoice.com/forums/57026-wcf-ria-services/suggestions/749248-offline-support-in-ria-services?ref=title
http://dotnet.uservoice.com/forums/4325-silverlight-feature-suggestions/suggestions/312717-local-database-support-sql-server-compact-sqlite?ref=title

Categoria: Tips
domenica, 19 giu 2011 Ore. 15.39

Messaggi collegati






  • Views Home Page: 190.275
  • Views Posts: 336.547
  • Views Gallerie: 350.750
  • n° Posts: 163
  • n° Commenti: 148
Anno 2014

Anno 2013

Anno 2012

Anno 2011

Anno 2010

Anno 2009

Anno 2008

Anno 2007

Anno 2006

Anno 2005
Copyright © 2002-2007 - Blogs 2.0
dotNetHell.it | Home Page Blogs
ASP.NET 2.0 Windows 2003