2011-01-03 18 views
5

Sto progettando un'applicazione desktop con più livelli: il livello GUI (WinForms MVP) contiene i riferimenti alle interfacce delle classi dell'adattatore e questi adattatori chiamano le classi BL che fanno il lavoro effettivo.Design pattern per chiamate asincrone in C#

Oltre all'esecuzione di richieste dalla GUI, BL attiva anche alcuni eventi che la GUI può sottoscrivere tramite le interfacce. Ad esempio, c'è un oggetto CurrentTime nel BL che cambia periodicamente e la GUI dovrebbe riflettere le modifiche.

ci sono due questioni che coinvolgono il multithreading:

  1. ho bisogno di fare un po 'di logica chiamate asincrone in modo che non blocchino la GUI.
  2. Alcuni degli eventi che i recessi della GUI vengono generati da thread non GUI.

A quale livello è meglio gestire il multithreading? La mia intuizione dice che il presentatore è il più adatto per questo, ho ragione? Puoi darmi qualche esempio di applicazione che faccia ciò di cui ho bisogno? E ha senso che il presentatore abbia un riferimento al modulo in modo che possa invocare delegati su di esso?

MODIFICA: La taglia andrà probabilmente a Henrik, a meno che qualcuno non dia una risposta ancora migliore.

risposta

5

Vorrei utilizzare un BLL basato su Task per quelle parti che possono essere descritte come "operazioni in background" (ovvero, vengono avviate dall'interfaccia utente e hanno un punto di completamento definito).Visual Studio Async CTP include un documento che descrive il modello asincrono basato su attività (TAP); Raccomando di progettare la tua API BLL in questo modo (anche se le estensioni della lingua async/await non sono ancora state rilasciate).

Per le parti del vostro BLL che sono "abbonamenti" (cioè, sono iniziate da l'interfaccia utente e continuerà a tempo indeterminato), ci sono alcune opzioni (in ordine di mia preferenza personale):

  1. Utilizzare un'API basata su Task ma con un valore TaskCompletionSource che non viene mai completato (o che solo si completa annullando come parte dello spegnimento dell'applicazione). In questo caso, raccomando di scrivere il proprio IProgress<T> e EventProgress<T> (nel CTP Asincrono): lo IProgress<T> fornisce al BLL un'interfaccia per segnalare lo stato di avanzamento (sostituendo gli eventi di avanzamento) e EventProgress<T> gestisce l'acquisizione del delegato "avanzamento del report" al Thread dell'interfaccia utente.
  2. Utilizzare il framework IObservable di Rx; questo è un buon abbinamento dal punto di vista del design, ma ha una curva di apprendimento abbastanza ripida ed è meno stabile di quanto mi piaccia personalmente (è una libreria preliminare).
  3. Utilizzare il modello asincrono basato su eventi (EAP) vecchio stile, in cui si acquisisce lo SynchronizationContext nel BLL e si aumentano gli eventi accodandoli a tale contesto.

EDIT 2011-05-17: Dopo aver scritto quanto sopra, il team Async CTP ha dichiarato che l'approccio (1) non è raccomandata (dal momento che in qualche modo gli abusi del "progresso" la segnalazione di sistema), e l'Rx il team ha rilasciato documentazione che chiarisce la loro semantica. Ora raccomando Rx per gli abbonamenti.

0

Il modo consigliato è utilizzare i thread sulla GUI e quindi aggiornare i controlli con Control.Invoke().

Se non si desidera utilizzare i thread nell'applicazione della GUI, è possibile utilizzare la classe BackgroundWorker.

La procedura consigliata prevede una logica nei moduli per aggiornare i controlli dall'esterno, normalmente un metodo pubblico. Quando questa chiamata viene effettuata da un thread che non è MainThread, è necessario proteggere gli accessi di thread illegali utilizzando control.InvokeRequired/control.Invoke() (dove control è il controllo target da aggiornare).

Dai un'occhiata a questo esempio AsynCalculatePi, forse è un buon punto di partenza.

+0

So come si comporta Control.Invoke(), ma la domanda riguarda il modello di progettazione e le migliori pratiche. Quale controllo utilizzo per Control.Invoke() - è consigliabile utilizzare il modulo? Significa che ho bisogno di un riferimento al modulo nel mio relatore o invoco Invoke() dal code-behind del modulo? –

+0

@IIya: Ok, ho capito. Dipende dalle circostanze, ma normalmente si scriverà una logica per aggiornare un controllo e si utilizzerà il controllo target per verificare InvokeRequired e quindi control.Invoke(). Vedi le mie modifiche. –

1

Vorrei prendere in considerazione il "Thread Proxy Mediator Pattern". Esempio su CodeProject

Fondamentalmente tutte le chiamate di metodo sugli adattatori vengono eseguite su un thread di lavoro e tutti i risultati vengono restituiti sul thread dell'interfaccia utente.

2

Dipende dal tipo di applicazione che si sta scrivendo - ad esempio - accettate i bug? Quali sono i tuoi dati richiesti: soft realtime? acido? clienti eventualmente coerenti e/o parzialmente connessi/a volte disconnessi?

Attenzione che esiste una distinzione tra concorrenza e asincronizzazione. È possibile avere asincronizzazione e quindi chiamare interleaving del metodo call senza effettivamente avere un programma in esecuzione simultaneo.

Un'idea potrebbe essere quella di avere un lato di lettura e scrittura dell'applicazione, in cui la scrittura pubblica eventi quando è stata modificata. Ciò potrebbe portare a un sistema basato su eventi: il lato di lettura verrebbe creato dagli eventi pubblicati e potrebbe essere ricostruito. L'interfaccia utente potrebbe essere basata su attività - in quanto un'attività da svolgere produrrebbe un comando che il BL prenderà (o livello dominio se lo si desidera).

Un passo logico successivo, se si dispone di quanto sopra, è anche andare di origine evento. Quindi si ricrea lo stato interno del modello di scrittura attraverso ciò che è stato precedentemente commesso. C'è un gruppo di google su CQRS/DDD che potrebbe aiutarti in questo.

Per quanto riguarda l'aggiornamento dell'interfaccia utente, ho trovato che le interfacce IObservable in System.Reactive, System.Interactive, System.CoreEx sono adatte. Permette di saltare diversi contesti di invocazione simultanei - dispatcher - pool di thread, ecc. E si integra bene con la Libreria parallela attività.

Dovresti anche considerare dove mettere la tua logica aziendale - se vai al dominio direi che potresti metterlo nella tua applicazione come avresti una procedura di aggiornamento in atto per i binari che distribuisci comunque, quando arriva il momento di aggiornare, ma c'è anche la scelta di metterlo sul server. I comandi potrebbero essere un buon modo per eseguire gli aggiornamenti sul lato scrittura e una comoda unità di lavoro quando il codice orientato alla connessione fallisce (sono piccoli e serializzabili e l'interfaccia utente può essere progettata attorno ad essi).

Per fare un esempio, dare un'occhiata a this thread, con questo codice, che aggiunge una priorità per l'IObservable.ObserveOnDispatcher (...) - chiamano:

public static IObservable<T> ObserveOnDispatcher<T>(this IObservable<T> observable, DispatcherPriority priority) 
    { 
     if (observable == null) 
      throw new NullReferenceException(); 

     return observable.ObserveOn(Dispatcher.CurrentDispatcher, priority); 
    } 

    public static IObservable<T> ObserveOn<T>(this IObservable<T> observable, Dispatcher dispatcher, DispatcherPriority priority) 
    { 
     if (observable == null) 
      throw new NullReferenceException(); 

     if (dispatcher == null) 
      throw new ArgumentNullException("dispatcher"); 

     return Observable.CreateWithDisposable<T>(o => 
     { 
      return observable.Subscribe(
       obj => dispatcher.Invoke((Action)(() => o.OnNext(obj)), priority), 
       ex => dispatcher.Invoke((Action)(() => o.OnError(ex)), priority), 
       () => dispatcher.Invoke((Action)(() => o.OnCompleted()), priority)); 
     }); 
    } 

L'esempio potrebbe essere utilizzato ti piace questa blog entry discute

public void LoadCustomers() 
{ 
    _customerService.GetCustomers() 
     .SubscribeOn(Scheduler.NewThread) 
     .ObserveOn(Scheduler.Dispatcher, DispatcherPriority.SystemIdle) 
     .Subscribe(Customers.Add); 
} 

... Così, per esempio con un negozio starbucks virtuale, si avrebbe un'entità di dominio che ha qualcosa come una classe di 'Barista', che produce eventi 'CustomerBoughtCappuccino': {costo: "$ 3", timestamp: "2011-01-03 12: 00: 03.334556 GMT + 0100 ', ... ecc.}. Il tuo read-side si iscriverebbe a questi eventi. Il lato di lettura potrebbe essere un modello di dati - per ciascuna delle schermate che presentano i dati - la vista avrebbe una classe ViewModel unica - che sarebbe sincronizzata con la vista in un dizionario osservabile like this. Il repository sarebbe (: IObservable), ei tuoi relatori si iscriveranno a tutto questo, o solo a una parte di esso. In questo modo la vostra interfaccia grafica potrebbe essere:

  1. Task guidato -> Comando guidato BL, con particolare attenzione alla gestione degli utenti
  2. Async
  3. lettura-scrittura-segregato

Dato che il vostro BL richiede solo comandi e non in cima a quello visualizza 'abbastanza buono per tutte le pagine'-tipo di modello di lettura, è possibile rendere la maggior parte delle cose in esso interne, interne protette e private, il che significa che è possibile utilizzare System.Contracts per dimostrare che non ho bug (!). Produrrebbe eventi che il tuo modello di lettura leggerà. È possibile prendere i principi principali da Caliburn Micro sull'orchestrazione dei flussi di lavoro delle attività asincrone cedute (IAsyncResults).

Ci sono alcuni Rx design guidelines che potresti leggere. E cqrsinfo.com su event sourcing e cqrs. Se sei effettivamente interessato ad andare oltre la sfera di programmazione asincrona nella sfera della programmazione simultanea, Microsoft ha rilasciato gratuitamente un po 'di written book, su come programmare tale codice.

Spero che aiuti.

+0

Grazie. Mi hai dato abbastanza indicazioni per passare un mese a leggere tutto questo :) –

+0

Sono contento;). Mandami un PM se hai bisogno di un tutor personale. Faccio consulenza. – Henrik