Assincronismo no WPF


Uma grande necessidade que existe ao escrevermos aplicações, é a necessidade de executar tarefas custosas e/ou periódicas (polling). Muitas vezes, se elas forem escritas em sua forma tradicional, vamos nos deparar com uma experiência ruim durante o uso do software, justamente porque essas tarefas serão executadas de forma síncrona, ou seja, até que elas não sejam concluídas, o usuário não poderá acessar qualquer outra área do sistema.

O .NET Framework fornece desde as primeiras versões suporte para escrita de código assíncrono. O problema é que a escrita deste código não é lá muito trivial, tendo que lidar com muitos detalhes de baixa nível, tais como asyncs results, callbacks, sincronização, etc. Para facilitar isso, a Microsoft trabalha na criação de novos recursos que serão incorporados na própria linguagem (C# e VB.NET), tornando a programação assíncrona muito mais simples de ser realizada, tornando-a tão intuitiva quanto a programação síncrona.

Além dessa mudança nas linguagens, ainda temos o Reactive Extensions, que uma das suas funcionalidades, é prover uma forma diferente de lidar com a programação assíncrona, que ao invés de “puxarmos” o resultado de algum lugar, podemos fazer com que esse resultado seja “empurrado” para a aplicação, o que lhe permitirá trabalhar de forma reativa. A finalidade deste artigo é apresentar como podemos proceder para trabalhar de forma assíncrona em uma aplicação Windows, e para o exemplo, vamos recorrer a um projeto baseado em WPF em conjunto com o Reactive Extensions.

Como disse anteriormente, podemos executar uma tarefa de forma assíncrona ou executar uma ação periodicamente. No primeiro caso, em que precisamos executar uma tarefa de forma assíncrona, podemos recorrer ao método de extensão ToAsync da classe Observable. Esse método possui centenas de overloads, qual contempla grande parte (senão todas) das versões dos delegates Action e Func. Sendo assim, podemos vincular diretamente um método que leva um tempo longo para executar, e com isso, reagir quando o resultado for retornado. A implementação deste código pode variar dependendo se você está ou não utilizando o padrão MVVM. Abaixo temos o exemplo de como podemos proceder para executar um cálculo custoso de forma assíncrona, utilizando Reactive Extensions, sem MVVM:

private void Operacao_Click(object sender, RoutedEventArgs e)
{
    int v1 = int.Parse(this.Valor1.Text);
    int v2 = int.Parse(this.Valor2.Text);

    Observable
        .ToAsync<int, int, int>(Somar)(v1, v2)
        .ObserveOnDispatcher()
        .Subscribe<int>(r => this.Resultado.Text = r.ToString());
}

private int Somar(int v1, int v2)
{
    Thread.Sleep(4000);

    return v1 + v2;
}

O único comentário relevante ao método Somar é que ele simula um processamento custoso através do método Sleep. Já no evento Click do botão, invocamos o método ToAsync, informando qual o método que deve ser disparado de forma assíncrona, incluindo logo na sequência, os parâmetros exigido pelo método Somar. Quando chamamos o método Subscribe, ele passa a monitorar qualquer resultado (de sucesso) gerado pelo método Somar, e neste caso, estamos apresentando-o em um terceiro TextBox.

Um detalhe extremamente importante é sobre o método ObserveOnDispatcher. O WPF possui uma classe chamada Dispatcher, que serve como um gerenciador de tarefas para serem executadas, e está sempre associada com uma determinada thread de UI. Isso quer dizer que qualquer notificação enviada pelo método Somar será enviada e executada pela própria thread que criou os controles de UI, já que aplicações Windows (WPF e Windows Forms) possuem essa afinidade. Se não nos atentarmos a este método, uma exceção do tipo InvalidOperationException será disparada, contendo a seguinte mensagem: The calling thread cannot access this object because a different thread owns it.

Já quando utilizamos MVVM, a implentação é um pouco diferente por conta da estrutura imposta pelo padrão. Os botões da View (Xaml) são executados através de um comando que deve implementar a interface ICommand. Neste caso, é muito comum recorrer à criação de um comando chamado de RelayCommand, que permite você injetar a lógica do comando a ser executado através de delegates. Só que é importante dizer que a execução deste comando é realizado de forma síncrona. Precisamos realizar uma pequena implementação para conseguir executar este mesmo comando de forma assíncrona. O método que representa a lógica a ser executada, pode ser executado de forma síncrona ou assíncrona, sem a necessidade de qualquer alteração no mesmo.

Abaixo temos o código que representa o comando que define que a execução de alguma tarefa seja realizada de forma assíncrona. Note que continuamos utilizando o Reactive Extensions, e no método ToAsync definimos o método que é informado no construtor desta mesma classe.

public class AsyncRelayCommand : RelayCommand
{
    public AsyncRelayCommand(Action execute)
        : base(execute) { }

    public AsyncRelayCommand(Action execute, Func<bool> canExecute)
        : base(execute, canExecute) { }

    public override void Execute(object parameter)
    {
        Observable
            .ToAsync(base.execute)()
            .Subscribe();
    }
}

Depois de criado esta classe, podemos fazer o uso da mesma diretamente em nossa ViewModel. Aqui optei por variar o método Somar, pois ao invés de receber os parâmetros e devolver o resultado, estou optando por acessar diretamente as propriedades no interior do método. E aqui cabe comentar um detalhe interessante: note que não usamos o método ObserveOnDispatcher na classe AsyncRelayCommand. Isso se deve ao fato de que o padrão MVVM faz com que a ViewModel seja completamente independente da View, e com isso, não conseguimos acessar seus respectivos controles e, consequentemente, não corremos risco nos depararmos novamente com aquela exceção que vimos acima.

public class CalculoViewModel : INotifyPropertyChanged
{
    public CalculoViewModel()
    {
        this.Calculo = new AsyncRelayCommand(Somar);
    }

    public ICommand Calculo { get; private set; }

    public string Valor1 { get; set;}

    public string Valor2 { get; set; }

    public string Resultado { get; set; }

    private void Somar()
    {
        Thread.Sleep(4000);

        this.Resultado =
            Convert.ToString(int.Parse(this.Valor1) + int.Parse(this.Valor2));
    }
}

Observação: Por questões de espaço eu preferi omitir a implementação necessária para notificar a alteração das propriedades (INotifyPropertyChanged). Isso continua sendo necessário, pois é assim que a View monitora toda e qualquer alteração que é realizada no interior da ViewModel para assim atualizar a UI.

Além das opções que vimos acima, ainda podemos necessitar que tenhamos um consumo periódico de alguma informação. Por exemplo, necessitamos monitorar serviço de cotação de valores, notícias de algum site (RSS), empregos, etc. Com isso, haverá uma tarefa sendo executando a cada X segundos, buscando as informações e, consequentemente, apresentando-as na tela para que o usuário possa visualizá-la.

Abaixo temos a ViewModel criada para atender este cenário. Temos uma propriedade chamada Noticias que retorna uma coleção do tipo ObservableCollection<Noticia>, qual será definida como fonte de dados de um controle ListBox da View (Xaml).

public class NoticiasViewModel
{
    public NoticiasViewModel()
    {
        this.Noticias = new ObservableCollection<Noticia>();
        this.MonitorarNoticias();
    }

    private void MonitorarNoticias()
    {
        Observable
            .Timer(TimeSpan.Zero, TimeSpan.FromSeconds(3))
            .Select(_ => BuscarNoticiasViaWeb())
            .ObserveOnDispatcher()
            .Subscribe(noticias => noticias.ForEach(n => this.Noticias.Add(n)));
    }

    private static List<Noticia> BuscarNoticiasViaWeb()
    {
        // Buscar notícias via Http
    }

    public ObservableCollection<Noticia> Noticias { get; set; }
}

Alguns novos operadores entram em cena aqui. O método Timer retorna uma sequência produzida (pelo método Select) a cada X segundos/minutos/horas. Note que voltamos a necessitar do método ObserveOnDispatcher, mesmo aqui, onde estamos utilizando MVVM. A questão é que quando definimos uma coleção como fonte de dados de algum controle, como é o caso do ListBox, ele envolve essa coleção em uma classe do tipo CollectionView, e esta herda diretamente da classe DispatcherObject, o que determina que ela tenha afinidade com a thread (Dispatcher) em que a mesma foi criada. Sendo assim, a não utilização do método ObserveOnDispatcher, vamos nos deparar com uma exceção do tipo NotSuppertedException, com a seguinte mensagem: This type of CollectionView does not support changes to its SourceCollection from a thread different from the Dispatcher thread.

Eis aqui algumas opções que temos para trabalharmos de forma assíncrona no WPF. Como disse antes, temos algumas mudanças que estão acontecendo nas linguagens que tendem a facilitar ainda mais a criação de código assíncrono, sem depender de qualquer recurso extra. De qualquer forma, essas opções já tornam o código bem mais expressivo e de fácil manutenção.

Publicidade

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair /  Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair /  Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair /  Alterar )

Conectando a %s