ReactiveUI 7.0如何处理抛出异常时处置的observable

时间:2016-11-26 23:10:29

标签: system.reactive reactiveui

我真的开始挖掘这个rx的东西了...基本上,我跟着this video一起只是为了在我开始真正使用它之前更多地了解ReactiveUI!

我正在尝试创建一种情况,当我们使用 WhenAnyValue 来执行受限制的搜索式搜索。并且,如果搜索函数抛出异常,我想在视图模型上设置一个名为IsError的属性(所以我可以显示X或其他东西)。这是我工作的ViewModel的重要部分:

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);

这很有效。当我的GetFilteredAsync抛出异常时,SearchCmmand.ThrownExceptions会被调用,我可以设置IsError属性。

然而,当SearchCmmand.ThrownExceptions第一次发生时,this.WhenAnyValue(v => v.SearchText)停止工作。我可以看到它被处理掉了。对SearchText的后续更改不会调用该命令。 (虽然如果我有一个绑定按钮的命令仍然有效)

这似乎是预期的行为,但我们怎样才能让观察能够再次发挥作用?我意识到我可以将它全部包装在try / catch中并返回一些不例外的东西,但是,我在video(大约39:03)中看到,在他的情况下,searchtext继续工作之后抛出异常? (该视频的源代码是here)。

我还看到here关于UserError的内容,但现在标记为Legacy。

2 个答案:

答案 0 :(得分:1)

好的,所以我有一些工作,我会发布它。我必须处理几个问题。一个是我在我的命令异步任务代码中设置我的IsError=false属性的事实(它在后台线程上触发并因此引发异常)而另一个是在处理之后如何重新订阅observable ThrownExceptions冒泡。我找到了两种方法/解决方法:

  1. 处理命令代码中的异常,以便ThrownExceptions永远不会被触发。
  2. 如果ThrownExceptions被触发,则处理并重新订阅WhenAnyValue observable以使其继续运行。 (这需要将变量保存到WhenAnyValue对象。)
  3. 这里是整个视图模型代码似乎工作。警告:我自己是rx / rxui的新手,我不知道这是否是做这一切的最好方法!我正在成像可能有更好的方法!

    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);
     }
    
    }
    

答案 1 :(得分:0)

您可以使用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);
            });
    }
}

看看这个: https://docs.reactiveui.net/en/user-guide/interactions/index.html