Sto scrivendo il refactoring di un programma Silverlight per consumare una parte della sua logica aziendale esistente da un servizio WCF. In tal modo, ho eseguito la restrizione in Silverlight 3 che consente solo le chiamate asincrone ai servizi WCF per evitare casi in cui le chiamate di servizio di lunga durata o non reattive bloccano il thread dell'interfaccia utente (SL ha un modello di accodamento interessante per richiamare i servizi WCF sul thread dell'interfaccia utente).Insidie di (Mis) utilizzo di Iteratori C# per implementare le Coroutine
Di conseguenza, scrivere ciò che una volta era semplice, sta diventando rapidamente più complesso (vedere gli esempi di codice alla fine della mia domanda).
Idealmente, vorrei usare coroutines per semplificare l'implementazione, ma purtroppo C# non supporta attualmente le coroutine come una funzione di lingua nativa. Tuttavia, C# ha il concetto di generatori (iteratori) usando la sintassi yield return
. La mia idea è di ri-utilizzare la parola chiave yield per permettermi di costruire un modello di coroutine semplice con la stessa logica.
Sono riluttante a farlo, tuttavia, perché sono preoccupato che potrebbero esserci delle insidie (tecniche) nascoste che non sto anticipando (data la mia relativa inesperienza con Silverlight e WCF). Sono anche preoccupato che il meccanismo di implementazione possa non essere chiaro agli sviluppatori futuri e potrebbe ostacolare piuttosto che semplificare i loro sforzi per mantenere o estendere il codice in futuro. Ho visto questa domanda su SO riguardo la riproposizione degli iteratori per costruire macchine a stati: implementing a state machine using the "yield" keyword, e anche se non è esattamente la stessa cosa che sto facendo, mi fa mettere in pausa.
Tuttavia, ho bisogno di fare qualcosa per nascondere la complessità delle chiamate di servizio e gestire lo sforzo e il potenziale rischio di difetti in questo tipo di cambiamento. Sono aperto ad altre idee o approcci che posso utilizzare per risolvere questo problema.
La versione originale non WCF del codice sembra qualcosa di simile:
void Button_Clicked(object sender, EventArgs e) {
using(var bizLogic = new BusinessLogicLayer()) {
try {
var resultFoo = bizLogic.Foo();
// ... do something with resultFoo and the UI
var resultBar = bizLogic.Bar(resultFoo);
// ... do something with resultBar and the UI
var resultBaz = bizLogic.Baz(resultBar);
// ... do something with resultFoo, resultBar, resultBaz
}
}
}
La versione re-factoring WCF diventa un po 'più coinvolti (anche senza la gestione delle eccezioni e pre/post condizioni di prova):
// fields needed to manage distributed/async state
private FooResponse m_ResultFoo;
private BarResponse m_ResultBar;
private BazResponse m_ResultBaz;
private SomeServiceClient m_Service;
void Button_Clicked(object sender, EventArgs e) {
this.IsEnabled = false; // disable the UI while processing async WECF call chain
m_Service = new SomeServiceClient();
m_Service.FooCompleted += OnFooCompleted;
m_Service.BeginFoo();
}
// called asynchronously by SL when service responds
void OnFooCompleted(FooResponse fr) {
m_ResultFoo = fr.Response;
// do some UI processing with resultFoo
m_Service.BarCompleted += OnBarCompleted;
m_Service.BeginBar();
}
void OnBarCompleted(BarResponse br) {
m_ResultBar = br.Response;
// do some processing with resultBar
m_Service.BazCompleted += OnBazCompleted;
m_Service.BeginBaz();
}
void OnBazCompleted(BazResponse bz) {
m_ResultBaz = bz.Response;
// ... do some processing with Foo/Bar/Baz results
m_Service.Dispose();
}
Il codice sopra è ovviamente una semplificazione, in quanto omette la gestione delle eccezioni, controlli nullità, e altre pratiche che sarebbero necessarie in codice di produzione. Ciò nonostante, penso che dimostri il rapido aumento della complessità che inizia a verificarsi con il modello di programmazione WCF asincrono in Silverlight. Il ri-factoring dell'implementazione originale (che non utilizzava un livello di servizio, ma aveva la sua logica incorporata nel client SL) sta rapidamente cercando di essere un compito scoraggiante. E uno che rischia di essere abbastanza incline agli errori.
La versione co-ordinaria del codice sarebbe simile a questa (non ho ancora testato questa):
void Button_Clicked(object sender, EventArgs e) {
PerformSteps(ButtonClickCoRoutine);
}
private IEnumerable<Action> ButtonClickCoRoutine() {
using(var service = new SomeServiceClient()) {
FooResponse resultFoo;
BarResponse resultBar;
BazResponse resultBaz;
yield return() => {
service.FooCompleted = r => NextStep(r, out resultFoo);
service.BeginFoo();
};
yield return() => {
// do some UI stuff with resultFoo
service.BarCompleted = r => NextStep(r, out resultBar);
service.BeginBar();
};
yield return() => {
// do some UI stuff with resultBar
service.BazCompleted = r => NextStep(r, out resultBaz);
service.BeginBaz();
};
yield return() => {
// do some processing with resultFoo, resultBar, resultBaz
}
}
}
private void NextStep<T>(T result, out T store) {
store = result;
PerformSteps(); // continues iterating steps
}
private IEnumerable<Action> m_StepsToPerform;
private void PerformSteps(IEnumerable<Action> steps) {
m_StepsToPerform = steps;
PerformSteps();
}
private void PerformSteps() {
if(m_StepsToPerform == null)
return; // nothing to do
m_StepsToPerform.MoveNext();
var nextStep = m_StepsToPerform.Current;
if(nextStep == null) {
m_StepsToPerform.Dispose();
m_StepsToPerform = null;
return; // end of steps
}
nextStep();
}
Ci sono un sacco di cose che devono essere migliorate nel codice qui sopra. Ma la premessa di base è quella di calcolare il pattern di continuazione (creando un punto di intercettazione per la gestione delle eccezioni e vari controlli) consentendo nel contempo il modello asincrono basato su evento di WCF da guidare quando ogni passo viene eseguito, in pratica quando l'ultima chiamata WCF asincronica completa. Mentre in superficie questo sembra più codice, vale la pena ricordare che PerformSteps()
e NextStep()
sono riutilizzabili, solo l'implementazione in ButtonClickCoRoutine()
cambierebbe con ogni sito di implementazione diverso.
Non sono del tutto sicuro che mi piaccia questo modello, e non sarei sorpreso se esistesse un modo più semplice per implementarlo.Ma non sono stato in grado di trovarne uno su "interwebs" o MSDN o altrove. Grazie in anticipo per l'aiuto.
No buona risposta, se non per dire che ci sentiamo il vostro dolore. Stiamo facendo molte ricerche su come le persone usano l'asincronia su Silverlight e altri quadri. I tipi di trasformazioni che facciamo per rendere gli iteratori simili a coroutine economiche sono un inizio, ma non hanno una generalità sufficiente per risolvere tutti i problemi in questo spazio. Mi piacerebbe sicuramente vedere questo tipo di schema espresso in modo pulito nella lingua, nella stessa maniera in cui esprimiamo in modo pulito i lati generazione (rendimento) e consumo (foreach) del pattern iteratore. –
Immagino che questa domanda spieghi la motivazione per l'aggiunta di asincrona/attesa a C#. –