妥善处理RX订阅

时间:2013-02-23 14:02:33

标签: silverlight system.reactive

我是Reactive Extensions的新手,这就是我用来做Popup Toaster Notification的方法。当鼠标越过烤面包机时,不透明度将恢复到100%。否则,它逐渐淡出。

代码有效,但我并不完全相信我没有泄漏资源,特别是在mouseOut订阅中。另外,我不确定这是否是实现此功能的最佳方式。

任何批评,提示都会受到赞赏。

    private void rxPop()
    {
        Rectangle toaster = (Rectangle)this.FindName("toaster1");
        Thickness newToasterPosition = new Thickness(
             toaster.Margin.Left, toaster.Margin.Top,
             toaster.Margin.Right, toaster.Margin.Bottom + toaster.Height);

        /* Animations */
        Storyboard slideInAnimation = slide(toaster,
            newToasterPosition,
            TimeSpan.FromMilliseconds(450));

        Storyboard fadeInAnimation = animateOpacity(toaster, 1.0, TimeSpan.FromMilliseconds(150));

        Storyboard fadeOutAnimation = animateOpacity(toaster, 0.0, TimeSpan.FromSeconds(3));

        /* Events */
        var mouseEnter = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>
            (h => toaster.MouseEnter += h,
             h => toaster.MouseEnter -= h);

        var mouseOut = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>
            (h => toaster.MouseLeave += h,
             h => toaster.MouseLeave -= h);

        var slideInCompleted = Observable.FromEventPattern<EventHandler, EventArgs>(
            h => slideInAnimation.Completed += h,
            h => slideInAnimation.Completed -= h);

        var fadeOutCompleted = Observable.FromEventPattern<EventHandler, EventArgs>(
            h => fadeOutAnimation.Completed += h,
            h => fadeOutAnimation.Completed -= h);

        // slideIn then fadeOut 
        slideInCompleted.Subscribe(e => fadeOutAnimation.Begin());

        var mouseEnterSubscription = mouseEnter
            .ObserveOnDispatcher()
            .Do(a =>
                {
                    fadeOutAnimation.Pause();
                    fadeInAnimation.Begin();
                    slideInAnimation.Pause();
                    mouseOut.Do(
                        b =>
                        {
                            fadeOutAnimation.Begin();
                            fadeInAnimation.Stop();
                            slideInAnimation.Resume();
                        }).Subscribe();
                })
            .Subscribe();

        fadeOutCompleted.Subscribe((e) => mouseEnterSubscription.Dispose());

        slideInAnimation.Begin();
    }

理想情况下,我希望以下列方式表达事件:

   slideIn then fadeOut
   unless mouseEnter 
        then fadeIn , slideIn.Pause
        until mouseLeave 
           then fadeOut.Begin and slideIn.Resume

在RX中最接近的方法是什么?

更新#1 * 更新#2 *(清理订阅())

这是一个有点清洁的尝试。

protected CompositeDisposable _disposables = new CompositeDisposable();

private void rxPop()
{

IDisposable mouseEnterSubscription = null;

/* Business logic: slideIn then fadeOut then remove from visual tree */
_disposables.Add(
  slideInAnimation
    .BeginUntilDone()
    .Select(slideInCompletedEvent =>
        fadeOutAnimation.BeginUntilDone())
    .Switch()
    .Subscribe(fadeOutAnimationCompletedEvent =>
    {
        mouseEnterSubscription.Dispose();

        // remove from visual tree
        (toaster.Parent as Panel).Children.Remove(toaster);
    }));

/* Business logic: mouseEnter/mouseLeave should pause/restart animations */
mouseEnterSubscription = mouseEnter
    .ObserveOnDispatcher()
    .Do(mouseEnterEventArgs =>
    {
        fadeOutAnimation.Pause();
        fadeInAnimation.Begin();
        slideInAnimation.Pause();
    })
    .Select(e => mouseOut)
    .Switch()
    .Do(mouseLeaveEventArgs =>
    {
        fadeOutAnimation.Begin();
        fadeInAnimation.Stop();
        slideInAnimation.Resume();
    })
    .Subscribe();

}

public static class RxExtensions
{
    public static IObservable<EventPattern<EventArgs>> BeginUntilDone(this Storyboard sb)
    {
        var tmp = Observable.FromEventPattern(
            h => sb.Completed += h,
            h => sb.Completed -= h);
        sb.Begin();
        return tmp;
    }
}

我的问题是:

  1. ObserveOnDispatcher()是否正确完成?

  2. Switch()是否为我配置了以前的IObservable?

  3. 我很难将上述内容翻译成LINQ查询语法

        /* Business Logic */
        var showToast =
            // Start by sliding in
            from slideInComplete in slideIn.BeginObservable()
            where slideInComplete
            // Then in parallel, fadeOut as well as wait for mouseEnter
            from fadeOutComplete in fadeOut.BeginObservable()
            from enter in mouseEnter
            // ... I'm lost here.
            // ... how do I express 
            // ..... mouseEnter should pause fadeOut?
            select new Unit();
    

1 个答案:

答案 0 :(得分:4)

好吧,首先,你在整个地方泄漏IDisposables - 这些Subscribe次调用中的每一次都会返回一个IDisposable,这只是超出了范围,而不是处理得当。但是,使用Rx lib中的一些IDisposable容器可以轻松修复该部分:

(来自测试工具的片段我把你的示例代码放在一起)

// new fields

// A serial disposable lets you wrap one disposable
// such that changing the wrapped disposable autocalls
// Dispose on the previous disposable
protected SerialDisposable _resubscriber;

// A composite disposable lets you track/dispose a whole
// bunch of disposables at once
protected CompositeDisposable _disposables;

// no real need to do this here, but might as well
protected void InitializeComponent()
{
    _disposables = new CompositeDisposable();
    _resubscriber = new SerialDisposable();
    // misc
    this.Unloaded += (o,e) => 
    {
        if(_disposables != null) _disposables.Dispose();
        if(_resubscriber != null) _resubscriber.Dispose();
    };

然后在您的查询中,将所有Subscribe个调用(除了一个,见下文)包装成这样的内容:

    // slideIn then fadeOut 
    _disposables.Add(slideInCompleted.Subscribe(e => fadeOutAnimation.Begin()));

唯一的例外是MouseOut“取消者”:

        .Do(a =>
            {
                fadeOutAnimation.Pause();
                fadeInAnimation.Begin();
                slideInAnimation.Pause();
                _resubscriber.Disposable = mouseOut.Do(
                    b =>
                    {
                        fadeOutAnimation.Begin();
                        fadeInAnimation.Stop();
                        slideInAnimation.Resume();
                    }).Subscribe();
            })

现在......就此而言:

slideIn then fadeOut
unless mouseEnter 
    then fadeIn , slideIn.Pause
    until mouseLeave 
        then fadeOut.Begin and slideIn.Resume

我将不得不考虑......我认为还有更多...... rx 这样做的方式,但我必须稍微思考一下。绝对可以处理IDisposable清理,等等!

(如果我能为第二位提出一些内容,将编辑此内容)

编辑:噢,我觉得我有一些很有希望......

首先,让我们设法将Storyboard开始/完成翻译成IObservable

public static class Ext
{
    public static IObservable<bool> BeginObservable(this Storyboard animation)
    {
        var sub = new BehaviorSubject<bool>(false);
        var onComplete = Observable.FromEventPattern<EventHandler, EventArgs>(
            h => animation.Completed += h,
            h => animation.Completed -= h);

        IDisposable subscription = null;
        subscription = onComplete.Subscribe(e => 
        {
            Console.WriteLine("Animation {0} complete!", animation.Name);
            sub.OnNext(true);
            if(subscription != null)
                subscription.Dispose();
        });

        Console.WriteLine("Starting animation {0}...", animation.Name);
        animation.Begin();
        return sub;
    }
}

基本上,这会设置一个“开始动画,并在完成后向我们true发出信号”序列......对好的部分!

所以我们假设您已经定义了以下Storyboards

  • fadeIn:将Opacity动画为1.0
  • fadeOut:将Opacity动画为1.0
  • slideIn:将Margin设置为“in”值
  • slideOut:将Margin设置为“out”值

这些可观察的内容(来自您的代码的轻微名称调整):

  • mouseEnter:=&gt; MouseEnter活动
  • mouseOut:=&gt; MouseLeave活动

您可以设置一个IObservable,其中包含实际序列:

var showToast = 
    // Start this mess on a "mouse enter" event
    from enter in mouseEnter
    // Start (in parallel) and wait until the fadeIn/slideIn complete
    from fadeInComplete in fadeIn.BeginObservable()
    from slideInComplete in slideIn.BeginObservable()
    where fadeInComplete && slideInComplete
    // Until you see a "mouse out" event
    from exit in mouseOut
    // Then start (in parallel) and wait until the fadeOut/slideOut complete
    from fadeOutComplete in fadeOut.BeginObservable()
    from slideOutComplete in slideOut.BeginObservable()
    where fadeOutComplete && slideOutComplete
    // And finally signal that this sequence is done
    // (we gotta select something, but we don't care what, 
    // so we'll select the equivalent of "nothing" in Rx speak)
    select new Unit();

编辑编辑:这是我使用的完整测试装备,也许您可​​以满足您的需求:

void Main()
{
    var window = new Window();
    var control = new MyControl();
    window.Content = control;
    window.Show();
}

public class MyControl : UserControl
{
    protected DockPanel _root;
    protected Rectangle _toaster;
    protected CompositeDisposable _disposables;
    protected Thickness _defaultToasterPosition = new Thickness(10, -60, 10, 10);

    public MyControl()
    {
        this.InitializeComponent();
    }

    protected void InitializeComponent()
    {
        _disposables = new CompositeDisposable();
        _root = new DockPanel();
        _toaster = new Rectangle();
        _toaster.SetValue(Rectangle.NameProperty, "toaster1");
        _toaster.Fill = Brushes.Red;
        _toaster.Stroke = Brushes.Black;
        _toaster.StrokeThickness = 3;
        _toaster.Width = 50;
        _toaster.Height = 50;
        _toaster.Opacity = 0.1;
        DockPanel.SetDock(_toaster, Dock.Bottom);
        _toaster.Margin = _defaultToasterPosition;
        rxPop();

        _root.Children.Add(_toaster);        
        this.Content = _root;

        this.Unloaded += (o,e) => 
        {
            if(_disposables != null) _disposables.Dispose();
        };
    }

    private void rxPop()
    {
        var toaster = (Rectangle)this.FindName("toaster1") ?? _toaster;
        var defaultToasterPosition = toaster.Margin;
        var newToasterPosition = new Thickness(
             defaultToasterPosition.Left, defaultToasterPosition.Top,
             defaultToasterPosition.Right, defaultToasterPosition.Bottom + toaster.Height);

        /* Animations */
        var slideIn = slide(toaster, newToasterPosition, TimeSpan.FromMilliseconds(450));
        var slideOut = slide(toaster, defaultToasterPosition, TimeSpan.FromMilliseconds(450));

        var fadeIn = animateOpacity(toaster, 1.0, TimeSpan.FromMilliseconds(150));
        var fadeOut = animateOpacity(toaster, 0.1, TimeSpan.FromSeconds(3));

        /* Events */
        var mouseEnter = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>
            (h => toaster.MouseEnter += h,
             h => toaster.MouseEnter -= h);

        var mouseOut = Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>
            (h => toaster.MouseLeave += h,
             h => toaster.MouseLeave -= h);

        var showToast = 
            // Start this mess on a "mouse enter" event
            from enter in mouseEnter
            // Start (in parallel) and wait until the fadeIn/slideIn complete
            from fadeInComplete in fadeIn.BeginObservable()
            from slideInComplete in slideIn.BeginObservable()
            where fadeInComplete && slideInComplete
            // Until you see a "mouse out" event
            from exit in mouseOut
            // Then start (in parallel) and wait until the fadeOut/slideOut complete
            from fadeOutComplete in fadeOut.BeginObservable()
            from slideOutComplete in slideOut.BeginObservable()
            where fadeOutComplete && slideOutComplete
            // And finally signal that this sequence is done
            // (we gotta select something, but we don't care what, 
            // so we'll select the equivalent of "nothing" in Rx speak)
            select new Unit();

        _disposables.Add(showToast.Subscribe());
    }

    private Storyboard slide(Rectangle rect, Thickness newPosition, TimeSpan inTime)
    {
        var sb = new Storyboard();
        sb.Duration = inTime;
        Storyboard.SetTarget(sb, rect);        
        Storyboard.SetTargetProperty(sb, new PropertyPath(Rectangle.MarginProperty));
        var path = new ThicknessAnimation(newPosition, inTime);
        sb.Children.Add(path);
        return sb;
    }

    private Storyboard animateOpacity(Rectangle rect, double newOpacity, TimeSpan inTime)
    {
        var sb = new Storyboard();
        sb.Duration = inTime;
        Storyboard.SetTarget(sb, rect);        
        Storyboard.SetTargetProperty(sb, new PropertyPath(Rectangle.OpacityProperty));
        var path = new DoubleAnimation(newOpacity, inTime);
        sb.Children.Add(path);
        return sb;
    }
}

public static class Ext
{
    public static IObservable<bool> BeginObservable(this Storyboard animation)
    {
        var sub = new BehaviorSubject<bool>(false);
        var onComplete = Observable.FromEventPattern<EventHandler, EventArgs>(
            h => animation.Completed += h,
            h => animation.Completed -= h);

        IDisposable subscription = null;
        subscription = onComplete.Subscribe(e => 
        {
            Console.WriteLine("Animation {0} complete!", animation.Name);
            sub.OnNext(true);
            if(subscription != null)
                subscription.Dispose();
        });

        Console.WriteLine("Starting animation {0}...", animation.Name);
        animation.Begin();
        return sub;
    }
}