Soluciones para la reincidencia asíncrona C # 5

16

Entonces, algo me ha estado molestando sobre el nuevo soporte asíncrono en C # 5:

El usuario presiona un botón que inicia una operación asíncrona. La llamada regresa inmediatamente y la bomba de mensajes comienza a funcionar nuevamente; ese es el punto.

Por lo tanto, el usuario puede presionar el botón nuevamente, lo que provoca un reencuentro. ¿Qué pasa si esto es un problema?

En las demostraciones que he visto, deshabilitan el botón antes de la awaitllamada y lo habilitan nuevamente después. Esto me parece una solución muy frágil en una aplicación del mundo real.

¿Deberíamos codificar algún tipo de máquina de estado que especifique qué controles deben deshabilitarse para un conjunto dado de operaciones en ejecución? ¿O hay un mejor camino?

Estoy tentado de mostrar un diálogo modal durante la operación, pero esto se parece un poco a usar un mazo.

¿Alguien tiene alguna idea brillante, por favor?


EDITAR:

Creo que deshabilitar los controles que no deberían usarse mientras se ejecuta una operación es frágil porque creo que se volverá complejo rápidamente cuando tenga una ventana con muchos controles. Me gusta simplificar las cosas porque reduce la posibilidad de errores, tanto durante la codificación inicial como en el mantenimiento posterior.

¿Qué sucede si hay una colección de controles que deberían deshabilitarse para una operación en particular? ¿Y qué pasa si se ejecutan varias operaciones simultáneamente?

Nicholas Butler
fuente
¿Qué tiene de frágil? Le da al usuario una indicación de que algo se está procesando. Agregar un poco de indicación dinámica se está procesando y eso me parece una interfaz de usuario perfectamente buena.
Steven Jeuris
PD: ¿C # de .NET 4.5 se llama C # 5?
Steven Jeuris
1
¿Solo curiosidad por qué esta pregunta está aquí y no TAN? Esto definitivamente se trata de código, por lo que pertenecería allí, creo ...
C. Ross
@ C.Ross, esta es más una pregunta de diseño / UI. No hay realmente un punto técnico que deba explicarse aquí.
Morgan Herlocker
Tenemos un problema similar en los formularios HTML ajax donde un usuario presiona el botón enviar, se envía una solicitud ajax y luego queremos bloquear al usuario para que no vuelva a presionar enviar, deshabilitar el botón es una solución muy común en esa situación
Raynos

Respuestas:

14

Entonces, algo me ha estado molestando sobre el nuevo soporte asíncrono en C # 5: el usuario presiona un botón que inicia una operación asíncrona. La llamada regresa inmediatamente y la bomba de mensajes comienza a funcionar nuevamente; ese es el punto. Por lo tanto, el usuario puede presionar el botón nuevamente, lo que provoca un reencuentro. ¿Qué pasa si esto es un problema?

Comencemos señalando que esto ya es un problema, incluso sin soporte asíncrono en el idioma. Muchas cosas pueden hacer que los mensajes se retiren mientras se maneja un evento. Usted ya tiene a la defensiva código para esta situación; la nueva característica del lenguaje solo lo hace más obvio , que es la bondad.

La fuente más común de un bucle de mensajes que se ejecuta durante un evento que causa reencuentro no deseado es DoEvents. Lo bueno de lo awaitcontrario DoEventses que awaithace que sea ​​más probable que las nuevas tareas no vayan a "matar de hambre" las tareas que se ejecutan actualmente. DoEventsbombea el bucle de mensajes y, a continuación, invoca sincrónicamente el controlador de eventos reentrante, mientras que awaitnormalmente pone en cola la nueva tarea para que se ejecute en algún momento en el futuro.

En las demostraciones que he visto, deshabilitan el botón antes de la llamada en espera y lo habilitan nuevamente después. Creo que deshabilitar los controles que no deberían usarse mientras se ejecuta una operación es frágil porque creo que rápidamente se volverá complejo cuando tenga una ventana con muchos controles. ¿Qué sucede si hay una colección de controles que deberían deshabilitarse para una operación en particular? ¿Y qué pasa si se ejecutan varias operaciones simultáneamente?

Puede administrar esa complejidad de la misma manera que manejaría cualquier otra complejidad en un lenguaje OO: abstrae los mecanismos en una clase y hace que la clase sea responsable de implementar correctamente una política . (Trate de mantener los mecanismos lógicamente distintos de la política; debería ser posible cambiar la política sin modificar demasiado los mecanismos).

Si tiene un formulario con muchos controles que interactúan de manera interesante, entonces está viviendo en un mundo de complejidad creado por usted mismo . Tienes que escribir código para gestionar esa complejidad; si no te gusta, simplifica el formulario para que no sea tan complejo.

Estoy tentado de mostrar un diálogo modal durante la operación, pero esto se parece un poco a usar un mazo.

Los usuarios odiarán eso.

Eric Lippert
fuente
Gracias Eric, supongo que esta es la respuesta definitiva: depende del desarrollador de la aplicación.
Nicholas Butler
6

¿Por qué crees que deshabilitar el botón antes awaity luego volver a habilitarlo cuando finaliza la llamada es frágil? ¿Es porque le preocupa que el botón nunca se vuelva a habilitar?

Bueno, si la llamada no regresa, entonces no puede volver a hacer la llamada, por lo que parece que este es el comportamiento que desea. Esto indica un estado de error que debería corregir. Si se agota el tiempo de espera de su llamada asíncrona, esto podría dejar su aplicación en un estado indeterminado, de nuevo en el que tener un botón habilitado podría ser peligroso.

El único momento en que esto podría complicarse es si hay varias operaciones que afectan el estado de un botón, pero creo que esas situaciones deberían ser muy raras.

ChrisF
fuente
Por supuesto, debes manejar los errores. He actualizado mi pregunta
Nicholas Butler
5

No estoy seguro de si esto se aplica a usted, ya que generalmente uso WPF, pero veo que hace referencia a una ViewModelpara que así sea.

En lugar de deshabilitar el botón, establezca una IsLoadingbandera en true. Entonces, cualquier elemento de la interfaz de usuario que desee deshabilitar puede vincularse a la bandera. El resultado final es que se convierte en el trabajo de la interfaz de usuario ordenar su propio estado habilitado, no su lógica de negocios.

Además de eso, la mayoría de los botones en WPF están vinculados a un ICommand, y generalmente configuro el ICommand.CanExecuteigual a !IsLoading, por lo que automáticamente evita que el comando se ejecute cuando IsLoading es igual a verdadero (también deshabilita el botón para mí)

Rachel
fuente
3

En las demostraciones que he visto, deshabilitan el botón antes de la llamada en espera y lo habilitan nuevamente después. Esto me parece una solución muy frágil en una aplicación del mundo real.

No hay nada frágil en esto, y es lo que el usuario esperaría. Si el usuario necesita esperar antes de presionar el botón nuevamente, haga que esperen.

Puedo ver cómo ciertos escenarios de control podrían hacer que decidir qué controles deshabilitar / habilitar en cualquier momento sea bastante complejo, pero ¿ es sorprendente que administrar una IU compleja sea complejo? Si tiene demasiados efectos secundarios atemorizantes de un control en particular, siempre puede deshabilitar todo el formulario y lanzar un "zumbido" sobre todo. Si este es el caso en cada control, entonces su IU probablemente no esté muy bien diseñada en primer lugar (acoplamiento apretado, agrupación de control deficiente, etc.).

Morgan Herlocker
fuente
Gracias, pero ¿cómo gestionar la complejidad?
Nicholas Butler
Una opción sería poner controles relacionados en el contenedor. Deshabilitar / habilitar por contenedor de control, no por control. es decir: EditorControls.Foreach (c => c.Enabled = false);
Morgan Herlocker
2

Deshabilitar el widget es la opción menos compleja y propensa a errores. Lo deshabilita en el controlador antes de comenzar la operación asíncrona y lo vuelve a habilitar al final de la operación. Por lo tanto, el deshabilitar + habilitar para cada control se mantiene local para el manejo de ese control.

Incluso puede crear un contenedor, que tomará un delegado y control (o más de ellos), deshabilitará los controles y ejecutará algo de forma asincrónica, que hará el delegado y luego volverá a habilitar los controles. Debe ocuparse de las excepciones (habilite finalmente), por lo que aún funciona de manera confiable si la operación falla. Y puede combinarlo con agregar mensajes en la barra de estado, para que el usuario sepa lo que está esperando.

Es posible que desee agregar algo de lógica para contar las deshabilitaciones, por lo que el sistema sigue funcionando si tiene dos operaciones, que deberían deshabilitar una tercera, pero que no son mutuamente excluyentes. Deshabilita el control dos veces, por lo que volverá a habilitarse solo si lo vuelve a habilitar dos veces. Eso debería cubrir la mayoría de los casos mientras se mantienen separados tanto como sea posible.

Por otro lado, cualquier máquina de estado se volverá compleja. En mi experiencia, todas las máquinas de estado que vi fueron difíciles de mantener y su máquina de estado necesitaría abarcar todo el diálogo, uniendo todo a todo.

Jan Hudec
fuente
Gracias, me gusta esta idea. Entonces, ¿tendría un objeto administrador para su ventana que cuente el número de solicitudes de activación y desactivación para cada control?
Nicholas Butler
@NickButler: Bueno, ya tienes un objeto de administrador para cada ventana: el formulario. Entonces tendría una clase implementando las solicitudes y contando y solo agregaría instancias a los formularios relevantes.
Jan Hudec
1

Aunque Jan Hudec y Rachel han expresado ideas relacionadas, sugeriría usar algo como una arquitectura de vista de modelo : expresar el estado del formulario en un objeto y vincular los elementos del formulario a ese estado. Esto deshabilitaría el botón de una manera que pueda escalar para escenarios más complejos.

psr
fuente