2013-02-23 86 views
1

我是Reactive Extensions的新手,這就是我想要做的Popup Toaster通知。當鼠標移過烤麪包機時,不透明度恢復到100%。否則,它會逐漸淡出。正確處置RX訂閱

該代碼有效,但我並不完全確信我沒有泄漏資源,特別是在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 *(清理訂閱())

這裏是一個有點cleanear嘗試。

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(); 
    
+0

你還是留下未予處置'IDisposable's - 那'Subscribe'調用創建一個訂閱,*會*超出範圍並被收集(可能會破壞您的動畫),但可能會泄露其他資源。至於暫停/取消,這實際上是這種linq樣式語法的一個好處......在這裏,我已經在我的完整測試平臺中添加了,試試看。 – JerKimball 2013-02-27 02:01:29

回答

4

嗯,首先,你漏IDisposables所有的地方 - 那些Subscribe電話中的每一個返回IDisposable,剛剛超出範圍,沒有妥善處置。這部分很容易但是固定的,使用一些IDisposable容器在Rx LIB的:在查詢以後

(從測試工具片段我扔在一起,在你的示例代碼)

// 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爲 「中」 價值
  • slideOut:Animates Margin「out」value

而這些觀測值(從代碼略有名調整):

  • mouseEnter:=>MouseEnter事件
  • mouseOut:=>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; 
    } 
} 
+0

謝謝。這只是把我的想法弄亂了。 BeginObservable()真的有效,因爲它避免了我所有的.Do()調用。 我唯一的問題是它不起作用。我調用showToast.Subscribe(x => Console.WriteLine(x));訂閱沒有任何內容。 – 2013-02-26 23:47:19

+0

做一些事情:使用(showToast.Subscribe(...)){Console.ReadLine();}並將鼠標懸停在該矩形上(格式化,在我的手機上抱歉) – JerKimball 2013-02-26 23:50:14

+0

Sorted the mouseOver problem。該解決方案運行,但它不是我想要的(即烤麪包機彈出和淡出,可通過鼠標懸停進行互換 - 如Outlook郵件通知)。我用一個工作示例更新了我的問題,但無法弄清楚如何將它轉換爲LINQ語法。代碼審查讚賞。 – 2013-02-27 00:53:10