Compartir a través de



Noviembre de 2015

Volumen 30, número 12

Programación asincrónica: asincronía desde el inicio

Por Mark Sowul

Las versiones más recientes de Microsoft .NET Framework hacen que la escritura de aplicaciones con capacidad de respuesta y alto rendimiento a través de las palabras clave async y await keywords sea más fácil que nunca. No exagero si digo que estas palabras clave han cambiado por completo la manera en que los desarrolladores de .NET programamos software. El código asincrónico que antes requería una impenetrable red de devoluciones de llamadas anidadas ahora puede escribirse (y comprenderse) casi con la misma facilidad que el código sincrónico y secuencial.

Puesto que ya hay material de muestra de creación y consumo de métodos asincrónicos, daré por sentado que estáis familiarizados con los aspectos más básicos. Si no lo estáis, podéis consultar la página de documentación de Visual Studio msdn.com/async para poneros al día.

La mayoría de la documentación sobre async avisa de que no basta con conectar un método asincrónico en el código existente: el autor de la llamada tiene que ser asincrónico. Según palabras de Lucian Wischik, desarrollador del equipo de lenguaje de Microsoft: “Async es como el virus zombie.” Por lo tanto, ¿cómo compilar async en las aplicaciones justo desde el principio sin recurrir a async void? Durante las refactorizaciones del código de inicio de la interfaz de usuario predeterminada, tanto para Windows Forms como para Windows Presentation Foundation (WPF), les mostraré el proceso de transformación del texto reutilizable de la interfaz de usuario en un diseño orientado a objetos, así como el proceso de agregar compatibilidad con async/await. También explicaré cuando tiene más sentido usar “async void”.

El grueso del presente artículo se centra en Windows Forms, ya que WPF requiere hacer cambios adicionales que podrían distraernos. En cada paso, explicaré los cambios realizados en una aplicación de Windows Forms y abordaré, a continuación, las diferencias para la versión WPF. También mostraré en este artículo todos los cambios de código básico, aunque los ejemplos completos (junto con las revisiones intermedias) para ambos entornos se proporcionan en la descarga de código en línea.

primeros pasos

Las plantillas de Visual Studio para las aplicaciones de Windows Forms y WPF no se prestan al uso de async durante el inicio (o a la personalización del proceso de inicio en general). Aunque C# pretende ser un lenguaje orientado a los objetos (todo el código debe estar en clases), el código de inicio predeterminado anima a los desarrolladores a usar la lógica en los métodos estáticos junto con Main o en un constructor complejo del formulario principal. (No, el acceso a la base de datos dentro del constructor MainForm no es una buena idea, y sí, he visto a desarrolladores hacerlo.) Esta situación siempre ha sido problemática; sin embargo, ahora con async, también sucede que no hay oportunidad clara de hacer que la aplicación se inicie de manera asincrónica.

Para empezar, he creado un nuevo proyecto con la plantilla de aplicación de Windows Forms en Visual Studio. La figura 1 muestra su código de inicio predeterminado en Program.cs.

Figura 1: Código de inicio de Windows Forms predeterminado

static class Program
{
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main()
  {
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);
    Application.Run(new Form1());
  }
}

Con WPF no es tan sencillo. El inicio de WPF predeterminado es bastante opaco. Incluso es difícil buscar código para personalizar. Podemos incluir código de inicialización en Application.OnStartup; sin embargo, ¿cómo retardar la aparición de la interfaz de usuario hasta que se hayan cargado los datos necesarios? Lo primero que hay hacer con WPF es exponer el proceso de inicio como código que se pueda editar. De este modo, conseguiremos llevar a WPF al mismo punto de inicio que el de Windows Forms. Los demás pasos del artículo son similares para ambos casos.

Una vez creada la nueva aplicación de WPF en Visual Studio, crearé una nueva clase denominada Program con el código que se muestra en la figura 2. Para reemplazar la secuencia de inicio predeterminada, hay que abrir las propiedades del proyecto y cambiar el objeto de inicio de “App” a la nueva clase creada “Program.”

Figura 2: Código de inicio equivalente de Windows Presentation Foundation

static class Program
{
  /// <summary>
  /// The main entry point for the application.
  /// </summary>
  [STAThread]
  static void Main()
  {
    App app = new App();
    // This applies the XAML, e.g. StartupUri, Application.Resources
    app.InitializeComponent();
    // Shows the Window specified by StartupUri
    app.Run();
  }
}

Si usamos “Go to Definition” en la llamada a InitializeComponent de la figura 2, el compilador generará el código Main equivalente al uso de App como objeto de inicio (así es como he conseguido abrir la “caja negra”).

Hacia un inicio orientado a objetos

En primer lugar, haré una refactorización del código de inicialización predeterminado para que adquiera un enfoque orientado a objetos: Tomaré la lógica de Main y la pasaré a una clase. Para ello, convertiré a Program en una clase no estática (como he comentado, los valores predeterminados apuntan en una dirección incorrecta) y le asignaré un constructor. A continuación, moveré el código de configuración al constructor y agregaré el método Start que ejecutará mi formulario.

He decidido llamar a la nueva versión Program1. Es la que se muestra en la figura 3. Este esqueleto muestra la idea principal: ejecutar el programa, Main crea un objeto y llama a los métodos, al igual que en cualquier escenario típico orientado a objetos.

Figura 3: Program1, el comienzo de un inicio orientado a objetos

[STAThread]
static void Main()
{
  Program1 p = new Program1();
  p.Start();
}
 
private readonly Form1 m_mainForm;
private Program1()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  m_mainForm = new Form1();
}
 
public void Start()
{
  Application.Run(m_mainForm);
}

Desacoplamiento de la aplicación del formulario

Con todo, la llamada a Application.Run que toma una instancia de formulario (al final, en mi método Start) plantea varios problemas. Uno de los problemas está relacionado con la arquitectura: No me gusta el hecho de que obliga a mi aplicación tenga que mostrar dicho formulario durante toda su duración. Esto no tendría importancia en muchas otras aplicaciones; sin embargo, hay aplicaciones que se ejecutan en segundo plano que no deben mostrar ninguna interfaz de usuario al iniciarse, excepto un icono en la barra de tareas o en el área de notificación. Sé que algunas aplicaciones muestran brevemente una pantalla cuando se inician antes de desaparecer. Apuesto a que su código de inicio sigue un proceso similar y que, a continuación, se ocultan lo antes posible cuando finaliza la carga del formulario. Cierto es que no es necesario resolver este problema en estos momentos, pero la separación es de vital importancia para la inicialización asincrónica.

En lugar de Application.Run(m_mainForm), usaré la sobrecarga de Run que no toma ningún argumento, ya que inicia la infraestructura de la interfaz de usuario sin vincularla a ningún formulario específico. Este desacoplamiento implica que tengo que mostrar el formulario yo mismo. También implica que el cierre del formulario no implicará salir de la aplicación, por lo que es necesario crear dicha conexión de manera explícita, tal como se muestra en la figura 4. Usaré esta oportunidad para agregar mi primer bucle de inicialización. “Initialize” es un método que voy a crear en la clase form para albergar la lógica necesaria para la inicialización como, por ejemplo, la recuperación de datos de una base de datos o sitio web.

Figura 4: Program2 con el bucle de mensaje separado del formulario principal

private Program2()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  m_mainForm = new Form1();
  m_mainForm.FormClosed += m_mainForm_FormClosed;
}
 
void m_mainForm_FormClosed(object sender, FormClosedEventArgs e)
{
  Application.ExitThread();
}
 
public void Start()
{
  m_mainForm.Initialize();
  m_mainForm.Show();
  Application.Run();
}

En la versión para WPF, el elemento StartupUri de la aplicación determina la ventana que debe mostrarse cuando se llama a Run, tal como veremos definido en el archivo de marcado App.xaml. Como era de esperar, la configuración de ShutdownMode predeterminada de la aplicación de OnLastWindowClose cierra la aplicación cuando se cierran todas las ventanas de WPF, así es como se vinculan las duraciones entre sí. (Hay que tener en cuenta que esto difiere de lo que sucede en Windows Forms. En Windows Forms, si la ventana principal abre una ventana secundaria y se cierra la primera ventana, la aplicación también se cerrará. En WPF, la aplicación no se cerrará a menos que se cierren ambas ventanas.)

Para conseguir la misma separación en WPF, primero hay que quitar StartupUri de App.xaml. En su lugar, crearé la ventana yo mismo, la inicializaré y mostraré antes de la llamada a App.Run:

public void Start()
{
  MainWindow mainForm = new MainWindow();
  mainForm.Initialize();
  mainForm.Show();
  m_app.Run();
}

Cuando cree la aplicación, estableceré app.ShutdownMode en ShutdownMode.OnExplicitShutdown para desacoplar la duración de la aplicación de la de las ventanas:

m_app = new App();
m_app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
m_app.InitializeComponent();

Para realizar el cierre específico, adjuntaré un controlador de eventos a MainWindow.Closed.

Puesto que WPF funciona mejor a la hora de separar problemas, es preferible inicializar un modelo de visualización en lugar de la ventana en sí misma: Por lo tanto, crearé la clase MainViewModel para crear el método Initialize ahí. Del mismo modo, la solicitud de cierre de la aplicación debe pasar por el modelo de visualización, por lo que agregaré un evento “CloseRequested” y un método “RequestClose” correspondiente al modelo de visualización. En la figura 5 se muestra la versión de WPF resultante de Program2 (Main no se muestra porque no se ha modificado).

Figura 5: Clase Program2, versión para Windows Presentation Foundation

private readonly App m_app;
private Program2()
{
  m_app = new App();
  m_app.ShutdownMode = ShutdownMode.OnExplicitShutdown;
  m_app.InitializeComponent();
}
 
public void Start()
{
  MainViewModel viewModel = new MainViewModel();
  viewModel.CloseRequested += viewModel_CloseRequested;
  viewModel.Initialize();
 
  MainWindow mainForm = new MainWindow();
  mainForm.Closed += (sender, e) =>
  {
    viewModel.RequestClose();
  };
     
  mainForm.DataContext = viewModel;
  mainForm.Show();
  m_app.Run();
}
 
void viewModel_CloseRequested(object sender, EventArgs e)
{
  m_app.Shutdown();
}

Obtención del entorno de hospedaje

Ahora que he separado Application.Run del formulario, abordaré otro aspecto relacionado con la arquitectura. Tal como está, la aplicación está insertada en la clase Program. Lo que queremos es “sintetizar” el entorno de hospedaje, por decirlo de algún modo. Voy a quitar todos los métodos de Windows Forms en Application de mi clase Program, dejando solo las funciones relacionadas con el programa en sí mismo, tal como se muestra en Program3, en la figura 6. Por último, agregaré un evento en la clase Program para que el vínculo entre el cierre del formulario y el cierre de la aplicación sea menos directo. De este modo, Program3 como clase no interactuará con Application.

Figura 6: Program3, con la modificación para que se pueda conectar en cualquier otro código

private readonly Form1 m_mainForm;
private Program3()
{
  m_mainForm = new Form1();
  m_mainForm.FormClosed += m_mainForm_FormClosed;
}
 
public void Start()
{
  m_mainForm.Initialize();
  m_mainForm.Show();
}
 
public event EventHandler<EventArgs> ExitRequested;
void m_mainForm_FormClosed(object sender, FormClosedEventArgs e)
{
  OnExitRequested(EventArgs.Empty);
}
 
protected virtual void OnExitRequested(EventArgs e)
{
  if (ExitRequested != null)
    ExitRequested(this, e);
}

La separación del entorno de hospedaje reporta varias ventajas. Por un lado, facilita la realización de pruebas (ahora es posible probar Program3 de manera limitada). Por otro lado, permite volver a usar el código en cualquier otro lugar, quizás insertado en una aplicación de mayor tamaño o en una pantalla “de inicio”.

La figura 7 muestra el código Main desacoplado. Tal como se puede observar, he vuelto a incluir la lógica de Application. Este diseño facilita la integración de WPF y Windows Forms o la sustitución gradual de Windows Forms por WPF. Pese a que esto queda fuera del ámbito de este artículo, en el código en línea adjunto se incluye un ejemplo de aplicación mixta. Al igual que con la refactorización anterior, esto reporta ventajas interesantes, aunque no necesariamente vitales: La relevancia de la “tarea en cuestión”, por así decirlo, es que hará que la versión asincrónica fluya con mayor naturalidad.

Figura 7: Código Main, capaz de hospedar un programa arbitrario

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
 
  Program3 p = new Program3();
  p.ExitRequested += p_ExitRequested;
  p.Start();
 
  Application.Run();
}
 
static void p_ExitRequested(object sender, EventArgs e)
{
  Application.ExitThread();
}

La asincronía que tanto hemos esperado

Ahora llega nuestra recompensa: puedo hacer que el método Start sea asincrónico, lo que me permite usar await y conseguir que la lógica de la inicialización sea asincrónica. Para seguir la convención, he renombrado Start a StartAsync e Initialize a InitializeAsync. También he cambiado el tipo de retorno a async Task:

public async Task StartAsync()
{
  await m_mainForm.InitializeAsync();
  m_mainForm.Show();
}

Estos son los cambios de Main para poder usarlo:

static void Main()
{
  ...
  p.ExitRequested += p_ExitRequested;
  Task programStart = p.StartAsync();
 
  Application.Run();
}

Para explicar el funcionamiento, y de paso resolver un problema pequeño pero importante, necesito abordar con detalle qué sucede con async/await.

Significado de await: Tengamos en cuenta el método StartAsync que acabamos de ver. Es importante tener en cuenta que, normalmente, el método async regresa cuando llega a la palabra clave await. El subproceso de ejecución continúa, al igual que sucede cuando regresa cualquier otro método. En este caso, el método StartAsync llega a “await m_mainForm.InitializeAsync” y regresa a Main que, a su vez, continúa con la llamada a Application.Run. Esto lleva al inesperado resultado de que probablemente Application.Run se ejecute antes de m_mainForm.Show, pese a que secuencialmente se produce después de m_mainForm.Show. Async y await facilitan la programación, aunque esto no quiere decir que la programación resulte sencilla.

Por ello los métodos async devuelven Tasks. La finalización de Task es lo que representa la “devolución” del método async en el sentido intuitivo, especialmente cuando todo el código se ha ejecutado. En el caso de StartAsync, esto implica que se ha completado tanto InitializeAsync como m_mainForm.Show. Con esto llegamos al primer problema que plantea el uso de async void: Sin objeto task, el autor de la llamada del método async void no puede saber cuándo ha finalizado la ejecución.

¿Cómo y cuándo se ejecuta el resto del código si el subproceso ha continuado y StartAsync ya se ha devuelto al autor de la llamada? Aquí es donde Application.Run entra en juego. Application.Run es un bucle infinito que permanece a la espera de tareas como, por ejemplo, el procesamiento de eventos de interfaz de usuario. Por ejemplo, cuando se sitúa el mouse sobre una ventana o cuando se hace clic en un botón, el bucle de mensaje Application.Run quitará de la cola el evento y enviará el código correspondiente como respuesta. A continuación, permanecerá a la espera del siguiente evento. Esto no solo se limita a la interfaz de usuario: Consideremos Control.Invoke, que ejecuta una función en el subproceso de la interfaz de usuario. Application.Run también procesa estas solicitudes.

En este caso, cuando finaliza InitializeAsync, el resto del método StartAsync se expondrá a dicho bucle de mensaje. Al usar await, Application.Run ejecutará el resto del método en el subproceso de la interfaz de usuario, como si se hubiera escrito una devolución de llamada mediante Control.Invoke. (ConfigureAwait controla si la continuación debe producirse en el subproceso de la interfaz de usuario. El artículo de Stephen Cleary de marzo de 2013 sobre procedimientos de programación asincrónica recomendados disponible en msdn.com/magazine/jj991977 proporciona más información al respecto).

Por ello es tan importante separar Application.Run de m_mainForm. Application.Run es quien lleva la voz cantante: debe estar ejecutándose para poder procesar el código después de “await,” incluso antes de que se muestre realmente la interfaz de usuario. Por ejemplo, si intentamos mover Application.Run fuera de Main y de nuevo a StartAsync, el programa se cerrará de inmediato: Cuando la ejecución llega a “await InitializeAsync”, el control vuelve a Main y, puesto que no hay más código para ejecutar, finaliza Main.

Esto también explica por qué el uso de async debe ser ascendente. Un antipatrón transitorio común es realizar una llamada a Task.Wait en lugar de a await, ya que el que realiza la llamada no es un método asincrónico pero probablemente lo bloqueará de inmediato. El problema es que la llamada a Wait bloqueará el subproceso de la interfaz de usuario y no podrá procesar la continuación. Sin la continuación, la tarea no se completará, por lo que la llamada a Wait nunca se devolverá: ¡un bloqueo en toda regla!

Await y Application.Run: ¿qué vino antes, el huevo o la gallina? Antes he mencionado que había un pequeño problema. He explicado que cuando se llama a await, el comportamiento predeterminado es el de continuar la ejecución del subproceso de la interfaz de usuario, que es justo lo que necesitamos. Sin embargo, la infraestructura para ello todavía no está configurada cuando se llama por primera vez a await, ya que el código aún no se ha ejecutado.

SynchronizationContext.Current es la clave de este comportamiento: Al llamar a await, la infraestructura captura el valor de SynchronizationContext.Current y lo usa para exponer la continuación. Así es como continúa en el subproceso de la interfaz de usuario. Windows Forms o WPF configuran el contexto de la sincronización cuando se inicia la ejecución del bucle del mensaje. Esto aún no ha sucedido dentro de StartAsync: Si examinamos SynchronizationContext.Current al principio de StartAsync, podremos ver que el valor es nulo. Si no hay contexto de sincronización, await expondrá la continuación al grupo de subprocesos en su lugar y no funcionará, puesto que no será el subproceso de la interfaz de usuario.

La versión de WPF dejará de responder por completo; sin embargo, “por casualidad”, la versión de Windows Forms funcionará. De manera predeterminada, Windows Forms configura el contexto de sincronización cuando se crea el primer control; en este caso, al construir m_mainForm (este comportamiento se controla mediante WindowsFormsSynchronizationContext.AutoInstall). Puesto que “await InitializeAsync” se produce después de crear el formulario, no hay problema. Sin embargo, si usara una llamada a await antes de crear m_mainForm, tendría el mismo problema. La solución consiste en configurar el contexto de sincronización al principio tal como se indica a continuación:

[STAThread]
static void Main()
{
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  SynchronizationContext.SetSynchronizationContext(
    new WindowsFormsSynchronizationContext());
 
  Program4 p = new Program4();
  ... as before
}

Para WPF, la llamada equivalente es la siguiente:

SynchronizationContext.SetSynchronizationContext(
  new DispatcherSynchronizationContext());

Control de excepciones

Ya casi lo tenemos. No obstante, todavía queda otro problema persistente en la raíz de la aplicación: Si InitializeAsync genera una excepción, el programa no podrá controlarla. El objeto de la tarea programStart contendrá la información de la excepción pero no hará nada al respecto, lo que dejará a mi aplicación bloqueada en una especie de purgatorio. Si pudiera usar “await StartAsync,” podría detectar la excepción en Main; sin embargo, no puedo usar await porque Main no es asincrónico.

A continuación se muestra el segundo problema con async void: No existe forma de detectar las excepciones que genera el método async void porque el que realiza la llamada no tiene acceso al objeto de la tarea. (Así que ¿cuándo se debe usar async void? Según las directrices típicas, el uso de async void debe quedar limitado a los controladores de eventos. El artículo de marzo de 2013 mencionado anteriormente también aborda esta cuestión. Por ello es recomendable leerlo para sacar el máximo partido de async/await.)

En circunstancias normales, TaskScheduler.UnobservedException aborda las tareas que generan excepciones que no se controlan posteriormente. El problema está en que la ejecución no está garantizada. En esta situación, lo más seguro es que no se ejecute: el programador de tareas detecta excepciones no observadas cuando finaliza este tipo de tarea. La finalización solo se produce cuando se ejecuta el recolector de elementos no utilizados. El recolector de elementos no utilizados se ejecuta solo cuando debe atender a una solicitud de más memoria.

Probablemente sepáis dónde nos lleva esto: en este caso, la excepción provocará que la aplicación no realice ninguna acción, por lo que no solicitará más memoria y el recolector de elementos no utilizados no se ejecutará. El resultado es que la aplicación dejará de responder. De hecho, esto explica por qué deja de responder la versión de WPF si no se especifica el contexto de la sincronización: el constructor de la ventana de WPF genera una excepción porque se está creando una ventana en un subproceso que no es de interfaz de usuario y, por lo tanto, no se controla la excepción. Esto obliga a abordar la tarea programStart y a agregar una continuación que se ejecutará en caso de error. En este caso, tiene sentido salir si la aplicación no puede inicializarse.

No puedo usar await en Main porque no es asincrónico; sin embargo, puedo crear un nuevo método asincrónico para exponer (y controlar) las excepciones que se produzcan durante la inicialización asincrónica: Este método estará compuesto por las instrucciones try/catch alrededor de await. Puesto que este método controlará todas las excepciones y no generará excepciones nuevas, se trata de otro de los pocos casos en los que es preferible usar async void:

private static async void HandleExceptions(Task task)
{
  try
  {
    await task;
  }
  catch (Exception ex)
  {
    ...log the exception, show an error to the user, etc.
    Application.Exit();
  }
}

Main usa este método de la manera siguiente:

Task programStart = p.StartAsync();
HandleExceptions(programStart);
Application.Run();

Como siempre, hay un pequeño problema (si async/await facilitan las cosas, podéis haceros una idea de lo difícil que era todo antes). Antes he mencionado que normalmente, cuando un método async llega a await, este regresa y el resto del método se ejecuta como una continuación. No obstante, en determinados casos, la tarea puede completarse de manera sincrónica. Si este es el caso, la ejecución del código no se interrumpe, lo que ya supone una ventaja en cuanto a rendimiento. Si esto sucede aquí, quiere decir que el método HandleExceptions se ejecutará por completo y, a continuación, regresará. Esto, a su vez, dará lugar a la ejecución de Application.Run: En este caso, si se produce una excepción, la llamada a Application.Exit tendrá lugar antes de la llamada a Application.Run, por lo que no tendrá ningún efecto.

Mi objetivo es forzar la ejecución de HandleExceptions como se muestra a continuación: necesito estar seguro de que “fracaso” con Application.Run antes de hacer nada más. De este modo, si se produce una excepción, sabré que Application.Run ya se está ejecutando y que Application.Exit interrumpirá correctamente su ejecución. De eso es de lo que se encarga Task.Yield: fuerza la ruta de acceso del código asincrónico actual en favor de su autor de la llamada para, posteriormente, reanudarse como continuación.

A continuación se muestra la corrección de HandleExceptions:

private static async void HandleExceptions(Task task)
{
  try
  {
    // Force this to yield to the caller, so Application.Run will be executing
    await Task.Yield();
    await task;
  }
  ...as before

En este caso, cuando llamo a “await Task.Yield”, HandleExceptions regresará y se ejecutará Application.Run. El resto de HandleExceptions se expondrá como continuación a SynchronizationContext, lo que significa que Application.Run la seleccionará.

Creo que Task.Yield puede servir como prueba de fuego de conocimiento de async/await: si sabéis cómo usar Task.Yield, probablemente tengáis buenos conocimientos sobre el funcionamiento de async/await.

Recompensa

Ahora que todo funciona como es debido, ha llegado el momento de divertirnos. Para ello, voy a mostrar lo fácil que resulta agregar una pantalla de presentación con capacidad de respuesta sin ejecutarla en un subproceso independiente. Independientemente de la diversión, tener una pantalla de presentación es bastante importante si la aplicación no se “inicia” de inmediato: Si el usuario inicia la aplicación y no sucede nada durante varios segundos, la experiencia con la aplicación no será buena.

Iniciar un subproceso independiente para la pantalla de presentación no resulta eficaz ni elegante, ya que tendríais que calcular las referencias de todas las llamadas correctamente entre los subprocesos. Proporcionar información de progreso en la pantalla de presentación resulta, por lo tanto, complejo. El cierre de esta pantalla requiere una llamada a Invoke o a un método equivalente. Además, cuando finalmente se cierra la pantalla de presentación, no suele ceder el foco al formulario principal, ya que no es posible establecer la propiedad entre la pantalla de presentación y el formulario principal si estos se encuentra en distintos subprocesos. Basta comparar todo esto con la sencillez de la versión asincrónica, tal como se puede apreciar en la figura 8.

Figura 8: Adición de una pantalla de presentación en StartAsync

public async Task StartAsync()
{
  using (SplashScreen splashScreen = new SplashScreen())
  {
    // If user closes splash screen, quit; that would also
    // be a good opportunity to set a cancellation token
    splashScreen.FormClosed += m_mainForm_FormClosed;
    splashScreen.Show();
 
    m_mainForm = new Form1();
    m_mainForm.FormClosed += m_mainForm_FormClosed;
    await m_mainForm.InitializeAsync();
 
    // This ensures the activation works so when the
    // splash screen goes away, the main form is activated
    splashScreen.Owner = m_mainForm;
    m_mainForm.Show();
 
    splashScreen.FormClosed -= m_mainForm_FormClosed;
    splashScreen.Close();
  }
}

Resumen

He mostrado cómo aplicar un diseño orientado a objetos en el código de inicialización de las aplicaciones (tanto de Windows Forms como de WPF) para garantizar la compatibilidad con la inicialización asincrónica. También he mostrado cómo superar varios pequeños problemas derivados del proceso de inicialización asincrónica. Hacer que la inicialización de vuestras aplicaciones sea asincrónica es vuestra responsabilidad; sin embargo, en msdn.com/async encontraréis algunas directrices que os resultarán útiles.

Habilitar el uso de async y await es solo el principio. Ahora que Program está más orientado a los objetos, la implementación de otras características resultará mucho más sencilla. Por ejemplo, puedo procesar argumentos de línea de comandos llamando a un método adecuado en la clase Program. Puedo hacer que el usuario tenga que iniciar sesión antes de mostrar la ventana principal. O bien, puedo iniciar la aplicación en el área de notificación sin mostrar ninguna ventana al inicio. Como es habitual, el diseño orientado a los objetos ofrece la oportunidad de ampliar y volver a usar funcionalidades en el código.


Mark Sowulpuede ser una simulación de software escrita en C# (haced vuestras suposiciones). Sowul, desarrollador de .NET comprometido desde el principio, comparte sus conocimientos sobre arquitectura y rendimiento de .NET y Microsoft SQL Server a través de su empresa de consultoría SolSoft Solutions basada en Nueva York. Podéis poneros en contacto con él a través de la dirección mark@solsoftsolutions.com y registraros para recibir sus mensajes de correo electrónico ocasionales sobre perspectivas de software en eepurl.com/_K7YD.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo: Stephen Cleary y James McCaffrey