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