Novembro de 2015

Volume 30, Número 12

Programação Assíncrona - Assíncrona desde o Início

Por Mark Sowul

A versões recentes do Microsoft .NET Framework facilitam mais do que nunca a gravação de aplicativos ágeis na resposta e de alto desempenho por meio das palavras-chave async e await – não é exagero dizer que mudamos o modo como nós, desenvolvedores .NET, gravamos software. O código assíncrono usado para exibir uma Web impenetrável de retornos de chamada aninhados agora pode ser gravado (e entendido) quase que tão facilmente quanto o código sequencial e sincronizado.

Já existe um amplo material sobre métodos de criação e consumo de métodos assíncronos; portanto, pressuponho que você já esteja familiarizado com o básico. Se você não está familiarizado, a página de Documentação do Visual Studio em msdn.com/async pode ajudar você.

A maioria da documentação sobre assincronia alerta que você não pode somente conectar um método assíncrono em um código existente; o autor da chamada em si precisa ser assíncrono. Nas palavras de Lucian Wischik, um desenvolvedor da equipe de linguagem da Microsoft: “Assincronia é como o vírus de zumbis”. Como você compila o async na malha do seu aplicativo desde o começo, sem reclassificar para async void? Vou mostrar isso durante o curso de diversas refatorações do código de inicialização da interface do usuário padrão, tanto para Windows Forms quanto para WPF (Windows Presentation Foundation), transformando o clichê da interface do usuário em um design orientado a objeto e adicionando suporte a async/await. Também explicarei quando faz/não faz sentido usar “async void”.

Neste artigo, a explicação principal concentra-se em Windows Forms; o WPF exige alterações adicionais, que podem sair do foco. Em cada etapa, explicarei primeiro as alterações com um aplicativo do Windows Forms e, em seguida, discutirei as diferenças necessárias para a versão do WPF. Mostro todas as alterações de código básicas nesse artigo, mas você pode encontrar exemplos completos (e revisões intermediárias) para ambos os ambientes no download de código online em anexo.

Primeiras etapas

Os modelos do Visual Studio para aplicativos do Windows Forms e WPF não os empresta para usar assincronia durante a inicialização (ou para personalizar o processo de inicialização, no geral). Apesar de C# se esforçar para ser uma linguagem orientada a objeto – todo o código precisa estar em classes – o código de inicialização padrão chama a atenção dos desenvolvedores para colocar uma lógica nos métodos estáticos com o Main ou, em um construtor extremamente complicado, para o formulário principal. (Não, não é uma boa ideia acessar o banco de dados dentro do construtor MainForm. Sim, já vi fazerem isso.) Esta situação sempre foi problemática, mas agora com a assincronia, também significa que não há uma oportunidade clara para que o aplicativo seja inicializado por conta própria de modo assíncrono.

Para começar, criei um novo projeto com o modelo do Aplicativo do Windows Forms no Visual Studio. A Figura 1 mostra seu código de inicialização padrão em Program.cs.

Figura 1 - Código de Inicialização do Windows Forms Padrão

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

Não é fácil com o WPF. A inicialização WPF padrão é opaca, mesmo acreditando que a personalização de qualquer código seja difícil. É possível colocar um código de inicialização em Application.OnStartup, mas como você atrasaria a exibição da interface do usuário até carregar os dados necessários? A primeira coisa que preciso fazer com o WPF é expor o processo de inicialização como um código que posso editar. Levarei o WPF para o mesmo ponto inicial do Windows Forms e, então, cada etapa do artigo será similar para as duas opções.

Depois de criar um novo aplicativo WPF no Visual Studio, crio uma nova classe, denominada Programa, com o código na Figura 2. Para substituir a sequência de inicialização padrão, abra as propriedades do projeto e altere o objeto de inicialização de “Aplicativo” para o “Programa”, criado recentemente.

Figura 2 - Código de Inicialização do Windows Presentation Foundation Equivalente

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

Se você usar “Ir para Definição” na chamada para InitializeComponent na Figura 2, você notará que o compilador gera o código Main equivalente de quando você usa Aplicativo como o objeto de inicialização (que é a forma como abro a “caixa preta” para você aqui).

Em Direção a uma Inicialização Orientada a Objeto

Primeiro, farei uma pequena refatoração do código de inicialização padrão para impulsionar em uma direção orientada a objeto: vou pegar a lógica de Main e movê-la para uma classe. Para fazer isso, tornarei Programa uma classe não estatística (como disse, os padrões impulsionados para a direção errada) e atribuirei um construtor a ele. Em seguida, levarei o código de configuração para o construtor e adicionarei um método de Início que executará meu formulário.

Chamei a nova versão de Programa1 e você pode visualizá-la na Figura 3. Este esqueleto mostra o núcleo da ideia: para executar o programa, Main agora cria um objeto e chama métodos nele, assim como em um cenário típico orientado a objeto.

Figure 3 - Programa1, Começo de uma Inicialização Orientada a Objeto

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

Desacoplando o Aplicativo do Formulário

Apesar de tudo, a chamada para Application.Run que usa uma instância de formulário (no final, no meu método de Início) impõe alguns problemas. O primeiro é um problema arquitetônico genérico: não gosto que ele liga o tempo de vida do meu aplicativo à exibição do formulário. Isso não seria um problema para diversos aplicativos, mas há aqueles que executam em segundo plano que não devem exibir nenhuma interface do usuário ao iniciar, exceto, talvez, por um ícone na barra de tarefas ou na área de notificação. Já vi alguns que piscam a tela, rapidamente, ao iniciar, antes de desaparecer. Aposto que o seu código de inicialização segue um processe similar e, em seguida, é ocultado o quanto antes, quando o formulário termina de carregar. Reconheço que este problema específico não deve ser, necessariamente, solucionado aqui, mas a separação será de suma importância para uma inicialização assíncrona.

Em vez de Application.Run(m_mainForm), usarei a sobrecarga de Execução que não usa um argumento: ela inicia a infraestrutura de interface do usuário sem ligá-la a nenhum formulário específico. A desacoplagem significa que tenho que mostrar o formulário por conta própria; além disso, quer dizer que fechar o formulário não encerrará mais o aplicativo; portanto, preciso transferir isso de modo explícito também, como mostrado na Figura 4. Também usarei esta oportunidade para adicionar um gancho com inicialização. “Inicializar” é um método que estou criando na minha classe de formulário para reter qualquer lógica necessária para inicializá-la, como recuperar dados de um banco de dados ou de um site.

Figura 4 - Programa2, Loop de Mensagens Agora Separado do Formulário 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();
}

Na versão do WPF, o StartupUri do aplicativo determina qual janela deve ser exibida quando uma Execução é chamada; você o encontrará definido no arquivo de marcação App.xaml. Não é surpresa que a configuração ShutdownMode padrão do Aplicativo de OnLastWindowClose feche o aplicativo quando todas as janelas de WPF são fechadas, portanto, é assim que os tempos de vida são ligados uns aos outros. (Observe que isso é diferente do Windows Forms. No Windows Forms, se a janela principal abrir uma janela filho e você fechar somente a primeira janela, o aplicativo será fechado. No WPF, ele não sai, a menos que fechar ambas as janelas.)

Para chegar à mesma separação no WPF, primeiro removo o StartupUri do App.xaml. Em vez disso, crio a janela por contra própria, a inicializo e mostro antes da chamada para App.Run:

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

Quando crio o aplicativo, defino app.ShutdownMode para ShutdownMode.OnExplicitShutdown, o que desacopla o tempo de vida do aplicativo daquele das janelas:

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

Para obter um desligamento explícito, anexarei um manipulador de eventos para MainWindow.Closed.

Claro que o WPF opera melhor ao separar os problemas, portanto, faz mais sentido inicializar um modelo de exibição em vez de a janela em si: criarei uma classe MainViewModel e meu método Inicializar neste local. Da mesma forma, a solicitação para fechar o aplicativo também deve passar pelo modelo de exibição, portanto, vou adicionar um evento “CloseRequested” e um método “RequestClose” correspondente ao modelo de exibição. A versão do WPF resultante do Programa2 está listada na Figura 5 (Main não é alterado, por isso, não vou mostrá-lo aqui).

Figura 5 - Classe de Programa2, Versão do 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();
}

Extraindo o Ambiente de Hospedagem

Agora que separei o Application.Run do meu formulário, quero abordar outra consideração arquitetônica. No momento, o Aplicativo está profundamente inserido na classe de Programa. Quero “abstrair” este ambiente de hospedagem, digamos assim. Vou remover todos os diversos métodos do Windows Forms no Aplicativo por meio da minha classe de Programa, deixando somente a funcionalidade relacionada ao programa em si, como mostrado no Programa3 na Figura 6. Uma última parte é adicionar um evento à classe de programa, assim o vínculo entre fechar o formulário e desligar o aplicativo é menos direto. Observe como o Programa 3, como uma classe, não tem interação com o Aplicativo!

Figura 6 - Programa3, Agora é Fácil Conectar-se em Outros Lugares

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

Separar o ambiente de hospedagem conta com alguns benefícios. Em especial, facilita os testes (agora, você pode testar Programa3 até uma extensão limitada). Ele também facilita a reutilização de código em outro local, provavelmente inserido em um aplicativo maior ou em uma tela de “iniciador”.

O Main desacoplado é mostrado na Figura 7 – movi a lógica do Aplicativo de volta para o lugar. Este design facilita a integração do WPF e do Windows Forms ou, talvez, para substituir gradualmente o Windows Forms pelo WPF. Isso está fora do escopo deste artigo, mas é possível encontrar um exemplo de um aplicativo misto no código online em anexo. Em relação à refatoração anterior, esses itens são recomendados, mas não são necessariamente cruciais: a relevância da “Tarefa em mãos”, a propósito, é o que vai fazer com que a versão assíncrona flua mais naturalmente, como notará em breve.

Figura 7 - Main, Agora Capaz de Hospedar Um Programa Arbitrário

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

Assincronia Aguardada

Por fim, a recompensa. Posso tornar o método Início assíncrono, o que me permite usar e tornar minha lógica de inicialização assíncrona. Por convenção, renomeei Início para StartAsync e Inicializar para InitializeAsync. Também alterei o tipo de retorno para Tarefa assíncrona:

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

Para usá-lo, Main também é alterado de forma semelhante:

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

Para explicar como isso funciona e resolver um problema sutil, mas importante, preciso explorar detalhadamente o que está acontecendo com async/await.

O verdadeiro significado de await: considere o método StartAsync apresentado. É importante perceber que, normalmente, quando um método async chega à palavra-chave await, ele é retornado. A execução do thread continua, assim como quando qualquer método é retornado. Nesse caso, o método StartAsync alcança “await m_mainForm.InitializeAsync” e retorna para Main, que continua chamando Application.Run. Isso leva, de alguma forma, ao resultado contraintuitivo de que Application.Run provavelmente deverá executar antes de m_mainForm.Show, apesar de, sequencialmente, ocorrer depois de m_mainForm.Show. Async e await simplificam, de fato, a programação assíncrona, mas ela ainda não é fácil.

É por isso que métodos assíncronos retornam Tarefas; esta é a Tarefa em conclusão que representa o método assíncrono “retornando” no sentido intuitivo, a saber quando todo o seu código deve ser executado. No caso de StartAsync, significa que concluiu tanto InitializeAsync quanto m_mainForm.Show. E esse é o primeiro problema ao usar async void: sem um objeto de tarefa, não há como o autor da chamada de um método async void saber quando a execução foi concluída.

Como e quando o restante do código é executado, se o thread continuou e o StartAsync já retornou para seu autor da chamada? É aí que o Application.Run entra em ação. Application.Run é um loop infinito, aguardando o trabalho a ser feito, principalmente o processamento de eventos de interface do usuário. Por exemplo, ao mover o mouse pela janela ou ao clicar em um botão, o loop de mensagem Application.Run vai remover da fila o evento e expedir o código apropriado como resposta e, em seguida, aguardará o próximo evento ser recebido. Isso não está limitado somente à interface do usuário, no entanto: considere Control.Invoke, que executa uma função no thread de IU. Application.Run também está processando estas solicitações.

Nesse caso, quando InitializeAsync for concluído, o restante do método StartAsync será postado neste loop de mensagens. Quando você usar await, o Application.Run executará o restante do método no thread de IU, como se você fosse gravar um retorno de chamada usando Control.Invoke. (Se a continuação precisar continuar no thread de IU, ela será controlada por ConfigureAwait. Você pode ler mais sobre isso no artigo de março de 2013 de Stephen Cleary sobre as melhores práticas na programação assíncrona em msdn.com/magazine/jj991977).

É por isso que é tão importante separar Application.Run de m_mainForm. O Application.Run comanda a exibição: precisa estar em execução para processar o código depois de “await”, mesmo antes de mostrar qualquer interface do usuário. Por exemplo, se você tentar mover Application.Run para fora de Main e voltar para StartAsync, o programa será fechado imediatamente: quando a execução alcançar “await InitializeAsync”, o controle retornará para Main e, em seguida, não haverá mais código para ser executado, chegando ao fim do Main.

Isso também explica o motivo pelo qual o uso de async precisa começar de baixo para cima. Um antipadrão comum, mas recente, é chamar Task.Wait em vez de await, pois o autor da chamada não é qualquer método async, mas provavelmente fará deadlock imediatamente. O problema é que o thread de IU será bloqueado por esta chamada para Aguardar e não poderá processar a continuação. Sem a continuação, a tarefa não será concluída; portanto, a chamada para Aguardar nunca retornará: é um beco sem saída!

Await e Application.Run um eterno problema: mencionei anteriormente que havia um problema sutil. Descrevi que quando você chama await, o comportamento padrão é continuar a execução no thread de IU, que é o que eu preciso aqui. Entretanto, a infraestrutura para isso não está definida ao chamar await pela primeira vez, pois o código apropriado ainda não foi executado!

SynchronizationContext.Current é a chave para este comportamento: Ao chamar await, a infraestrutura captura o valor de SynchronizationContext.Current e usa isso para postar a continuação; é assim que ela continua no thread de IU. O contexto de sincronização é definido pelo Windows Forms ou pelo WPF quando inicia a execução do loop de mensagens. Dentro de StartAsync, isso ainda não aconteceu: se você examinar SynchronizationContext.Current no começo de StartAsync, verá que está nulo. Se não houver um contexto de sincronização, await postará a continuação do pool de thread em seu lugar e, como isso não vai ser no thread de IU, não funcionará.

A versão do WPF vai parar de responder imediatamente, mas a versão do Windows Forms funcionará “por acaso”. Por padrão, o Windows Forms define o contexto de sincronização quando o primeiro controle é criado – nesse caso, quando construo m_mainForm (este comportamento é controlado pelo WindowsFormsSynchronizationContext.AutoInstall). Como “await InitializeAsync” ocorre depois que eu criar o formulário, não tem problema. Se eu tivesse colocado uma chamada await antes de criar m_mainForm, entretanto, teria o mesmo problema. A solução é definir o contexto de sincronização por si só no começo, da seguinte forma:

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

Para WPF, a chamada equivalente é:

SynchronizationContext.SetSynchronizationContext(
  new DispatcherSynchronizationContext());

Manipulação de exceção

Quase lá! Mas, ainda tenho outro problema persistente na raiz do aplicativo: se InitializeAsync cria uma exceção, o problema não sabe tratá-lo. O objeto da tarefa programStart conterá as informações de exceção, mas nada será feito com isso; portanto, o meu aplicativo ficará preso em uma espécie de purgatório. Se pudesse usar “await StartAsync”, poderia capturar a exceção no Main, mas não posso usar await, pois Main não é assíncrono.

Isso ilustra o segundo problema com async void: Não há como capturar corretamente as exceções lançadas por um método async void, pois o autor da chamada não tem acesso ao objeto da tarefa. (Então, quando você deve usar async void? Diretrizes comuns dizem que async void deve se limitar, principalmente, a manipuladores de eventos. O artigo de março de 2013 que mencionei também discute isso; recomendo que você o leia para entender ainda mais de async/await.)

Sob circunstâncias normais, TaskScheduler.UnobservedException lida com tarefas que criam exceções que não são manuseadas posteriormente. O problema é que, não há garantia de sua execução. Nesta situação, é quase certeza que não vai: o agendador de tarefas detecta exceções não observadas quando tal tarefa é finalizada. A finalização acontece somente quando o coletor de lixo é executado. O coletor de lixo é executado somente quando precisa atender uma solicitação de mais memória.

Você pode entender onde isso vai levar: nesse caso, uma exceção resultará na inatividade do aplicativo, não solicitará mais memória e o coletor de lixo não será executado. O efeito é que o aplicativo parará de responder. Na verdade, é por isso que a versão do WPF é desligada se você não especificar o contexto de sincronização: o construtor de janela do WPF lança uma exceção, pois uma janela está sendo criada em um thread que não é de IU e, então, essa exceção não é tratada. Por fim, é necessário lidar com a tarefa programStart e adicionar uma continuação que será executada no caso de um erro. Nesse caso, faz sentido encerrar se o aplicativo não consegue ser inicializado sozinho.

Não posso usar await no Main, pois não é async, mas posso criar um novo método async exclusivamente para expor (e tratar) exceções lançadas durante a inicialização assíncrona: ela consistirá somente em um try/catch relacionado a um await. Como esse método tratará todas as exceções e não lançará novas, este é mais um dos casos limitados em que async void faz sentido:

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 isso da seguinte forma:

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

claro que, como de costume, há um pequeno problema (se asyn/await facilita as coisas, você pode imaginar como era difícil). Eu disse mais cedo que, normalmente, quando um método async alcança uma chamada para await, ele retorna, e o restante do método executa uma continuação. Em alguns casos, porém, a tarefa pode ser concluída de modo sincronizado; se esse for o caso, a execução do código não é corrompida, o que é um benefício para o desempenho. Se isso acontecer aqui, porém, quer dizer que o método HandleExceptions será executado em sua totalidade e, em seguida, retornará um Application.Run e seguirá: Nesse caso, se houver uma exceção, agora a chamada Application.Exit ocorrerá antes da chamada para Application.Run e não entrará em vigor.

O que quero fazer é forçar HandleExceptions a executar como uma continuação: preciso ter certeza que vou “fracassar” em Application.Run antes de fazer qualquer outra coisa. Dessa forma, se houver uma exceção, sei que Application.Run já está em execução e Application.Exit o interromperá adequadamente. Task.Yield faz o seguinte: força o caminho de código asyn atual a ceder ao autor da chamada e, então, retomar como uma continuação.

Veja aqui a correção para 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

Nesse caso, quando chamo “await Task.Yield”, HandleExceptions será retornado e Application.Run executado. Então, o restante de HandleExceptions será postado como uma continuação para SynchronizationContext atual, o que significa que ele será capturado por Application.Run.

A propósito, acredito que Task.Yield é um bom teste decisivo para entender async/await: Se você entender o uso de Task.Yield, provavelmente tem uma compreensão sólida de como async/await funciona.

A Recompensa

Agora que tudo está funcionando, é hora de se divertir um pouco: vou mostrar como é fácil adicionar uma tela inicial dinâmica sem executá-la em um thread separado. Legal ou não, é importante ter uma tela inicial se o seu aplicativo não “iniciar” logo de cara: se o usuário iniciar seu aplicativo e nada acontecer por vários segundos, esta é uma experiência de usuário ruim.

Iniciar um thread separado de uma tela inicial não é eficiente e também pode ser ultrapassado – você precisa realizar marshaling de todas as chamadas corretamente entre os threads. Fornecer informações de progresso na tela inicial, entretanto, é difícil, e mesmo fechá-la exige uma chamada para Invocar ou o equivalente. Além disso, quando a tela inicial finalmente é fechada, ela não costuma focar no formulário principal, pois é impossível definir uma propriedade entre a tela inicial e o formulário principal se eles estiverem em threads diferentes. Compare isso à simplicidade da versão assíncrona, mostrada na Figura 8.

Figura 8 - Adicionando uma Tela Inicial a 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();
  }
}

Conclusão

Mostrei como aplicar um design orientado a objeto ao código de inicialização do seu aplicativo, seja no Windows Forms ou WPF, para dar suporte de modo fácil à inicialização assíncrona. Também mostrei como superar alguns problemas sutis que podem surgir do processo de inicialização assíncrona. Em relação a tornar sua inicialização assíncrona, infelizmente você terá de fazê-lo por si só, mas encontrará diretrizes em msdn.com/async.

Habilitar o uso de async e await é apenas o começo. Agora que Programa é mais orientado a objeto, outros recursos ficam mais diretos para serem implementados. Posso processar argumentos de linha de comando ao chamar um método apropriado na classe de Programa. Posso solicitar que o usuário faça logon antes de mostrar a janela principal. Posso iniciar o aplicativo na área de notificação sem mostrar nenhuma janela na inicialização. Como de costume, um design orientado a objeto fornece a oportunidade de estender e reutilizar a funcionalidade no seu código.


Mark Sowulpode, na verdade, ser uma simulação de software gravada no C# (é o que as pessoas especulam). Um desenvolvedor .NET dedicado desde o começo, Sowul compartilha sua riqueza de arquitetura e conhecimento de desempenho em .NET e Microsoft SQL Server por meio de seus negócios de consultoria em Nova York, a SolSoft Solutions. Entre em contato com ele via mark@solsoftsolutions.com e inscreva-se para receber emails ocasionais sobre informações de software em eepurl.com/_K7YD.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Stephen Cleary e James McCaffrey