System.Collections.ObjectModel
namespace.
INotifyPropertyChanged
, for working with user actions, ICommand
, and for working with collections, for INotifyCollectionChanged
and implementing ObservableCollection
and ReadOnlyObservableCollection
.
INotifyPropertyChanged
and ICommand
usually remains on the developer’s conscience and the MVVM framework used, but the use of ObservableCollection
imposes a number of restrictions on us! For example, we cannot change a collection from a background thread without Dispatcher.Invoke
or a similar call, which could be useful if you work with data arrays that some background operation synchronizes with the server. It should be noted that in the idiomatic MVVM, the model layer does not need to be aware of the GUI application architecture used, and be compatible with the MVC or MVP model, and that is why there are numerous Dispatcher.Invoke
controls that allow access to the user interface control from a background thread running in the domain service, violate the principle of division of responsibility between the layers of the application.
Dispatcher.Invoke
call into the interface, so as not to depend on the GUI framework used, move Dispatcher.Invoke
to the view model and change the ObservableCollection
needed, however there is a much simpler and more elegant way of solving the indicated range of tasks without writing a bicycle . Let's start the study!
T
, IEnumerable
), and during reactive interaction, the provider function asynchronously delivers the data to the consumer function (push-based approach, Task
, IObservable
).
IObservable
and IObserver
from the System namespace, defining a series of LINQ-like operations on the IObservable
interface, called LINQ over Observable. Reactive extensions support .NET Standard and work wherever the Microsoft .NET platform works.
ICommand
and INotifyPropertyChanged
, providing such powerful tools as ReactiveCommand<TIn, TOut>
and WhenAnyValue
. WhenAnyValue
allows WhenAnyValue
to convert a class property that implements INotifyPropertyChanged to an event stream of type IObservable<T>
, which simplifies the implementation of dependent properties.
public class ExampleViewModel : ReactiveObject { [Reactive] // ReactiveUI.Fody, // - // OnPropertyChanged Name. public string Name { get; set; } public ExampleViewModel() { // OnPropertyChanged("Name"). this.WhenAnyValue(x => x.Name) // IObservable<string> .Subscribe(Console.WriteLine); } }
ReactiveCommand<TIn, TOut>
allows you to work with a command as with an event of type IObservable<TOut>
, which is published whenever the command completes execution. Also, any command has a ThrownExceptions
property of type 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(); // : 42
IObservable<T>
, as with an event that publishes a new value of type T
whenever the state of the object being monitored changes. Simply put, IObservable<T>
is a stream of events, a sequence stretched in time.
IEnumerable<T>
or more specialized, and the event itself would be IObservable<IEnumerable<T>>
. But, as a critically-minded reader correctly notes, this is fraught with serious problems with the performance of the application, especially if there are not a dozen elements in our collection, but a hundred, or even a few thousand!
ObservableCollection<T>
. The DynamicData architecture is based primarily on domain-specific programming concepts. The ideology of use is based on the fact that you control a certain data source, a collection to which the code responsible for synchronizing and changing data has access. Next, you apply a series of statements to the source, with the help of which you can declaratively transform the data without the need to manually create and modify other collections. In fact, with DynamicData you separate read and write operations, and you can only read in a reactive way - therefore, the inherited collections will always be synchronized with the source.
IObservable<T>
, DynamicData defines operations on IObservable<IChangeSet<T>>>
and IObservable<IChangeSet<TValue, TKey>>
, where IChangeSet
is a chunk containing information about the collection change — the type of change and the elements that were affected. This approach can significantly improve the performance of the code for working with collections, written in a reactive style. In this case, IObservable<IChangeSet<T>>
can always be transformed into a regular IObservable<IEnumerable<T>>
, if it becomes necessary to process all elements of the collection at once. If it sounds difficult - do not worry, from the code examples everything will become clear and transparent!
System.Reactive
and which tasks common application developers with GUI can help solve. Let's start with a comprehensive example published by DynamicData on GitHub . In the example, the data source is a SourceCache<Trade, long>
containing a collection of deals. The task is to show only active transactions, transform models into proxy objects, sort the collection.
// System.Collections.ObjectModel, // . ReadOnlyObservableCollection<TradeProxy> list; // , . // Add, Remove, Insert . var source = new SourceCache<Trade, long>(trade => trade.Id); var cancellation = source // . // IObservable<IChangeSet<Trade, long>> .Connect() // . .Filter(trade => trade.Status == TradeStatus.Live) // -. // IObservable<IChangeSet<TrandeProxy, long>> .Transform(trade => new TradeProxy(trade)) // . .Sort(SortExpressionComparer<TradeProxy> .Descending(trade => trade.Timestamp)) // GUI . .ObserveOnDispatcher() // - // System.Collections.ObjectModel. .Bind(out list) // , // . .DisposeMany() .Subscribe();
SourceCache
, which is the data source, the ReadOnlyObservableCollection
also change accordingly. At the same time, when removing elements from the collection, the Dispose
method will be called, the collection will always be updated only in the GUI stream and remain sorted and filtered. Cool, no Dispatcher.Invoke
and complex code!
SourceList
and SourceCache<TObject, TKey>
. It is recommended to use SourceCache
whenever the TObject
has a unique key, otherwise use SourceList
. These objects provide a familiar .NET API for developers to modify data — the Add
, Remove
, Insert
methods, and the like. To convert data sources to IObservable<IChangeSet<T>>
or IObservable<IChangeSet<T, TKey>>
, use the .Connect()
operator. For example, if you have a service that updates a collection of items in the background, you can easily synchronize the list of these items with the GUI, without Dispatcher.Invoke
and architectural excesses:
public class BackgroundService : IBackgroundService { // . private readonly SourceList<Trade> _trades; // . // , , // Publish() Rx. public IObservable<IChangeSet<Trade>> Connect() => _trades.Connect(); public BackgroundService() { _trades = new SourceList<Trade>(); _trades.Add(new Trade()); // ! // . } }
IObservable<IChangeSet<Trade>>
into a ReadOnlyObservableCollection
our view model.
public class TradesViewModel : ReactiveObject { private readonly ReadOnlyObservableCollection<TradeVm> _trades; public ReadOnlyObservableCollection<TradeVm> Trades => _trades; public TradesViewModel(IBackgroundService background) { // , , // System.Collections.ObjectModel. background.Connect() .Transform(x => new TradeVm(x)) .ObserveOn(RxApp.MainThreadScheduler) .Bind(out _trades) .DisposeMany() .Subscribe(); } }
Transform
, Filter
and Sort
, DynamicData includes a host of other operators, supports grouping, logical operations, collection smoothing, using aggregate functions, excluding identical elements, counting elements, and even virtualization at the presentation model level. You can read more about all the operators in the project's README on GitHub .
SourceList
and SourceCache
, the DynamicData library also includes a single-threaded implementation of a modifiable collection — ObservableCollectionExtended
. To synchronize two collections in your view model, declare one of them as ObservableCollectionExtended
, and the other as ReadOnlyObservableCollection
and use the ToObservableChangeSet
operator, which behaves the same as Connect
but is designed to work with ObservableCollection
.
// . ReadOnlyObservableCollection<TradeVm> _derived; // -. var source = new ObservableCollectionExtended<Trade>(); source.ToObservableChangeSet(trade => trade.Key) .Transform(trade => new TradeProxy(trade)) .Filter(proxy => proxy.IsChecked) .Bind(out _derived) .Subscribe();
INotifyPropertyChanged
interface. For example, if you want to be notified of a collection change whenever a property of a particular element changes, use the AutoRefresh
operator and pass the selector of the desired property with the argument. AutoRefesh
and other DynamicData operators will allow you to easily and easily validate a huge number of forms and nested forms that are displayed on the screen!
// IObservable<bool> var isValid = databases .ToObservableChangeSet() // IsValid. .AutoRefresh(database => database.IsValid) // . .ToCollection() // , . .Select(x => x.All(y => y.IsValid)); // ReactiveUI, IObservable<bool> // ObservableAsProperty. _isValid = isValid .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.IsValid);
INotifyPropertyChanged
and ICommand
, and DynamicData takes care of synchronizing collections by implementing INotifyCollectionChanged
, extending the capabilities of reactive extensions and taking care of performance.
Source: https://habr.com/ru/post/445098/