DynamicData: coleções dinâmicas, arquitetura MVVM e extensões reativas

Fevereiro de 2019 marcou o lançamento do ReactiveUI 9 - a estrutura de plataforma cruzada para a criação de aplicativos GUI na plataforma Microsoft .NET. O ReactiveUI é uma ferramenta para forte integração de extensões reativas com o padrão de design do MVVM. Você pode se familiarizar com a estrutura através de uma série de vídeos ou da página de boas-vindas da documentação . A atualização do ReactiveUI 9 inclui várias correções e melhorias , mas provavelmente a mais crucial e interessante é a integração com a estrutura DynamicData , permitindo que você trabalhe com coleções dinâmicas da maneira reativa. Vamos descobrir para que podemos usar o DynamicData e como essa poderosa estrutura reativa funciona sob o capô!


1. Introdução


Vamos primeiro determinar os casos de uso do DynamicData e descobrir o que não gostamos nas ferramentas padrão para trabalhar com conjuntos de dados dinâmicos no espaço de nome System.Collections.ObjectModel .


O modelo MVVM, como sabemos, assume a divisão de responsabilidade entre a camada do modelo, a camada de apresentação e o modelo de apresentação do aplicativo, também conhecido como modelo de exibição. A camada de modelo é representada por entidades e serviços de domínio e não sabe nada sobre a camada de modelo de exibição. O modelo encapsula toda a lógica complexa do aplicativo, enquanto o modelo de exibição delega operações para o modelo, fornecendo acesso a informações sobre o estado atual do aplicativo por meio de propriedades, comandos e coleções observáveis ​​na exibição. A ferramenta padrão para trabalhar com propriedades dinâmicas é a interface INotifyPropertyChanged , para trabalhar com ações do usuário - ICommand e para trabalhar com coleções - a interface INotifyCollectionChanged , além de implementações como ObservableCollection<T> e ReadOnlyObservableCollection<T> .




As implementações das interfaces INotifyPropertyChanged e ICommand geralmente dependem do desenvolvedor e da estrutura MVVM usada, mas o uso do padrão ObservableCollection<T> impõe várias limitações! Por exemplo, não podemos alterar a coleção de um encadeamento em segundo plano sem Dispatcher.Invoke ou uma chamada semelhante, e isso seria super útil para sincronizar matrizes de dados com o servidor por meio de uma operação em segundo plano. Vale ressaltar que, ao usar a arquitetura MVVM limpa, a camada de modelo não deve saber nada sobre a estrutura da GUI usada e deve ser compatível com a camada de modelo na terminologia MVC ou MVP. É por isso que essas inúmeras chamadas do Dispatcher.Invoke nos serviços de domínio violam o princípio de segregação de responsabilidades.


Obviamente, poderíamos declarar um evento em um serviço de domínio e transmitir uma parte com itens alterados como argumentos de evento, depois assinar o evento, encapsular a Dispatcher.Invoke chamada por trás de uma interface, para que nosso aplicativo não dependa de nenhuma GUI , chame essa interface do modelo de exibição e modifique ObservableCollection<T> acordo, mas há uma maneira muito mais elegante de lidar com esses problemas sem a necessidade de reinventar a roda. O que estamos esperando então?


Extensões reativas. Gerenciando fluxos de dados observáveis


Para entender completamente as abstrações introduzidas pelo DynamicData e como trabalhar com a alteração dos conjuntos de dados reativos, vamos relembrar o que é programação reativa e como usá-la no contexto da plataforma Microsoft .NET e no padrão de design MVVM . A maneira de organizar a interação entre os componentes do programa pode ser interativa ou reativa. Com a abordagem interativa, o consumidor recebe dados do produtor de forma síncrona (baseada em pull, T, IEnumerable) e com a abordagem reativa, o produtor envia os dados para o consumidor de forma assíncrona (baseada em push, Task, IObservable).




A programação reativa está programando com fluxos de dados assíncronos, e as extensões reativas são a implementação da programação reativa, com base nas interfaces IObservable e IObserver do namespace System, definindo uma série de operações semelhantes a LINQ na interface IObservable , conhecidas como LINQ over Observable. As extensões reativas suportam o .NET Standard e são executadas onde quer que o Microsoft .NET seja executado.




O ReactiveUI oferece aos desenvolvedores de aplicativos a vantagem de usar as implementações reativas para as interfaces ICommand e INotifyPropertyChanged , fornecendo ferramentas como ReactiveCommand<TIn, TOut> e WhenAnyValue . WhenAnyValue permite converter uma propriedade de uma classe que implementa a interface INotifyPropertyChanged em um fluxo de eventos do tipo IObservable<T> , isso simplifica a implementação de propriedades dependentes.


 public class ExampleViewModel : ReactiveObject { [Reactive] // Attribute from the ReactiveUI.Fody package, // takes care of aspect-oriented INPC implementation // for this particular property. public string Name { get; set; } public ExampleViewModel() { // Here we subscribe to OnPropertyChanged("Name") events. this.WhenAnyValue(x => x.Name) // IObservable<string> .Subscribe(Console.WriteLine); } } 

ReactiveCommand<TIn, TOut> permite trabalhar com um comando como com IObservable<TOut> , que é publicado sempre que um comando conclui a execução. Além disso, qualquer comando possui uma propriedade ThrownExceptions do tipo IObservable<Exception> .


 // ReactiveCommand<Unit, int> var command = ReactiveCommand.Create(() => 42); command // IObservable<int> .Subscribe(Console.WriteLine); command .ThrownExceptions // IObservable<Exception> .Select(exception => exception.Message) // IObservable<string> .Subscribe(Console.WriteLine); command.Execute().Subscribe(); // Outputs: 42 

Até o momento, trabalhamos com IObservable<T> , como em um evento que publica um novo valor do tipo T sempre que o estado do objeto observado é alterado. Simplificando, IObservable<T> é um fluxo de eventos, uma coleção do tipo T esticada no tempo.


Obviamente, poderíamos trabalhar com coleções com a mesma facilidade e naturalidade - sempre que uma coleção muda, podemos publicar uma nova coleção com elementos alterados. Nesse caso, o valor publicado seria do tipo IEnumerable<T> ou mais especializado, e o próprio fluxo observável seria do tipo IObservable<IEnumerable<T>> . Mas, como observa corretamente um leitor crítico, isso está repleto de problemas críticos de desempenho, especialmente se não houver uma dúzia de elementos em nossa coleção, mas uma centena ou mesmo alguns milhares!


Introdução ao DynamicData


DynamicData é uma biblioteca que permite usar o poder das extensões reativas ao trabalhar com coleções. O Rx é extremamente poderoso, mas pronto para o uso não fornece nada para ajudar no gerenciamento de coleções, e o DynamicData corrige isso. Na maioria dos aplicativos, é necessário atualizar dinamicamente as coleções - geralmente, uma coleção é preenchida com itens quando o aplicativo é iniciado e, em seguida, a coleção é atualizada de forma assíncrona, sincronizando informações com um servidor ou banco de dados. Os aplicativos modernos são bastante complexos e geralmente é necessário criar projeções de coleções - filtrar, transformar ou classificar elementos. O DynamicData foi projetado para se livrar do código incrivelmente complexo que precisaríamos para gerenciar conjuntos de dados que mudam dinamicamente. A ferramenta está desenvolvendo e refinando ativamente, e agora mais de 60 operadores são suportados para trabalhar com coleções.




DynamicData não é uma implementação alternativa de ObservableCollection<T> . A arquitetura do DynamicData é baseada principalmente em conceitos de programação controlados por domínio. A ideologia de uso é baseada no fato de você controlar uma certa fonte de dados, uma coleção à qual o código responsável pela sincronização e mutação de dados tem acesso. Em seguida, você aplica uma série de operadores à fonte de dados. Com a ajuda desses operadores, você pode transformar declarativamente os dados sem a necessidade de criar e modificar manualmente outras coleções. De fato, com o DynamicData, você separa as operações de leitura e gravação e só pode ler de maneira reativa - portanto, as coleções herdadas sempre permanecerão sincronizadas com a fonte.


Em vez do IObservable<T> clássico IObservable<T> , DynamicData define operações em IObservable<IChangeSet<T>> e IObservable<IChangeSet<TValue, TKey>> , em que IChangeSet é um bloco contendo informações sobre a alteração da coleção, incluindo o tipo de alteração e os elementos afetados. Essa abordagem pode melhorar significativamente o desempenho do código para trabalhar com coleções, gravadas de maneira reativa. Você sempre pode transformar IObservable<IChangeSet<T>> em IObservable<IEnumerable<T>> , se for necessário acessar todos os elementos de uma coleção de uma vez. Se isso parecer difícil - não se preocupe, os exemplos de código abaixo deixarão tudo claro!


Dados dinâmicos em ação


Vejamos alguns exemplos para entender melhor como o DynamicData funciona, como difere do System.Reative e quais tarefas os desenvolvedores comuns do software GUI podem ajudar a resolver. Vamos começar com um exemplo abrangente publicado no GitHub . Neste exemplo, a fonte de dados é SourceCache<Trade, long> contendo uma coleção de transações. O objetivo é mostrar apenas transações ativas, transformar modelos em objetos proxy, classificar a coleção.


 // The default collection from the System.Collections.ObjectModel // namespace, to which we bind XAML UI controls. ReadOnlyObservableCollection<TradeProxy> list; // The mutable data source, containing the list of transactions. // We can use Add, Remove, Insert and similar methods on it. var source = new SourceCache<Trade, long>(trade => trade.Id); var cancellation = source // Here we transform the data source to an observable change set. .Connect() // Now we have IObservable<IChangeSet<Trade, long>> here. // Filter only active transactions. .Filter(trade => trade.Status == TradeStatus.Live) // Transform the models into proxy objects. .Transform(trade => new TradeProxy(trade)) // No we have IObservable<IChangeSet<TrandeProxy, long>> // Order the trade proxies by timestamp. .Sort(SortExpressionComparer<TradeProxy> .Descending(trade => trade.Timestamp)) // Use the dispatcher scheduler to update the GUI. .ObserveOnDispatcher() // Bind the sorted objects to the collection from the // System.Collections.ObjectModel namespace. .Bind(out list) // Ensure that when deleting elements from the // collections, the resources will get disposed. .DisposeMany() .Subscribe(); 

No exemplo acima, ao alterar o SourceCache que é a fonte dos dados, o ReadOnlyObservableCollection também muda de acordo. Ao mesmo tempo, ao remover itens da coleção, o método Dispose será chamado, a coleção sempre será atualizada apenas a partir do encadeamento da GUI e permanecerá classificada e filtrada. Legal, agora não temos Dispatcher.Invoke . Dispatcher.Invoke chamadas e o código é simples e legível!


Fontes de dados. SourceList e SourceCache


O DynamicData fornece duas coleções especializadas que podem ser usadas como uma fonte de dados mutável. Essas coleções são SourceList<TObject> e SourceCache<TObject, TKey> . É recomendável usar o SourceCache sempre que o TObject tiver uma chave exclusiva; caso contrário, use o SourceList . Esses objetos fornecem o familiar para a API de desenvolvedores .NET para gerenciamento de coleções - métodos como Add , Remove , Insert . Para converter fontes de dados em IObservable<IChangeSet<T>> ou em IObservable<IChangeSet<T, TKey>> , use o operador .Connect() . Por exemplo, se você tiver um serviço que atualiza periodicamente uma coleção de itens em segundo plano, poderá sincronizar facilmente a lista desses itens com a GUI, sem o Dispatcher.Invoke e um código semelhante:


 public class BackgroundService : IBackgroundService { // Declare the mutable data source containing trades. private readonly SourceList<Trade> _trades; // Expose the observable change set to the outside world. // If we have more than one subscriber, it is recommended // to use the Publish() operator from reactive extensions. public IObservable<IChangeSet<Trade>> Connect() => _trades.Connect(); public BackgroundService() { _trades = new SourceList<Trade>(); _trades.Add(new Trade()); // Mutate the source list! // Even from the background thread. } } 

Com a ajuda dos poderosos operadores DynamicData, podemos transformar IObservable<IChangeSet<Trade>> em ReadOnlyObservableCollection declarado em nosso modelo de exibição.


 public class TradesViewModel : ReactiveObject { private readonly ReadOnlyObservableCollection<TradeVm> _trades; public ReadOnlyObservableCollection<TradeVm> Trades => _trades; public TradesViewModel(IBackgroundService background) { // Connect to the data source, transform elements, bind // them to the read-only observable collection. background.Connect() .Transform(x => new TradeVm(x)) .ObserveOn(RxApp.MainThreadScheduler) .Bind(out _trades) .DisposeMany() .Subscribe(); } } 

Além dos operadores Transform , Filter e Sort , o DynamicData suporta agrupamentos, operações lógicas, achatamento de coleções, uso de funções agregadas, eliminação de elementos idênticos, contagem de elementos e até virtualização no nível do modelo de visualização. Você pode ler mais sobre todos os operadores no README do projeto no GitHub .




Além de SourceList e SourceCache , a biblioteca DynamicData inclui uma implementação de coleção mutável de thread único - ObservableCollectionExtended . Para sincronizar duas coleções no seu modelo de exibição, declare uma delas como ObservableCollectionExtended e a outra como ReadOnlyObservableCollection e, em seguida, use o operador ToObservableChangeSet , que faz quase a mesma coisa que o Connect, mas pretende trabalhar com a ObservableCollection .


 // Declare the derived collection. ReadOnlyObservableCollection<TradeVm> _derived; // Declare and initialize the source collection. var source = new ObservableCollectionExtended<Trade>(); source.ToObservableChangeSet(trade => trade.Key) .Transform(trade => new TradeProxy(trade)) .Filter(proxy => proxy.IsChecked) .Bind(out _derived) .Subscribe(); 

O DynamicData também oferece suporte ao rastreamento de alterações nas classes que implementam a interface INotifyPropertyChanged . Por exemplo, se você deseja receber notificações sempre que uma propriedade for alterada, use o operador AutoRefresh e passe o seletor de propriedades necessário. AutoRefresh e outros operadores DynamicData podem permitir que você valide sem esforço um número gigante de formulários e formulários aninhados exibidos na tela!


 // IObservable<bool> var isValid = databases .ToObservableChangeSet() // Subscribe only to IsValid property changes. .AutoRefresh(database => database.IsValid) // Materialize the collection. .ToCollection() // Determine if all forms are valid. .Select(x => x.All(y => y.IsValid)); // If ReactiveUI is used, you can transform the // IObservable<bool> variable to a property declared // as ObservableAsPropertyHelper<bool>, eg IsValid. _isValid = isValid .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.IsValid); 

Você pode criar interfaces de usuário complexas usando a funcionalidade DynamicData, e é especialmente relevante para sistemas que exibem uma grande quantidade de dados em tempo real, como aplicativos de mensagens instantâneas e sistemas de monitoramento.





Conclusão


O ReactiveX é uma ferramenta poderosa que permite trabalhar com fluxos de eventos e com a interface do usuário, escrever códigos portáteis e de manutenção e resolver tarefas complexas de maneira simples e elegante. O ReactiveUI permite que desenvolvedores .NET integrem extensões reativas em seus projetos usando a arquitetura MVVM com implementações reativas de INotifyPropertyChanged e ICommand , enquanto DynamicData cuida do gerenciamento de coleções implementando INotifyCollectionChanged , expandindo os recursos de extensões reativas com foco no desempenho.


As bibliotecas ReactiveUI e DynamicData são totalmente compatíveis com todas as estruturas da GUI na plataforma .NET, incluindo Windows Presentation Foundation, Universal Windows Platform, Avalonia , Xamarin.Android, Xamarin Forms e Xamarin iOS. Você pode começar a estudar o DynamicData na página correspondente da documentação do ReactiveUI . Também se familiarize com o projeto DynamicData Snippets , contendo exemplos de código para quase tudo que você pode precisar.

Source: https://habr.com/ru/post/pt454074/


All Articles