无法取消订阅Rx

时间:2016-04-12 17:33:20

标签: c# winforms task system.reactive reactive-programming

背景

我正在编写一些执行以下操作的软件:

  1. 用户点击“开始”。
  2. 启动任务,执行一些工作并启动事件以更新GUI。
  3. observable使用任务中的事件,并将数据打印到GUI中的富文本框。
  4. 当我第一次点击“开始”时,一切正常,但不是在那之后。我第一次点击开始,我得到一个看起来像的输出:

    Clicking START once

    这看起来没问题,这里没有错。但是,当我第二次点击START时,我得到以下输出。

    Clicking START again

    现在,我相信我知道为什么会发生这种情况。据我所知,我的观察者从第一次点击START时从未取消订阅,因此所有内容都被打印两次。点击开始按钮时,会发生以下情况:

        /// <summary>
        /// Starts the test.
        /// </summary>
        /// <param name="sender">The "start" button.</param>
        /// <param name="e">Clicking on the "start" button.</param>
        private void button_go_Click(object sender, RoutedEventArgs e)
        {
            var uiContext = SynchronizationContext.Current;
            var results = Observable.FromEventPattern<TestResultHandler, TestResultArgs>(
                handler => (s, a) => handler(s, a),
                handler => this.myTest.Results += handler,
                handler => this.myTest.Results -= handler)
                .ObserveOn(uiContext)
                .Subscribe(x => this.richTextBox_testResults.Document.Blocks.Add(new Paragraph(new Run(x.EventArgs.Results))));
    
            // start running the test
            this.runningTest = new Task(() => { this.myTest.Run(); });
            this.runningTest.Start();
    
            // block on purpose, wait for the task to finish
            this.runningTest.Wait();
        }
    

    我不熟悉.NET中的反应式扩展(已经使用它不到一个小时)。我基本上使用了Stephen Cleary在C#cookbook中的Concurrency中的一个例子,以便在这里开始。尽管如此,我相信我有点知道问题是什么......

    提议的计划

    如果我可以取消订阅观察,那么我认为这个问题会消失。我认为这有点复杂,但是,基于ReactiveX.io使用的反应模式,听起来我实际上应该只是在它实际存在的时间段内巧​​妙地观察任务,然后自然地取消订阅。这是我调试这个问题的时候,我并不是100%确定这个取消订阅的东西实际上会解决这个问题。

    问题

    有没有办法取消订阅观察?或者,有没有办法只在存在期间观察我的任务?

1 个答案:

答案 0 :(得分:3)

Rx使用IDisposable界面,您可以取消订阅尚未自然结束的可观察订阅。如果您的observable发送OnCompletedOnError通知,则会自动为您处理订阅。

这种方法的一个明显优势是,您可以创建一个CompositeDisposable来汇总所有订阅,以启用单个取消订阅。这比删除事件处理程序所需的分离效果要好得多。

在您的代码中,您并未结束订阅,因此,对于您button_go的每次点击,您都需要创建新订阅。

您可以使用四种解决方案,每种解决方案都有所不同。

(1)

请记住,需要致电.Dispose()的关键是,如果您的可观察的替代品没有自然结束并且您希望它结束​​。因此,您只需在查询中添加.Take(1)即可在生成一个值后自然结束。

private void button_go_Click(object sender, RoutedEventArgs e)
{
    var uiContext = SynchronizationContext.Current;
    var results = Observable.FromEventPattern<TestResultHandler, TestResultArgs>(
        handler => (s, a) => handler(s, a),
        handler => this.myTest.Results += handler,
        handler => this.myTest.Results -= handler)
        .Take(1)
        .ObserveOn(uiContext)
        .Subscribe(x => this.richTextBox_testResults.Document.Blocks.Add(new Paragraph(new Run(x.EventArgs.Results))));

    // start running the test
    this.runningTest = new Task(() => { this.myTest.Run(); });
    this.runningTest.Start();

    // block on purpose, wait for the task to finish
    this.runningTest.Wait();
}

然后会自动为您处理订阅。

(2)

您可以使用SerialDisposable来管理每个订阅。 MSDN文档将其描述为:

  

代表一次性用品,其下面的一次性用品可以换成另一种一次性用品,这样可以处理以前的一次性用品。

您的代码将如下所示:

private SerialDisposable _results = new SerialDisposable();

private void button_go_Click(object sender, RoutedEventArgs e)
{
    var uiContext = SynchronizationContext.Current;
    _results.Disposable = Observable.FromEventPattern<TestResultHandler, TestResultArgs>(
        handler => (s, a) => handler(s, a),
        handler => this.myTest.Results += handler,
        handler => this.myTest.Results -= handler)
        .ObserveOn(uiContext)
        .Subscribe(x => this.richTextBox_testResults.Document.Blocks.Add(new Paragraph(new Run(x.EventArgs.Results))));

    // start running the test
    this.runningTest = new Task(() => { this.myTest.Run(); });
    this.runningTest.Start();

    // block on purpose, wait for the task to finish
    this.runningTest.Wait();
}

(3)

您始终可以确保只创建一次订阅。

private IDisposable _results = null;

private void button_go_Click(object sender, RoutedEventArgs e)
{
    if (_results == null)
    {
        var uiContext = SynchronizationContext.Current;
        _results = Observable.FromEventPattern<TestResultHandler, TestResultArgs>(
            handler => (s, a) => handler(s, a),
            handler => this.myTest.Results += handler,
            handler => this.myTest.Results -= handler)
            .ObserveOn(uiContext)
            .Subscribe(x => this.richTextBox_testResults.Document.Blocks.Add(new Paragraph(new Run(x.EventArgs.Results))));
    }

    // start running the test
    this.runningTest = new Task(() => { this.myTest.Run(); });
    this.runningTest.Start();

    // block on purpose, wait for the task to finish
    this.runningTest.Wait();
}

(4)

最后一种方法是将整个操作包含在一个可观察的内容中,并针对每个新订阅进行煽动。这是使用Rx的正确方法。 Observable应该保持自己的状态,以便订阅可以100%相互独立。

现在,您的代码使用this.myTest&amp; this.runningTest所以它显然有状态。你应该尝试删除这些。但是,如果不这样做,您的代码将如下所示:

private void button_go_Click(object sender, RoutedEventArgs e)
{
    var uiContext = SynchronizationContext.Current;
    Observable
        .Create<System.Reactive.EventPattern<TestResultArgs>>(o =>
        {
            var subscription =
                Observable
                    .FromEventPattern<TestResultHandler, TestResultArgs>(
                        h => this.myTest.Results += h,
                        h => this.myTest.Results -= h)
                    .Take(1)
                    .Subscribe(o);
            this.runningTest = new Task(() => { this.myTest.Run(); });
            this.runningTest.Start();
            return subscription;
        })
        .ObserveOn(uiContext)
        .Subscribe(x => this.richTextBox_testResults.Document.Blocks.Add(new Paragraph(new Run(x.EventArgs.Results))));

    // block on purpose, wait for the task to finish
    this.runningTest.Wait();
}

理想情况下,您应该将myTest的创建和销毁纳入observable。

所以,我倾向于做这样的事情:

private void button_go_Click(object sender, RoutedEventArgs e)
{
    var uiContext = SynchronizationContext.Current;
    Observable
        .Create<System.Reactive.EventPattern<TestResultArgs>>(o =>
        {
            var myTest = new MyTest();
            var subscription =
                Observable
                    .FromEventPattern<TestResultHandler, TestResultArgs>(
                        h => myTest.Results += h,
                        h => myTest.Results -= h)
                    .Take(1)
                    .Subscribe(o);
            myTest.Run();
            return subscription;
        })
        .ObserveOn(uiContext)
        .Subscribe(x => this.richTextBox_testResults.Document.Blocks.Add(new Paragraph(new Run(x.EventArgs.Results))));
}

这最后一个真的是你问题的答案:&#34;或者,有没有办法只能在它存在的时间内观察我的任务?&#34;