ReactiveUI 7.0 how to handle with observables that are disposed when exception is thrown

自闭症网瘾萝莉.ら 提交于 2019-12-25 08:37:50

问题


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.


回答1:


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:

  1. handle the exceptions in the command code so that ThrownExceptions never actually gets fired.
  2. 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);
 }

}



回答2:


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



来源:https://stackoverflow.com/questions/40824104/reactiveui-7-0-how-to-handle-with-observables-that-are-disposed-when-exception-i

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!