📜 ⬆️ ⬇️

Introduction to ReactiveUI: Studying Teams

Part 1: Introduction to ReactiveUI: we pump properties in ViewModel
Part 2: Introduction to ReactiveUI: Collections

We have already discussed the possibilities of ReactiveUI related to working with properties, building dependencies between them, as well as working with collections. These are one of the main primitives, on the basis of which the development using ReactiveUI is built. Another such primitive are the commands that we consider in this part. Commands encapsulate actions that are performed in response to some event: usually this is a user request or some kind of monitored change. We will learn what can be done with the help of teams in ReactiveUI, discuss the features of their work and find out how teams in ReactiveUI differ from the teams with which we are familiar with WPF and its relatives.
But before moving on to the commands, consider the broader topics related to reactive programming in general: the relationship between Task <T> and IObservable <T>, and what hot and cold sequences are.

Task vs. Iobservable


So, let's draw a parallel between Task <T> (+ async, await, that's all) and IObservable <T>. It will be important for us to understand how to work with teams in ReactiveUI, but the described approach is more widely used and does not hurt to know about it. So: Task <T> is IObservable <T> . But they are certainly not equivalent: IObservable <T> can solve a much larger range of tasks.
It sounds suspicious, doesn't it? Let's figure it out. Immediately see an example:
Task<string> task = Task.Run(() => { Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); return "  "; }); Console.WriteLine(DateTime.Now.ToLongTimeString() + "  -     "); string result = task.Result; Console.WriteLine(DateTime.Now.ToLongTimeString() + "  : " + result); 

We have created a task, it will execute asynchronously and will not prevent us from doing something else immediately after its launch, without waiting for completion. The result is predictable:
18:19:47 We do something before the start of the task result waiting
18:19:47 Starting a long task
18:19:48 We complete the long task
18:19:48 The result: The result of a long task

The first 2 lines will be displayed immediately and may have a different order, as lucky.
And now let's rewrite to IObservable <T>:
 IObservable<string> task = Observable.Start(() => { Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); return "  "; }); Console.WriteLine(DateTime.Now.ToLongTimeString() + "  -     "); string result = task.Wait(); //       Console.WriteLine(DateTime.Now.ToLongTimeString() + "  : " + result); 

The difference is in two lines: IObservable <string> instead of Task <string>, Observable.Start () instead of Task.Run () and task.Wait () instead of task.Result. The result of the work is exactly the same.

Let's look at another well-known technique with the launch of the action after the task is completed:
 //Task task.ContinueWith(t => Console.WriteLine(DateTime.Now.ToLongTimeString() + "  : " + t.Result)); //IObservable task.Subscribe(t => Console.WriteLine(DateTime.Now.ToLongTimeString() + "  : " + t)); 

There is practically no difference again.
')
It turns out that Task <T> can be represented via IObservable <T>, which will produce one element and a completion signal. There is no big philosophical and architectural difference between such approaches, and you can use any. Even async / await is available in both cases: if we are to get the result from the IObservable in the asynchronous method, we can not do the blocking wait through the Wait () method, as in the example, but use await . Moreover, these two approaches can be combined, transformed one representation into another and use the advantages of both.

Hot and cold sequences


Let's discuss one more question concerning the work with observable sequences (observable). They can be of two types: cold (cold) and hot (hot). Cold sequences are passive and begin to generate notifications upon request, at the time of subscribing to them. Hot sequences are active and do not depend on whether someone subscribes to them: notifications are still generated, they just sometimes go into void.
Timer ticks, mouse movement events, network requests are hot sequences. By subscribing to them at some point, we will start receiving relevant notifications. 10 observers will subscribe - notifications will be delivered to everyone. Here is an example with a timer:


The cold sequence is, for example, a query to the database or a file reading line by line. The request or reading is started upon subscription, and OnNext () is called with each new received string. When lines end, OnComplete () will be called. When you re-subscribe, everything repeats again: a new query to the database or opening a file, the return of all received results, a completion signal:




Classic commands ...


We now turn to our current topic - to the teams. Commands, as I mentioned, encapsulate actions taken in response to certain events. Such an event could be the user pressing the "Save" button; then the action to be encapsulated is the save operation. But the command can be executed not only in response to an explicit user action or related indirect events. The signal from the timer, triggered every 5 minutes, regardless of the user, can also initiate the same “Save” command. And although commands are usually used specifically for actions that the user performs in one way or another, you should not neglect their use in other cases.
Commands also let you know if execution is currently available. For example, we want to save not always available, but only when all the required form fields are filled in, and the availability of the command would determine if the button is active in the interface.
Let's see what is the interest of the ICommand team:
 public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); } 

Execute, obviously, executes the command. You can pass a parameter to it, but you should not use this option if the required value can be obtained within the command itself (for example, taken from the ViewModel). Then we understand why. But, of course, there are situations when passing a parameter is the most acceptable option.
CanExecute checks if the command is currently available. It also has a parameter, and everything is the same as with Execute. It is important that CanExecute with a certain parameter value allows or prohibits the execution of a command with only the same parameter value; for other values, the result may differ. It should also be remembered that Execute doesn’t check CanExecute before execution of actions for the possibility of execution; this is the task of the calling code.
The CanExecuteChanged event occurs when the status of the execution opportunity changes and it is worth re-checking CanExecute. For example, when all the fields in the form have been filled out and it became possible to save, you need to initiate the inclusion of a button in the interface. A button with an attached command will find out about this exactly like this.

... and what's wrong with them


The first problem is that the CanExecuteChanged event does not indicate for which parameter value the execution opportunity status has changed. This is the very reason why the use of parameters when calling Execute / CanExecute should be avoided: the ICommand interface with respect to parameters is not particularly consistent. With the same reactive teams, as we shall see, this approach did not get along at all.

The second problem - Execute () returns control only after the completion of the command execution. When a command is executed for a long time, the user is upset because he is faced with a hung interface.
Who would like that?
The program has a command launch button, log output and a progress bar, which in a normal situation should constantly move. The command at startup writes the current time to the log, one and a half seconds does something (for example, loads data) and writes the completion time.

The progress bar stops, the log is updated only when the command is completed, the interface hangs. It turned out bad ...

How to save the situation? Of course, you can implement the command so that it only initiates the execution of the necessary actions in another thread and returns control. But then another problem arises: the user can click on the button again and run the command again, before the previous one is completed. Let's complicate the implementation: let's make CanExecute return false while the task is running. The interface will not hang, the command will not run in parallel several times, we have achieved our goal. But all this needs to be done with your own hands. And in the ReactiveUI team already know how all this and more.



Reactive teams


Let's get acquainted with ReactiveCommand <T>. Don't confuse: there is still a non-generic implementation with the same name: ReactiveCommand (it is in the ReactiveUI.Legacy namespace, and is obviously outdated). This generic parameter does not mean a type of parameter, but a type of result, but we will return to this later.

Immediately try to create and run a command, first dropping everything related to CanExecute. Notice that usually we don’t create any commands directly via the new operator, but use the static ReactiveCommand class that provides the necessary methods.
 var command = ReactiveCommand.Create(); command.Subscribe(_ => { Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); }); command.Execute(null); Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); Console.ReadLine(); 

The ReactiveCommand.Create () method creates synchronous tasks, they are of type ReactiveCommand <object>. Execute () returns control only after completion:
19:01:07 Starting a long task
19:01:08 End a long task
19:01:08 After starting the team

Later we will look at ways to create asynchronous commands, but for now let's take a look at controlling the ability to execute a command.

Ability to execute a command


Let's discuss CanExecute and related features. In addition to what we have already seen (the CanExecute method and the CanExecuteChanged event), the ReactiveCommand provides the IsExecuting and CanExecuteObservable sequences:
 var command = ReactiveCommand.Create(); command.Subscribe(_ => Console.WriteLine(" ")); command.CanExecuteChanged += (o, a) => Console.WriteLine("CanExecuteChanged event: now CanExecute() == {0}", command.CanExecute(null)); command.IsExecuting.Subscribe(isExecuting => Console.WriteLine("IsExecuting: {0}", isExecuting)); command.CanExecuteObservable.Subscribe(canExecute => Console.WriteLine("CanExecuteObservable: {0}", canExecute)); Console.WriteLine("  ,  ..."); command.Execute(null); Console.WriteLine("  "); 

IsExecuting: False
CanExecuteObservable: False
CanExecuteObservable: True
CanExecuteChanged event: now CanExecute () == True
Subscribed to everything, run the command ...
IsExecuting: True
CanExecuteChanged event: now CanExecute () == False
CanExecuteObservable: False
Running command
IsExecuting: False
CanExecuteChanged event: now CanExecute () == True
CanExecuteObservable: True
After running the command

You can especially not pay attention to what happens immediately after the subscription and before the launch of the command: this is initialization. In fact, we immediately return to the current state upon subscription (it turns out a cold first element and hot following ones). And CanExecuteObservable is initially set to false. It seems that when you subscribe, they first give us this value, and then the team determines that we have not provided a mechanism for determining accessibility, and makes the command available by default.
Judging by the output of the program, the command is already unavailable during its execution. This is especially true for asynchronous commands: they will not be run in parallel several times. Thus, CanExecute, CanExecuteObservable and the CanExecuteChanged event depend not only on what we provide for the calculation, but also on whether the command is being executed now. IsExecuting provides information about whether the command is currently being executed, and this can be used, for example, to display some kind of work indicator.

Let's now give the team information on when it can be executed. For this, every method for creating commands in the ReactiveCommand class has overloads that take IObservable <bool> canExecute . The team will subscribe to this sequence and, upon receiving changes, will update their information on the availability of execution. We look:
 var subject = new Subject<bool>(); var command = ReactiveCommand.Create(subject); command.CanExecuteChanged += (o, a) => Console.WriteLine("CanExecuteChanged event: now CanExecute() == {0}", command.CanExecute(null)); command.CanExecuteObservable.Subscribe(canExecute => Console.WriteLine("CanExecuteObservable: {0}", canExecute)); Console.WriteLine("  "); subject.OnNext(true); Console.WriteLine("  "); subject.OnNext(false); Console.WriteLine("    "); subject.OnNext(false); 

Subject here is observable, which we control with our own hands, giving out the necessary values ​​to the team through it. At least it is very convenient for testing. We subscribe to everything, make the execution available, and then two times unavailable. What result will we get?
CanExecuteObservable: False
Making execution available
CanExecuteChanged event: now CanExecute () == True
CanExecuteObservable: True
Making execution unavailable
CanExecuteChanged event: now CanExecute () == False
CanExecuteObservable: False
Once again we make execution inaccessible

It seems everything is expected. Initially, execution is not available. Then the team begins to respond to the changes we are making. Here it is worth only mentioning that when we send the same availability state several times in a row, the team ignores repetitions. Note also that CanExecuteObservable is simply a sequence of values ​​of type bool, and here there is an incompatibility with the fact that the CanExecute method has a parameter. In ReactiveCommand, it is simply ignored.

Ways to invoke a command


We have already seen the command call with the Execute () method. Look at other ways:

IObservable <T> ExecuteAsync (object parameter)
There is one feature: the command will not start until the subscription to the result of ExecuteAsync (). We use it:
 command.ExecuteAsync().Subscribe(); 

However, the synchronous command does not become asynchronous from this. Of course, ExecuteAsync () will return control immediately, but execution has not yet started! A Subscribe (), which starts it, returns control only after the completion of the command. In fact, now we have written the equivalent of Execute (). However, this is natural, because ExecuteAsync () returns a cold sequence and subscribing to it initiates the execution of our long task. And it is executed in the current thread. Although this can be fixed by explicitly specifying where to subscribe:
 command.ExecuteAsync().SubscribeOn(TaskPoolScheduler.Default).Subscribe(); 

Now the TPL scheduler is responsible for completing the subscription. Accordingly, the subscription will be executed in something like Task.Run (), and everything will work as it should. But to do this in reality is not worth it, and this example only shows one of the possibilities. All sorts of planners are many, and one day we will touch on this topic.

Task <T> ExecuteAsyncTask (object parameter)
Unlike ExecuteAsync (), this method immediately runs the command. We try:
 command.ExecuteAsyncTask(); 

We were returned Task <>, but there is still no happiness in life. ExecuteAsyncTask () also returns control only after the completion of the command, and gives us the already completed task. Some kind of setup.

InvokeCommand ()
This method allows you to conveniently customize the command call when a signal appears in the sequence (for example, a property change). Like that:
 this.WhenAnyValue(x => x.FullName).Where(x => !string.IsNullOrEmpty(x)).InvokeCommand(this.Search); //     this.WhenAnyValue(x => x.FullName).Where(x => !string.IsNullOrEmpty(x)).InvokeCommand(this, x => x.Search); // ,       

So far we have not found a way to execute the command asynchronously. Of course, you can use the ExecuteAsync () method and assign a scheduler to perform the subscription, but this is a crutch. Moreover, WPF does not know about this method and will still call Execute () and hang itself.

Asynchronous reactive commands


Synchronous commands make sense when actions are performed quickly and there is no reason to complicate things. And for long tasks, asynchronous commands are needed. Here two methods help us: ReactiveCommand.CreateAsyncObservable () and ReactiveCommand.CreateAsyncTask (). The difference between them is only in what the action performed is expressed. We return to the first section of the article and how to present asynchronous tasks.

Let's see CreateAsyncObservable:
 var action = new Action(() => { Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); Thread.Sleep(1000); Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); }); var command = ReactiveCommand.CreateAsyncObservable(_ => Observable.Start(action)); Console.WriteLine(DateTime.Now.ToLongTimeString() + "  ..."); command.Execute(42); Console.WriteLine(DateTime.Now.ToLongTimeString() + "   "); Console.ReadLine(); 

2:33:50 Run the command ...
2:33:50 After starting the team
2:33:50 Starting a long task
2:33:51 ending a long task

Hooray! Execute is no longer blocked until the command is completed, and the interface will not hang. With ExecuteAsync and ExecuteAsyncTask, everything is the same: there are no locks.

Now reateAsyncTask:
 var command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(action)); var command = ReactiveCommand.CreateAsyncTask(_ => doSomethingAsync()); //   Task<T> var command = ReactiveCommand.CreateAsyncTask(async _ => await doSomethingAsync()); 

Both of the described methods have many overloads that support, for example, the transfer of CanExecuteObservable or the possibility of cancellation.
In addition, a result can be returned from an asynchronous command. Generic-parameter T from ReactiveCommand <T> is just the type of the result of the command:
 ReactiveCommand<int> command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(() => 42)); var result = await command.ExecuteAsync(); // result == 42 

And you can immediately send it somewhere:
 var command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(() => 42)); command.Subscribe(result => _logger.Log(result)); _answer = command.ToProperty(this, this.Answer); //        (ObservableToPropertyHelper) 

It is guaranteed that the result is returned in the UI stream. Therefore, by the way, it is contraindicated to perform some long actions as a reaction to the execution of a command in Subscribe. In the case of synchronous commands, the own result cannot be returned, the command is of type ReactiveCommand <object> and the values ​​with which the command was launched will be returned.

We catch exceptions that occur when teams work


Exceptions to the operation of commands can occur constantly, especially if we are talking about some kind of data loading. Accordingly, it is necessary to learn how to catch and process them. Where and how to do it?

Synchronous commands
 var command = ReactiveCommand.Create(); command.Subscribe(_ => { throw new InvalidOperationException(); }); command.ThrownExceptions.Subscribe(e => Console.WriteLine(e.Message)); command.Execute(null); //   command.ExecuteAsync().Subscribe(); //  InvalidOperationException await command.ExecuteAsyncTask(); //  InvalidOperationException 

Since the calls of all methods are synchronous, one would expect that they will throw exceptions. But everything is not so simple. Execute () does not actually throw an exception. It is implemented in such a way that all exceptions simply swallow. The other two methods throw exceptions immediately, as expected.

With asynchronous commands, everything is much more interesting.
ReactiveCommand provides a ThrownExceptions sequence through which exceptions that occur during the execution of asynchronous commands come. There is no difference between Observable and Task teams. Let's create teams for our experiment:
 var command = ReactiveCommand.CreateAsyncTask(_ => Task.Run(() => { throw new InvalidOperationException(); })); /* 1  */ var command = ReactiveCommand.CreateAsyncObservable(_ => Observable.Start(() => { throw new InvalidOperationException(); })); /* 2  */ command.ThrownExceptions.Subscribe(e => Console.WriteLine(e.Message)); 

And try different ways to invoke the command:
 command.Execute(null); //   ThrownExceptions command.ExecuteAsyncTask(); // InvalidOperationException  -   (Task   ) command.ExecuteAsync().Subscribe(); // InvalidOperationException  -  ,  Task    await command.ExecuteAsync(); //  InvalidOperationException     ThrownExceptions await command.ExecuteAsyncTask(); //  InvalidOperationException     ThrownExceptions var subj = new Subject<Unit>(); subj.InvokeCommand(command); subj.OnNext(Unit.Default); //   ThrownExceptions 

If we somehow subscribe to the command itself (for example, command.ToProperty (...)), then when an exception occurs, the OnError () command is not sent.

In this example, it seems strange examples in which exceptions will occur "sometime". In the TPL, this was necessary so that uncaught exceptions did not disappear without a trace. Here it was possible to transfer them through ThrownExceptions and not to throw "in the future." But such is the implementation, and in the next version ReactiveUI will seem to change something in this respect.

Cancel asynchronous commands


Commands that can take a long time would be good to be able to cancel. There are many ways to do this. Create an asynchronous command, which will loop the message in a loop until it is canceled:
 var command = ReactiveCommand.CreateAsyncTask(async (a, t) => { while (!t.IsCancellationRequested) { Console.WriteLine(""); await Task.Delay(300); } Console.WriteLine(""); }); 

The first way to cancel is canceling a subscription based on ExecuteAsync ():
 var disposable = command.ExecuteAsync().Subscribe(); Thread.Sleep(1000); disposable.Dispose(); 

The second way is to transfer a token via ExecuteAsyncTask ():
 var source = new CancellationTokenSource(); command.ExecuteAsyncTask(ct: source.Token); Thread.Sleep(1000); source.Cancel(); 

But what to do if we want to cancel a command that is launched by the Execute () method, that is, when called, for example, by WPF itself? This is also easy to do, for this we need to wrap the Task in IObservable and use the TakeUntil () method. I will give an example with calling another command to cancel:
 Func<CancellationToken, Task> action = async (ct) => { while (!ct.IsCancellationRequested) { Console.WriteLine(""); await Task.Delay(300); } Console.WriteLine(""); }; IReactiveCommand<object> cancelCommand = null; var runCommand = ReactiveCommand.CreateAsyncObservable(_ => Observable.StartAsync(action).TakeUntil(cancelCommand)); cancelCommand = ReactiveCommand.Create(runCommand.IsExecuting); runCommand.Execute(null); Thread.Sleep(1000); cancelCommand.Execute(null); 

The command is executed until the next notification appears in the cancelCommand sequence. In fact, there may not even be a command at the place of cancelCommand, but any observable sequence.

In all these methods there is one subtlety: when we initiate cancellation, the command is immediately considered complete and available for re-execution, but if the cancellation token is ignored by the task, then some actions can continue inside. This should also be taken into account if the command becomes canceled. This is especially the case when we cancel a command in which there is no Task <T> at all:
 Action action = () => { ... }; var runCommand = ReactiveCommand.CreateAsyncObservable(_ => Observable.Start(action).TakeUntil(cancelCommand)); 

Here, the action will be executed after we cancel the execution and the command will again be available for the call. Only it will happen behind the curtain, and can lead to very unexpected results.

Team unification


You can easily create a command that calls other commands:
 RefreshAll = ReactiveCommand.CreateCombined(RefreshNotifications, RefreshMessages); 

In this case, the command is executable when all commands from the transferred set are executable. There is also an overload into which a separate canExecute can be passed to indicate when the command can be executed. In this case, the possibility of executing each command and the parameter passed are taken into account.



An example of working with teams


Let's write a small example showing the use of commands.
We will do a search. Well, conditional search. In fact, we will mimic the activity for a second and a half and return as a result a collection of several query modifications. We will also support canceling the search, clearing the old result when starting a new search and the ability to perform a search automatically when the input data changes.
We are looking at the view model:
 public class SearchViewModel : ReactiveObject { [Reactive] public string SearchQuery { get; set; } [Reactive] public bool AutoSearch { get; set; } private readonly ObservableAsPropertyHelper<ICollection<string>> _searchResult; public ICollection<string> SearchResult => _searchResult.Value; public IReactiveCommand<ICollection<string>> Search { get; } public IReactiveCommand<object> CancelSearch { get; } public SearchViewModel() { Search = ReactiveCommand.CreateAsyncObservable( this.WhenAnyValue(vm => vm.SearchQuery).Select(q => !string.IsNullOrEmpty(q)), _ => Observable.StartAsync(ct => SearchAsync(SearchQuery, ct)).TakeUntil(CancelSearch) ); CancelSearch = ReactiveCommand.Create(Search.IsExecuting); Observable.Merge( Search, Search.IsExecuting.Where(e => e).Select(_ => new List<string>())) .ToProperty(this, vm => vm.SearchResult, out _searchResult); this.WhenAnyValue(vm => vm.SearchQuery) .Where(x => AutoSearch) .Throttle(TimeSpan.FromSeconds(0.3), RxApp.MainThreadScheduler) .InvokeCommand(Search); } private async Task<ICollection<string>> SearchAsync(string query, CancellationToken token) { await Task.Delay(1500, token); return new List<string>() { query, query.ToUpper(), new string(query.Reverse().ToArray()) }; } } 

Of course, there are shortcomings. One of them in the auto search. If during the execution of the search the user changes the query, the current search will not be stopped and it will be completed first, and then a search will be performed on a new query. However, correcting this is a matter of a couple of lines. Or, say, it is strange not to clear the search results in the case when the user has erased the entire query. But we will leave such complex examples to the following parts, but for now we restrict ourselves to the existing logic. Once again I will draw attention to the fact that, in general, not the most primitive behavior of our search engine is concentrated in one place and is described fairly briefly and clearly.

Look at XAML:
 <Grid DataContext="{Binding ViewModel, ElementName=Window}"> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> <RowDefinition Height="Auto"/> </Grid.RowDefinitions> <StackPanel> <Label>Search query:</Label> <TextBox Margin="10, 5" Text="{Binding SearchQuery, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> </StackPanel> <ListBox Grid.Row="1" ItemsSource="{Binding SearchResult}"/> <Grid Grid.Row="2"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"></ColumnDefinition> <ColumnDefinition Width="Auto"></ColumnDefinition> </Grid.ColumnDefinitions> <ProgressBar Margin="10" x:Name="SearchExecutingProgressBar" /> <StackPanel Orientation="Horizontal" Grid.Column="1"> <CheckBox VerticalAlignment="Center" IsChecked="{Binding AutoSearch, Mode=TwoWay}">Auto search</CheckBox> <Button Margin="10" Command="{Binding Search}">Run</Button> <Button Margin="10" Command="{Binding CancelSearch}">Cancel</Button> </StackPanel> </Grid> </Grid> 

The question here may call ProgressBar. I wanted it included in the search process. But in the Search command, the IsExecuting property is not a bool, but a sequence, and binding to it in XAML will not work. Therefore, we will do the binding in the constructor of our view:
 public partial class MainWindow : Window { public SearchViewModel ViewModel { get; } public MainWindow() { ViewModel = new SearchViewModel(); InitializeComponent(); this.WhenAnyObservable(w => w.ViewModel.Search.IsExecuting).BindTo(SearchExecutingProgressBar, pb => pb.IsIndeterminate); } } 

Yes, in ReactiveUI there is support for such binding here, and in theory I could do them all this way. But we will talk about binders another time, but for now I have limited myself to what I can't do without.

Look at the result



Hooray!It works as it should, the interface does not hang during the search, cancellation works, autosearch works , the customer is ecstatic and gives a triple bonus .



In the next episode


So let's summarize a little. In this part, we figured out what the relationship is between Task <T> and IObservable <T>. We compared hot and cold sequences. But our main topic was the team. We learned how to create synchronous and asynchronous commands and call them in different ways. We figured out how to enable and disable the ability to execute them, as well as how to intercept errors in asynchronous commands. In addition, we figured out how to cancel asynchronous commands.
I wanted to touch upon the view model in this part, but somehow I didn’t expect the teams to stretch into such a sheet. Therefore, this time it will not be. In the next part, we will consider either bindings, or testing and planners.
Do not switch!

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


All Articles