MVVM

Multithreading e expedição em aplicativos MVVM

Laurent Bugnion

Baixar o código de exemplo

Há cerca de um ano, eu comecei uma série de artigos sobre o padrão Model-View-ViewModel (MVVM) para o site da MSDN Magazine (você pode acessar todos eles em is.gd/mvvmmsdn). Os artigos mostram como usar os componentes do MVVM Light Toolkit para criar aplicativos pouco rígidos de acordo com esse padrão. Eu exploro os padrões de contêiner injeção de dependência (dependency injection, DI) e inversão de controle (IOC) (incluindo o MVVM Light SimpleIoc), apresento o Messenger e discuto serviços View (como Navigation, Dialog e por aí vai). Eu também mostro como criar dados de tempo de design para maximizar o uso de designers visuais com o Blend e falo sobre os componentes RelayCommand e EventToCommand, que substituem manipuladores de eventos para um relacionamento mais desassociado entre o View e o ViewModel.

Neste artigo, quero mergulhar em outro cenário frequente nos aplicativos clientes modernos — manipular múltiplos threads e ajudá-los a se comunicarem entre si. Multithreading é um assunto de cres­cente importância nos frameworks para aplicativos modernos, como o Windows 8, Windows Phone, Windows Presentation Foundation (WPF), Silverlight e outros. Em cada uma dessas plataformas, mesmo na menos poderosa, é necessário iniciar e gerenciar threads em segundo plano. De fato, pode-se dizer que isso é ainda mais importante em plataformas menores e com menos poder de computação, para oferecer uma melhor experiência de usuário.

A plataforma Windows Phone é um bom exemplo. Na primeiríssima versão (Windows Phone 7), era bem difícil conseguir uma rolagem macia em listas longas, especialmente quando os modelos de itens continham imagens. No entanto, em versões posteriores a decodificação de imagens, assim como algumas animações, são passadas para um thread dedicado em segundo plano. Resultado: quando uma imagem é carregada, isso não tem mais um impacto sobre o thread principal, e a rolagem permanece suave.

Esse exemplo sublinha alguns conceitos importantes que vou explorar neste artigo. Começarei revisando como o multithreading funciona de modo geral em aplicativos baseados em XAML.

Em termos simples, um thread pode ser considerado como a menor unidade de execução de um aplicativo. Cada aplicativo tem pelo menos um thread, que é chamado thread principal. Esse é o thread que é iniciado pelo OS quando o método principal do aplicativo é chamado, na inicialização. Observe que mais ou menos o mesmo cenário ocorre em todas as plataformas suportadas, tanto no WPF sendo executado em computadores poderosos quanto em dispositivos baseados em Windows Phone e com poder de computação limitado.

Quando um método é chamado, a operação é adicionada a uma fila. Cada operação é executada sequencialmente, de acordo com a ordem em que foram adicionadas à fila (apesar de ser possível influenciar a ordem de execução das operações atribuindo prioridades a elas). O objeto responsável por gerenciar a fila é chamado de dispatcher do thread. Esse objeto é uma instância da classe Dispatcher no WPF, Silverlight e Windows Phone. No Windows 8, o objeto dispatcher recebe o nome CoreDispatcher e usa uma API um pouco diferente.

Como exigido pelo aplicativo, novos threads podem ser iniciados explicitamente no código, implicitamente por algumas bibliotecas ou pelo sistema operacional. O principal propósito de iniciar um novo thread é executar uma operação (ou esperar pelo resultado de uma) sem bloquear o resto do aplicativo. Como, por exemplo, no caso de operação computacionalmente intensa, uma operação E/S e assim por diante. É por isso que os aplicativos modernos são cada vez mais multithreaded, porque os requerimentos de UX também são cada vez maiores. À medida que os aplicativos se tornam mais complexos, a quantidade de threads que eles iniciam aumenta. Um bom exemplo dessa tendência é o framework Windows Runtime usado nos aplicativos da Windows Store. Nesses modernos aplicativos clientes, operações assíncronas (operações em execução em threads de segundo plano) são muito comuns. Por exemplo, cada acesso a arquivo no Windows 8 é agora uma operação assíncrona. É assim que um arquivo é lido (de forma síncrona) no WPF:

public string ReadFile(FileInfo file)
{
  using (var reader = new StreamReader(file.FullName))
  {
    return reader.ReadToEnd();
  }
}

E esta é a operação equivalente (assíncrona) no Windows 8:

public async Task<string> ReadFile(IStorageFile file)
{
  var content = await FileIO.ReadTextAsync(file);
  return content;
}

Perceba a presença das palavras-chave await e async na versão do Windows 8. Elas estão ali para evitar o uso de callbacks em operações assíncronas e para facilitar a leitura do código. Eles são necessários aqui porque a operação do arquivo é assíncrona. A versão do WPF, ao contrário, é síncrona, arriscando bloquear o thread principal se o arquivo que estiver sendo lido for muito longo. Isso pode causar animações que não são fluídas, ou uma IU que falha em se atualizar, o que piora a experiência do usuário.

De modo similar, operações longas nos seus aplicativos devem ser realizadas em um thread em segundo plano se significarem um risco à fluidez da IU. Por exemplo, no WPF, Silverlight e Windows Phone, o código da Figura 1 inicia uma operação em segundo plano que executa um longo loop. Em todo loop, o thread é colocado para dormir por um curto tempo para dar tempo aos outros threads de processarem suas próprias operações.

Figura 1 Operação assíncrona no Microsoft .NET Framework

public void DoSomethingAsynchronous()
{
  var loopIndex = 0;
  ThreadPool.QueueUserWorkItem(
    o =>
    {
      // This is a background operation!
      while (_condition)
      {
        // Do something
        // ...
        // Sleep for a while
        Thread.Sleep(500);
      }
  });
}

Deixando os threads se comunicarem

Quando um thread precisa se comunicar com outro, algumas precauções devem ser tomadas. Por exemplo, eu vou modificar o código da Figura 1 para exibir uma mensagem de status ao usuário em cada loop. Para fazer isso, eu apenas adiciono uma linha de código ao loop while, que define a propriedade Text de um controle StatusTextBlock no XAML:

while (_condition)
{
  // Do something
  // Notify user
  StatusTextBlock.Text = 
    string.Format("Loop # {0}", loopIndex++);
  // Sleep for a while
  Thread.Sleep(500);
}

O aplicativo chamado SimpleMultiThreading que acompanha este artigo mostra esse exemplo. Se você executar o aplicativo usando o botão rotulado “Start (crashes the app)” (”Iniciar (trava o aplicativo)”), o aplicativo, adivinha só, trava. O que aconteceu? Quando um objeto é criado, ele pertence ao thread no qual o método construtor foi chamado. Para elementos da IU, os objetos são pelo analisador XAML quando o documento XAML é carregado. Tudo isso acontece no thread principal. Consequentemente, todos os elementos da IU pertencem ao thread principal, que também costuma ser chamado de thread da IU. Quando o thread em segundo plano no código anterior tenta modificar a propriedade Text do StatusTextBlock, isso cria um acesso ilegal de threads cruzados. Por isso, uma exceção é lançada. Isso pode ser demonstrado executando o código em um depurador. A Figura 2 mostra a caixa de diálogo da exceção. Note a mensagem “Additional information”, que indica a raiz do problema.

Cross-Thread Exception Dialog
Figura 2 Caixa de diálogo de exceção de thread cruzado

Para que esse código funcione, o thread em segundo plano precisa fazer contato com o dispatcher do thread principal para enfileirar a operação nele. Felizmente, cada FrameworkElement é também um DispatcherObject, como demonstrado pela hierarquia de classes .NET na Figura 3. Cada DispatcherObject expõe uma propriedade Dispatcher que dá acesso ao dispatcher do seu dono. Assim, o código pode ser modificado como demonstrado na Figura 4.

Window Class Hierarchy
Figura 3 Hierarquia de classes

Figura 4 Despachando o chamado para o thread de IU

while (_condition)
{
  // Do something
  Dispatcher.BeginInvoke(
    (Action)(() =>
    {
      // Notify user
      StatusTextBlock.Text = 
        string.Format("Loop # {0}", loopIndex++);
    }));
  // Sleep for a while
  Thread.Sleep(500);
}

Expedição em aplicativos MVVM

Quando a operação em segundo plano é executada a partir de um ViewModel, as coisas são um pouco diferentes. Tipicamente, ViewModels não herdam do DispatcherObject. Eles são “POCOs“ (“plain old CLR objects“, ou “simples e velhos objetos CLR“) que implementam a interface INotifyPropertyChanged. Por exemplo, a Figura 5 mostra um ViewModel derivando da classe MVVM Light ViewModelBase. Bem ao estilo MVVM, eu adiciono uma propriedade observável chamada Status que eleva o evento PropertyChanged. Então, a partir do código do thread em segundo plano, eu tento definir essa propriedade com uma mensagem de informação.

Figura 5 Atualizando uma propriedade vinculada no ViewModel

public class MainViewModel : ViewModelBase
{
  public const string StatusPropertyName = "Status";
  private bool _condition = true;
  private RelayCommand _startSuccessCommand;
  private string _status;
  public RelayCommand StartSuccessCommand
  {
    get
    {
      return _startSuccessCommand
        ?? (_startSuccessCommand = new RelayCommand(
          () =>
          {
            var loopIndex = 0;
            ThreadPool.QueueUserWorkItem(
              o =>
              {
                // This is a background operation!
                while (_condition)
                {
                  // Do something
                  DispatcherHelper.CheckBeginInvokeOnUI(
                    () =>
                    {
                      // Dispatch back to the main thread
                      Status = string.Format("Loop # {0}", 
                         loopIndex++);
                    });
                  // Sleep for a while
                  Thread.Sleep(500);
                }
              });
          }));
    }
  }
  public string Status
  {
    get
    {
      return _status;
    }
    set
    {
      Set(StatusPropertyName, ref _status, value);
    }
  }
}

Executar esse código no Windows Phone ou Silverlight funciona direitinho até eu tentar vincular os dados da propriedade Status a um TextBlock no front end do XAML. Executar a operação de novo trava o aplicativo. Como antes, assim que o thread em segundo plano tenta acessar um elemento que pertence a outro thread, a exceção é lançada. Isso ocorre mesmo que o acesso seja feito através de vinculação de dados.

Note que no WPF as coisas são diferentes, e o código mostrado na Figura 5 funciona mesmo que os dados da propriedade Status estejam vinculados a um TextBlock. Isso é porque o WPF automaticamente expede o evento PropertyChanged para o thread principal, diferente de todos os outros frameworks XAML. Em todos os outros frameworks, seria necessário uma solução de expedição. De fato, o que é realmente necessário é um sistema que faça a expedição do chamado apenas se necessário. Para compartilhar o código do ViewModel entre o WPF e outros frameworks, seria ótimo se você não tivesse que se preocupar com a necessidade de fazer a expedição, mas sim tivesse um objeto que fizesse isso automaticamente.

Como o ViewModel é um POCO, ele não tem acesso a uma propriedade Dispatcher, então eu preciso de outra maneira de acessar o thread principal para enfileirar uma operação. Essa é a finalidade do componente MVVM Light DispatcherHelper. Em essência, o que essa classe faz é armazenar o Dispatcher do thread principal em uma propriedade estática e expor alguns métodos utilitários para acessá-lo de forma conveniente e consistente. Para estar funcional, a classe precisa ser inicializada no thread principal. Idealmente, isso deveria ser feito o quanto antes no tempo de vida do aplicativo, para que os recursos estejam disponíveis desde a sua inicialização. Tipicamente, em um aplicativo MVVM Light, o DispatcherHelper é inicializado no App.xaml.cs, que é o arquivo que define a classe de inicialização do aplicativo. No Windows Phone, você chama o Dispatcher­Helper.Initialize no método InitializePhoneApplication, logo depois que o quadro principal do aplicativo é criado. No WPF, a classe é inicializada no construtor do aplicativo. No Windows 8, você chama o método Initialize no OnLaunched, logo depois das janelas terem sido ativadas.

Depois que o chamado ao método DispatcherHelper.Initialize estiver completo, a propriedade UIDispatcher da classe DispatcherHelper vai conter uma referência ao dispatcher do thread principal. É relativamente raro usar a propriedade diretamente, mas é possível se for necessário. Em vez disso, no entanto, o melhor é o usar o método CheckBeginInvokeOnUi. Ele leva um delegado como parâmetro. Tipicamente, você usaria uma expressão lambda como demonstrado na Figura 6, mas também pode ser um método nomeado.

Figura 6 Utilizando o DispatcherHelper para evitar travamento

while (_condition)
{
  // Do something
  DispatcherHelper.CheckBeginInvokeOnUI(
    () =>
    {
      // Dispatch back to the main thread
      Status = string.Format("Loop # {0}", loopIndex++);
    });
  // Sleep for a while
  Thread.Sleep(500);
}

Como sugere o nome, esse método executa uma verificação (check) antes. Se o chamador do método já estiver em execução no thread principal, não será necessário fazer expedição. Nesse caso, o delegado é executado imediatamente, diretamente no thread principal. Se, no entanto, o chamador estiver em um thread em segundo plano, a expedição será executada.

Como o método verifica antes de fazer a expedição, o chamador pode confiar no fato de que o código sempre usará o melhor chamado. Isso é especialmente útil quando você está escrevendo código multiplataforma, onde o multithreading pode funcionar com pequenas diferenças em diferentes plataformas. Nesse caso, o código do ViewModel demonstrado na Figura 6 continua podendo ser compartilhado, sem necessidade de modificar a linha onde a propriedade Status é definida.

Além disso, o DispatcherHelper abstrai as diferenças na API de expedição entre plataformas XAML. No Windows 8, os principais membros do CoreDispatcher são o método RunAsync e a propriedade HasThreadAccess. Em outros frameworks XAML, porém, são usados os métodos BeginInvoke e CheckAccess, respectivamente. Usando o DispatcherHelper, você não tem que se preocupar com essas diferenças e pode compartilhar o código com mais facilidade.

Expedição na vida real: sensores

Vou ilustrar o uso do DispatcherHelper construindo um aplicativo Windows Phone usando o sensor de Bússola.

No código de exemplo que acompanha este artigo há um rascunho de aplicativo chamado CompassSample - Start. Quando você abre esse aplicativo no Visual Studio, o acesso ao sensor de bússola a partir do MainViewModel está encapsulado em um serviço chamado SensorService, que é uma implementação da interface ISensorService. Esses dois elementos podem ser encontrados na pasta Model.

O MainViewModel ganha uma referência ao ISensorService em seu construtor e registra cada alteração na bússola usando o método RegisterForHeading do SensorService. Esse método exige um callback, que será executado a cada vez que o sensor relatar uma mudança no rumo do dispositivo baseado em Windows Phone. No MainViewModel, substitua o construtor padrão pelo código a seguir:

sensorService.RegisterForHeading(
  heading =>
  {
    Heading = string.Format("{0:N1}°", heading);
    Debug.WriteLine(Heading);
  });

Infelizmente, não há jeito de simular a bússola do dispositivo no emulador do Windows Phone. Para testar o código, você precisa executar o aplicativo no dispositivo físico. Conecte um dispositivo de desenvolvedor e execute o código em modo de depuração clicando em F5. Observe o console de Saída no Visual Studio. Você verá a saída da Bússola listada. Se você movimentar o dispositivo, será capaz de encontrar o norte e observar como os valores ficam se atualizando.

A seguir, eu vinculo um TextBlock no XAML à propriedade Heading no MainViewModel. Abra o MainPage.xaml e encontre o TextBlock localizado no ContentPanel. Substitua o “Nothing yet” da propriedade Text por “{Binding Heading}”. Se você executar o aplicativo de novo, em modo de depuração, vai presenciar um travamento com uma mensagem de erro parecida com a de antes. De novo, isso é uma exceção de thread cruzado.

O erro é lançado porque o sensor da bússola é executado em um thread em segundo plano. Quando o código de callback é chamado, ele também é executado no thread em segundo plano, assim como o definidor da propriedade Heading. Como o TextBlock pertence ao thread principal, a exceção é lançada. Também aqui você precisa criar uma “zona de segurança” para cuidar da tarefa de fazer a expedição das operações para o thread principal. Para fazer isso, abra a classe SensorService. O evento CurrentValueChanged é manipulado por um método chamado CompassCurrentValueChanged; é aqui que o método de callback é executado. Substitua o código pelo seguinte, que usa DispatcherHelper:

void CompassCurrentValueChanged(
  object sender,
  SensorReadingEventArgs<CompassReading> e)
{
  if (_orientationCallback != null)
  {
    DispatcherHelper.CheckBeginInvokeOnUI(
      () => _orientationCallback(e.SensorReading.TrueHeading));
  }
}

Agora o DispatcherHelper precisa ser inicializado. Para fazer isso, abra o App.xaml.cs e encontre o método chamado InitializePhoneApplication. Bem ao final desse método, adicione DispatcherHelper.Initialize();. Executar o código agora produz o resultado esperado, exibindo adequadamente o rumo do dispositivo Windows Phone.

Observe que nem todos os sensores do Windows Phone elevam seus eventos em um thread em segundo plano. O sensor GeoCoordinateWatcher, por exemplo, que é usado para observar a geolocalização do telefone, convenientemente já retorna no thread principal. Ao utilizar o DispatcherHelper, você não precisa se preocupar com isso e pode sempre chamar o callback do thread principal do mesmo modo.

Conclusão

Eu discuti como o Microsoft .NET Framework manipula threads e quais precauções precisam ser tomadas quando um thread em segundo plano quiser modificar um objeto criado no thread principal (também chamado de thread da IU). Você viu como isso pode causar um travamento e que, para evitá-lo, o Dispatcher do thread principal deve ser usado para manipular a operação adequadamente.

Então eu traduzi esse conhecimento para um aplicativo MVVM e apresentei o componente DispatcherHelper do kit de ferramentas MVVM Light. Eu mostrei como você pode usar esse componente para evitar problemas quando estiver se comunicando a partir de um thread em segundo plano, e como ele otimiza esse acesso e abstrai as diferenças entre o WPF e os outros frameworks baseados em XAML. Fazendo isso, ele permite o fácil compartilhamento de código ViewModel e facilita o seu trabalho.

Por último, demonstrei em um exemplo real como o Dispatcher­Helper pode ser usado em um aplicativo Windows Phone para evitar problemas quando você estiver trabalhando com certos sensores que elevam seus eventos em um thread em segundo plano.

No próximo artigo, irei mais fundo no componente Messenger do MVVM Light e mostrarei como ele pode ser utilizado para facilitar a comunicação entre objetos sem a necessidade de eles saberem um sobre o outro, de modo verdadeiramente desassociado.

Laurent Bugnion é diretor sênior da IdentityMine Inc., parceira da Microsoft, que trabalha com tecnologias como Windows Presentation Foundation, Silverlight, Pixelsense, Kinect, Windows 8, Windows Phone e UX. Ele mora em Zurique, na Suíça. É também um Microsoft MVP e Diretor Regional da Microsoft.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Thomas Petchel (Microsoft)