保存订阅状态以便稍后恢复

时间:2013-06-30 12:39:52

标签: c# system.reactive

更新 - 已解决

最终的解决方案与布兰登的建议略有不同,但他的回答让我走上正轨。

class State
{
  public int Offset { get; set; }
  public HashSet<string> UniqueImageUrls = new HashSet<string>();
}

public IObservable<TPicture> GetPictures(ref object _state)
{
  var localState = (State) _state ?? new State();
  _state = localState;

  return Observable.Defer(()=>
  {
    return Observable.Defer(() => Observable.Return(GetPage(localState.Offset)))
      .SubscribeOn(TaskPoolScheduler.Default)
      .Do(x=> localState.Offset += 20)
      .Repeat()
      .TakeWhile(x=> x.Count > 0)
      .SelectMany(x=> x)
      .Where(x=> !localState.UniqueImageUrls.Contains(x.ImageUrl))
      .Do(x=> localState.UniqueImageUrls.Add(x.ImageUrl));
  });
}

IList<TPicture> GetPage(int offset)
{
  ... 
  return result;
}

原始问题

我目前正在努力解决以下问题。下面显示的PictureProvider实现正在使用偏移量变量,该变量用于提供实际数据的后端服务的分页结果。我想要实现的是一个优雅的解决方案,使当前偏移量可供observable的消费者使用,以允许稍后在正确的偏移量处恢复可观察序列。 GetPictures()的 intialState 参数已经解决了恢复问题。

也欢迎以更像RX方式改进代码的建议。我实际上不确定Task.Run()的内容是否合适。

  public class PictureProvider :
    IPictureProvider<Picture>
  {
    #region IPictureProvider implementation

    public IObservable<Picture> GetPictures(object initialState)
    {
      return Observable.Create<Picture>((IObserver<Picture> observer) =>
      {
        var state = new ProducerState(initialState);
        ProducePictures(observer, state);
        return state;
      });
    }

    #endregion

    void ProducePictures(IObserver<Picture> observer, ProducerState state)
    {
      Task.Run(() =>
      {
        try
        {
          while(!state.Terminate.WaitOne(0))
          {
            var page = GetPage(state.Offset);

            if(page.Count == 0)
            {
              observer.OnCompleted();
              break;
            }

            else
            {
              foreach(var picture in page)
                observer.OnNext(picture);


              state.Offset += page.Count;
            }
          }
        }

        catch (Exception ex)
        {
          observer.OnError(ex);
        }

        state.TerminateAck.Set();
      });
    }

    IList<Picture> GetPage(int offset)
    {
      var result = new List<Picture>();

      ... boring web service call here

      return result;
    }

    public class ProducerState :
      IDisposable
    {
      public ProducerState(object initialState)
      {
        Terminate = new ManualResetEvent(false);
        TerminateAck = new ManualResetEvent(false);

        if(initialState != null)
          Offset = (int) initialState;
      }

      public ManualResetEvent Terminate { get; private set; }
      public ManualResetEvent TerminateAck { get; private set; }

      public int Offset { get; set; }

      #region IDisposable implementation

      public void Dispose()
      {
        Terminate.Set();
        TerminateAck.WaitOne();

        Terminate.Dispose();
        TerminateAck.Dispose();
      }

      #endregion
    }
  }

2 个答案:

答案 0 :(得分:1)

我建议重构你的界面以将状态作为数据的一部分。现在,客户有他们需要重新订阅的地方。

此外,一旦开始使用Rx,您会发现很少需要使用ManualResetEvent这样的同步原语。如果您重构代码以便检索每个页面都是它自己的Task,那么您可以删除所有同步代码。

此外,如果您在GetPage中呼叫“无聊的网络服务”,那么只需将其设为异步即可。这样就无需调用Task.Run和其他好处。

这是一个使用.NET 4.5 async / await语法的重构版本。它也可以在没有异步/等待的情况下完成。我还添加了一个使用GetPageAsync的{​​{1}}方法,万一你真的无法将你的webservice调用转换为异步

Observable.Run

客户只需添加/// <summary>A set of pictures</summary> public struct PictureSet { public int Offset { get; private set; } public IList<Picture> Pictures { get; private set; } /// <summary>Clients will use this property if they want to pick up where they left off</summary> public int NextOffset { get { return Offset + Pictures.Count; } } public PictureSet(int offset, IList<Picture> pictures) :this() { Offset = offset; Pictures = pictures; } } public class PictureProvider : IPictureProvider<PictureSet> { public IObservable<PictureSet> GetPictures(int offset = 0) { // use Defer() so we can capture a copy of offset // for each observer that subscribes (so multiple // observers do not update each other's offset return Observable.Defer<PictureSet>(() => { var localOffset = offset; // Use Defer so we re-execute GetPageAsync() // each time through the loop. // Update localOffset after each GetPageAsync() // completes so that the next call to GetPageAsync() // uses the next offset return Observable.Defer(() => GetPageAsync(localOffset)) .Select(pictures => { var s = new PictureSet(localOffset, pictures); localOffset += pictures.Count; }) .Repeat() .TakeWhile(pictureSet => pictureSet.Pictures.Count > 0); }); } private async Task<IList<Picture>> GetPageAsync(int offset) { var data = await BoringWebServiceCallAsync(offset); result = data.Pictures.ToList(); } // this version uses Observable.Run() (which just uses Task.Run under the hood) // in case you cannot convert your // web service call to be asynchronous private IObservable<IList<Picture>> GetPageAsync(int offset) { return Observable.Run(() => { var result = new List<Picture>(); ... boring web service call here return result; }); } } 电话即可获取SelectMany。如果愿意,他们可以选择存储IObservable<Picture>

pictureSet.NextOffset

答案 1 :(得分:0)

我没有考虑如何保存订阅状态,而是考虑如何重放输入的状态(即我尝试创建一个可序列化的ReplaySubject,在恢复时,只会重新订阅并回收到目前的状况)。