I'm really starting to dig this rx thing... Basically, I am following along with this video just to teach myself more about ReactiveUI before I start using it for real!
I am trying to create a situation when we use WhenAnyValue to perform a throttled search-as-you-type. And, if the search function throws an exception, I want to set a property on the view model called IsError
(so I can show an X or something). This the important parts of the ViewModel I have working:
public ReactiveCommand<string, IEnumerable<DictItem>> SearchCmmand;
... in vm constructor:
//create our command async from task. executes on worker thread
SearchCmmand = ReactiveCommand.CreateFromTask<string, IEnumerable<DicItem>>(async x => {
this.IsError = false;
//this may throw an exception:
return await GetFilteredAsync(this.SearchText);
});
//SearchCommand is subscribable.
//set the Filtered Items property. executes on main thread
SearchCmmand.Subscribe(filteredItems => {
this.FilteredItems = filteredItems;
});
//any unhandled exceptions that are thown in SearchCommand will bubble up through the ThrownExceptions observable
SearchCmmand.ThrownExceptions.Subscribe(ex=> {
this.IsError = true;
//but after this, then the WhenAnyValue no longer continues to work.
//how to get it back?
});
//invoke the command when SearchText changes
this.WhenAnyValue(v => v.SearchText)
.Throttle(TimeSpan.FromMilliseconds(500))
.InvokeCommand(SearchCmmand);
And this works. When my GetFilteredAsync
throws an exception, the SearchCmmand.ThrownExceptions
gets called and I can set my IsError
property.
However, when SearchCmmand.ThrownExceptions
happens the first time, the this.WhenAnyValue(v => v.SearchText)
stops working. I can see that it gets disposed. Subsequent changes to SearchText do not invoke the command. (though the command still works if I have a button bound to it)
It seems this is intended behaviour, but how can we get the observable working again? I realize that I could just wrap it all in a try/catch and return something that is not an exception, however, I see in the video (around 39:03) that in his case the searchtext continues to work after the exception is thrown? (the source code for that vid is here).
i also see here something about UserError
, but that's now marked as Legacy.
Ok so I have got something working, i though i'd post it. There were a couple issues i had to deal with. One was the fact that i was setting my IsError=false
property inside my command async task code, (which fires on the background thread and hence throws an exception) and the other was dealing with how to re-subscribe the observable after the ThrownExceptions bubbles up. There are 2 approaches/workarounds that I found worked:
- handle the exceptions in the command code so that ThrownExceptions never actually gets fired.
- if ThrownExceptions does get fired, then dispose and re-subscribe the WhenAnyValue observable so that it keeps going. (this requires keeping a variable to the WhenAnyValue object.)
here is the entire view model code that seems to work. WARNING: being new to rx/rxui myself, i don't know if this is the best way to do all of this! I'm imaginging there could be better ways!
public class SearchViewModel1 : ReactiveObject {
IEnumerable<DictItem> itemList; //holds the master items. used like a repo (just for demo, i'd use a separate repo or service class for real)
ObservableAsPropertyHelper<bool> _isBusy;
public bool IsBusy {
get { return _isBusy.Value; }
}
bool _isError;
public bool IsError {
get { return _isError; }
set { this.RaiseAndSetIfChanged(ref _isError, value); }
}
//the filtered items property that we want to bind our list to
IEnumerable<DictItem> _filteredItems;
public IEnumerable<DictItem> FilteredItems {
get { return _filteredItems; }
set { this.RaiseAndSetIfChanged(ref _filteredItems, value); }
}
//the search text, this will be bound
//and this viewmodel will respond to changes to the property.
string _searchText;
public string SearchText {
get { return _searchText; }
set { this.RaiseAndSetIfChanged(ref _searchText, value); }
}
//this is the reacive command that takes a string as a parameter,
public ReactiveCommand<string, IEnumerable<DictItem>> SearchCmmand { get; set; }
//a reference to our observable in case we lose it and need to resubscribe
IDisposable whenAnySearchText;
//convenience method to set the IsError property. can be called by a worker thread
void SetIsErrorFromWorkerThread(bool isError) {
Observable.Return(isError)
.SubscribeOn(RxApp.MainThreadScheduler)
.Subscribe(b => this.IsError = b);
}
//constructor is where we wire it all up
public SearchViewModel1(IEnumerable<DictItem> itemList) {
this.itemList = itemList;
FilteredItems = itemList;
//this observable keeps track of when SearchText is blank.
var searchTextHasValue = this.WhenAnyValue(x => x.SearchText)
.Select(x => !string.IsNullOrWhiteSpace(x));
//create our command async from task.
//it will only actually fire if searchTextHasValue is true.
SearchCmmand = ReactiveCommand.CreateFromTask<string, IEnumerable<DictItem>>(async x => {
SetIsErrorFromWorkerThread(false);
//first we'll try to capture any exceptions here, so we don't lose the observable.
try {
return await GetFilteredAsync(SearchText, itemList);
} catch (Exception ex) {
SetIsErrorFromWorkerThread(true);
return Enumerable.Empty<DictItem>();
}
},
searchTextHasValue);
//searchCommand is subscribable. set the Filtered Items property synchronous here on main thread
SearchCmmand.Subscribe(filteredItems => {
FilteredItems = filteredItems;
});
//any unhandled exceptions that are thown in SearchCommand will bubble up through the ThrownExceptions observable
SearchCmmand.ThrownExceptions.Subscribe(ex => {
//note: because we are handling exceptions in the command code,
//this should be a very last-case and never-happen scenario.
//but we seem to be able to recover by re-subscribing the observable
IsError = true;
//we have lost the subscription. so set it again?
//is this even a good idea?
whenAnySearchText.Dispose();
whenAnySearchText = this.WhenAnyValue(v => v.SearchText)
.Throttle(TimeSpan.FromMilliseconds(500))
.InvokeCommand(SearchCmmand);
});
//the IsBusy can just be wired from the Command observable stream
_isBusy = SearchCmmand.IsExecuting.ToProperty(this, vm => vm.IsBusy);
//bind our whenAnySearchText
whenAnySearchText = this.WhenAnyValue(v => v.SearchText)
.Throttle(TimeSpan.FromMilliseconds(500))
.InvokeCommand(SearchCmmand);
}
//the task to run the search/filter
async Task<IEnumerable<DictItem>> GetFilteredAsync(string filterText, IEnumerable<DictItem> items) {
await Task.Delay(1000);
if (filterText.Length == 5) {
throw new InvalidOperationException("You cannot search 5 characters! Why? No reason, it's contrived.");
}
return items.Where(x => x.Name.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) >= 0);
}
}
you can use interactions
public enum ErrorRecoveryOption
{
Retry,
Abort
}
public static class Interactions
{
public static readonly Interaction<Exception, ErrorRecoveryOption> Errors = new Interaction<Exception, ErrorRecoveryOption>();
}
public class SomeViewModel : ReactiveObject
{
public async Task SomeMethodAsync()
{
while (true)
{
Exception failure = null;
try
{
DoSomethingThatMightFail();
}
catch (Exception ex)
{
failure = ex;
}
if (failure == null)
{
break;
}
// this will throw if nothing handles the interaction
var recovery = await Interactions.Errors.Handle(failure);
if (recovery == ErrorRecoveryOption.Abort)
{
break;
}
}
}
}
public class RootView
{
public RootView()
{
Interactions.Errors.RegisterHandler(
async interaction =>
{
var action = await this.DisplayAlert(
"Error",
"Something bad has happened. What do you want to do?",
"RETRY",
"ABORT");
interaction.SetOutput(action ? ErrorRecoveryOption.Retry : ErrorRecoveryOption.Abort);
});
}
}
Look at this :
https://docs.reactiveui.net/en/user-guide/interactions/index.html