2011-01-03 23 views
5

Estoy diseñando una aplicación de escritorio con varias capas: la capa de GUI (WinForms MVP) contiene referencias a interfaces de clases de adaptadores, y estos adaptadores llaman clases BL que hacen el trabajo real.Patrón de diseño para llamadas asincrónicas en C#

Además de ejecutar solicitudes desde la GUI, el BL también activa algunos eventos a los que la GUI puede suscribirse a través de las interfaces. Por ejemplo, hay un objeto CurrentTime en el BL que cambia periódicamente y la GUI debe reflejar los cambios.

Hay dos cuestiones que involucran a múltiples hilos:

  1. Necesito hacer parte de la lógica llamadas asíncronas para que no bloqueen la GUI.
  2. Algunos de los eventos que reciben las notificaciones GUI provienen de subprocesos que no pertenecen a la GUI.

¿A qué nivel es mejor manejar el multihilo? Mi intuición dice que el Presentador es el más adecuado para eso, ¿estoy en lo cierto? ¿Me puede dar alguna aplicación de ejemplo que haga lo que necesito? ¿Y tiene sentido que el presentador haga una referencia al formulario para que pueda invocar delegados sobre él?

EDITAR: La recompensa probablemente irá a Henrik, a menos que alguien dé una respuesta aún mejor.

Respuesta

5

Me gustaría utilizar una BLL basada en Task para aquellas partes que pueden describirse como "operaciones en segundo plano" (es decir, que están iniciadas por la IU y tienen un punto de finalización definido).El Visual Studio Async CTP incluye un documento que describe el Patrón asincrónico basado en tareas (TAP); Recomiendo diseñar su API de BLL de esta manera (aunque las extensiones de idioma async/await no se han lanzado todavía).

Para partes de su BLL que son "suscripciones" (es decir, que están iniciadas por la interfaz de usuario y continuar indefinidamente), hay algunas opciones (por orden de mi preferencia personal):

  1. Use una API basada en Task pero con un TaskCompletionSource que nunca se completa (o que se completa cancelando como parte del cierre de la aplicación). En este caso, recomiendo escribir su propio IProgress<T> y EventProgress<T> (en el Async CTP): el IProgress<T> le da a su BLL una interfaz para reportar el progreso (reemplazando eventos de progreso) y EventProgress<T> manejando la SynchronizationContext para coordinar el delegado de "informe de progreso" al Hilo de interfaz de usuario.
  2. Utilice el marco IObservable de Rx; este es un buen partido en cuanto al diseño, pero tiene una curva de aprendizaje bastante empinada y es menos estable de lo que personalmente me gusta (es una biblioteca de prelanzamiento).
  3. Utilice el Patrón asincrónico basado en eventos (EAP) anticuado, donde captura el SynchronizationContext en su BLL y levanta eventos al ponerlos en cola en ese contexto.

EDITAR 2011-05-17: Después de haber escrito lo anterior, el equipo asíncrono CTP ha declarado que el enfoque (1) no está recomendada (ya un tanto abusa de la "presentación de informes de progreso" del sistema), y la Rx el equipo ha publicado documentación que aclara su semántica. Ahora recomiendo Rx para las suscripciones.

0

La manera recomendada es usar hilos en la GUI, y luego actualizar los controles con Control.Invoke().

Si no desea utilizar subprocesos en su aplicación GUI, puede utilizar la clase BackgroundWorker.

La mejor práctica es tener cierta lógica en sus Formularios para actualizar sus controles desde el exterior, normalmente un método público. Cuando esta llamada se realiza desde un subproceso que no es el MainThread, debe proteger los accesos de subprocesos ilegales usando control.InvokeRequired/control.Invoke() (donde el control es el control de destino para actualizar).

Eche un vistazo a este ejemplo AsynCalculatePi, tal vez es un buen punto de partida.

+0

Sé cómo se comporta Control.Invoke(), pero la pregunta es sobre el patrón de diseño y sobre las mejores prácticas. ¿Qué control utilizo para Control.Invoke()? ¿Es una mejor práctica usar el formulario? ¿Significa que necesito una referencia al formulario en mi presentador, o llamo a Invoke() desde el código subyacente del formulario? –

+0

@IIya: Ok, lo entiendo. Depende de las circunstancias, pero normalmente escribirá una lógica para actualizar un control, y usará el control objetivo para verificar InvokeRequired y luego control.Invoke(). Ver mis ediciones –

1

Consideraría el "Patrón del mediador proxy de subprocesos". Ejemplo aquí en CodeProject

Básicamente todas las llamadas de método en sus adaptadores se ejecutan en un subproceso de trabajo y todos los resultados se devuelven en el subproceso de la interfaz de usuario.

2

Depende del tipo de aplicación que está escribiendo, por ejemplo, ¿acepta errores? ¿Cuáles son sus requisitos de datos, en tiempo real? ¿ácido? clientes eventualmente consistentes y/o parcialmente conectados/a veces desconectados?

Tenga en cuenta que existe una distinción entre concurrencia y asincronía. Puede tener asynchronocity y, por lo tanto, invocar el intercalado de llamadas de método sin tener realmente un programa de ejecución simultánea.

Una idea podría ser tener un lado de lectura y escritura de la aplicación, donde el lado de escritura publica eventos cuando se ha modificado. Esto podría conducir a un sistema impulsado por eventos: el lado de lectura se generaría a partir de los eventos publicados y podría reconstruirse. La interfaz de usuario podría ser impulsada por tareas, en el sentido de que una tarea para realizar produciría un comando que su BL tomaría (o capa de dominio si así lo desea).

Un siguiente paso lógico, si tiene lo anterior, es ir también de origen de evento. Luego, recrearías el estado interno del modelo de escritura a través de lo que se había comprometido previamente. Hay un grupo de google sobre CQRS/DDD que podría ayudarte con esto.

Con respecto a la actualización de la IU, he encontrado que las interfaces IObservable en System.Reactive, System.Interactive, System.CoreEx son adecuadas. Le permite omitir diferentes contextos de invocación simultáneos: despachador, grupo de subprocesos, etc., e interopera bien con la Biblioteca de tareas paralelas.

También debería considerar dónde ubica su lógica comercial: si utiliza el dominio, le diría que podría ponerlo en su aplicación, ya que tendría un procedimiento de actualización para los binarios que distribuye De todos modos, cuando llega el momento de actualizar, también existe la opción de ponerlo en el servidor. Los comandos pueden ser una buena manera de realizar las actualizaciones en el lado de escritura y una unidad de trabajo conveniente cuando el código orientado a la conexión falla (son pequeños y serializables y la interfaz de usuario se puede diseñar a su alrededor).

Para dar un ejemplo, echar un vistazo a this thread, con este código, que añade una prioridad a la IObservable.ObserveOnDispatcher (...) - llaman:

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)); 
     }); 
    } 

El ejemplo anterior se podría utilizar como esto blog entry discute

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

... Así, por ejemplo, con una tienda de starbucks virtual, que tendría una entidad de dominio que tiene algo así como una clase 'barista', que produce eventos 'CustomerBoughtCappuccino': {costos: '$ 3', marca de tiempo: '2011-01-03 12: 00: 03.334556 GMT + 0100 ', ... etc}. Su lado de lectura se suscribiría a estos eventos. El lado de lectura podría ser un modelo de datos: para cada una de las pantallas que presentan datos, la vista tendría una clase de modelo de vista única, que se sincronizaría con la vista en un diccionario observable like this. El repositorio sería (: IObservable), y sus presentadores se suscribirían a todo eso, o solo a una parte de él. De esa manera su interfaz gráfica de usuario podría ser:

  1. tarea impulsada -> comando impulsado BL, con especial atención en las operaciones del usuario
  2. asíncrono
  3. lectura-escritura-segregada

Teniendo en cuenta que el BL sólo toma comandos y no muestra un modelo de lectura lo suficientemente bueno para todas las páginas, puede hacer que la mayoría de las cosas en él sean internas, internas y privadas, lo que significa que puede usar System.Contracts para demostrar que no tiene ningún error en él (!). Produciría eventos que leería su modelo de lectura. Puede tomar los principios principales de Caliburn Micro sobre la orquestación de flujos de trabajo de tareas asincrónicas entregadas (IAsyncResults).

Hay algunos Rx design guidelines que puede leer. Y cqrsinfo.com sobre fuentes de eventos y cqrs. Si de verdad está interesado en ir más allá de la esfera de programación asincrónica en la esfera de programación concurrente, Microsoft ha lanzado un pozo written book gratis, sobre cómo programar dicho código.

Espero que ayude.

+0

Gracias. Me has dado suficientes consejos para pasar un mes leyendo todo esto :) –

+0

Estoy contento;). Envíame un PM si necesitas un tutor personal. Yo consulto. – Henrik

Cuestiones relacionadas