Created
March 11, 2015 14:15
-
-
Save jrgcubano/7e447d6cd26d55ecff82 to your computer and use it in GitHub Desktop.
WPF Weak events pattern using Rx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Disposables; | |
using System.Linq; | |
using System.Windows; | |
using System.Windows.Controls; | |
using System.Windows.Input; | |
using System.Windows.Media; | |
using System.Diagnostics; | |
static class Program | |
{ | |
[STAThread] | |
static void Main() | |
{ | |
var app = new Application(); | |
app.Run(new MainWindow()); | |
} | |
} | |
sealed class MainWindow : Window | |
{ | |
public event MouseEventHandler CanvasMoves; | |
public MainWindow() | |
{ | |
Width = 200; | |
Height = 200; | |
Title = "Main"; | |
var gcButton = new Button { | |
Content = "Perform GC" | |
}; | |
gcButton.Click += delegate { | |
GC.Collect(); | |
GC.WaitForPendingFinalizers(); | |
}; | |
var mouseCanvas = new Canvas { | |
Background = new SolidColorBrush(Colors.Chartreuse), | |
Margin = new Thickness(10), | |
Height = 100, | |
Width = 100 | |
}; | |
mouseCanvas.MouseMove += (sender, args) => { | |
var handler = CanvasMoves; | |
if (handler != null) handler(sender, args); | |
}; | |
var sp = new StackPanel(); | |
sp.Children.Add(gcButton); | |
sp.Children.Add(mouseCanvas); | |
Content = sp; | |
Loaded += delegate { | |
var child = new ChildWindow(this); | |
child.Show(); | |
}; | |
MouseMove += delegate { | |
var temp = new int[100000]; | |
}; | |
} | |
} | |
sealed class ChildWindow : Window | |
{ | |
readonly IDisposable subscription; | |
public ChildWindow(MainWindow main) | |
{ | |
Width = 200; | |
Height = 100; | |
Left = 400; | |
Title = "Child"; | |
Owner = main; | |
var text = new TextBlock(); | |
this.subscription = | |
Observable | |
.FromEvent<MouseEventArgs>( | |
h => main.CanvasMoves += h.Invoke, | |
h => { | |
main.CanvasMoves -= h.Invoke; | |
MessageBox.Show("Unsubscribed from:\n" + new StackTrace()); | |
}) | |
.AsWeakObservable() | |
.Select(e => e.EventArgs.GetPosition(main)) | |
.Select(p => string.Format("x: {0}; y: {1}", p.X, p.Y)) | |
.Delay(TimeSpan.FromMilliseconds(200)) | |
.ObserveOnDispatcher() | |
.Subscribe(s => { text.Text = s; }); | |
Content = text; | |
} | |
~ChildWindow() | |
{ | |
MessageBox.Show("ChildWindow finalized!"); | |
} | |
} | |
public static class ObservableEx | |
{ | |
/// <summary> | |
/// Returns an observable sequence that contains the values | |
/// of the underlying .NET event, does not hold the strong | |
/// reference to the subscribing observers and automatically | |
/// unsubscribes from the event when the subscription (returned | |
/// <see cref="IDisposable"/>) is garbage collected.</summary> | |
/// <remarks> | |
/// If unsubscription is invoked during garbage collection, | |
/// it be invoked in the finalizer thread.</remarks> | |
public static IObservable<IEvent<TEventArgs>> FromWeakEventAuto<TEventArgs>( | |
Action<EventHandler<TEventArgs>> addHandler, | |
Action<EventHandler<TEventArgs>> removeHandler) | |
where TEventArgs : EventArgs | |
{ | |
if (addHandler == null) | |
throw new ArgumentNullException("addHandler"); | |
if (removeHandler == null) | |
throw new ArgumentNullException("removeHandler"); | |
return Observable.CreateWithDisposable<IEvent<TEventArgs>>(ob => | |
{ | |
// do not hold the strong reference | |
var wr = new WeakReference(ob); | |
EventHandler<TEventArgs> handler = (sender, args) => | |
{ | |
// check the observer alive each event invokation | |
var o = (IObserver<IEvent<TEventArgs>>) wr.Target; | |
if (o != null) | |
{ | |
o.OnNext(Event.Create(sender, args)); | |
} | |
}; | |
addHandler(handler); // connect the handler | |
// return the disposable which will unsubscribe at GC | |
return new FinalizerDisposable(() => removeHandler(handler)); | |
}); | |
} | |
/// <summary> | |
/// Returns an observable sequence that contains the values | |
/// of the underlying .NET event and does not hold the strong | |
/// reference to the subscribing observers. If the subscription | |
/// and observer is garbage collected and event fires, | |
/// the handler will be automatically unsubscribed.</summary> | |
/// <remarks> | |
/// If observer will be garbage collected and event never fires | |
/// again the subscription handler will consume a bit of memory.</remarks> | |
public static IObservable<IEvent<TEventArgs>> FromWeakEvent<TEventArgs>( | |
Action<EventHandler<TEventArgs>> addHandler, | |
Action<EventHandler<TEventArgs>> removeHandler) | |
where TEventArgs : EventArgs | |
{ | |
if (addHandler == null) | |
throw new ArgumentNullException("addHandler"); | |
if (removeHandler == null) | |
throw new ArgumentNullException("removeHandler"); | |
return Observable.Create<IEvent<TEventArgs>>(ob => | |
{ | |
// do not hold the strong reference | |
var wr = new WeakReference(ob); | |
EventHandler<TEventArgs> handler = null; | |
handler = (sender, args) => | |
{ | |
// check the observer alive each event invokation | |
var o = (IObserver<IEvent<TEventArgs>>) wr.Target; | |
if (o != null) | |
{ | |
o.OnNext(Event.Create(sender, args)); | |
} | |
else | |
{ | |
// clean yourself if the observer is collected | |
removeHandler(handler); | |
} | |
}; | |
addHandler(handler); | |
return () => removeHandler(handler); | |
}); | |
} | |
/// <summary> | |
/// Creates an observable that does not hold the strong reference to the | |
/// subscribing observers and automatically unsubscribes when the subscription | |
/// (returned <see cref="IDisposable"/>) is garbage collected.</summary> | |
/// <remarks> | |
/// If unsubscription is invoked during garbage collection, | |
/// it be invoked in the finalizer thread.</remarks> | |
public static IObservable<T> AsWeakObservable<T>(this IObservable<T> source) | |
{ | |
if (source == null) | |
throw new ArgumentNullException("source"); | |
return Observable.CreateWithDisposable<T>(o => | |
{ | |
var observer = new ObserverWeakWrapper<T>(o); | |
// subscribes to the underlying observable | |
var subscription = source.Subscribe(observer); | |
// pass the subscription to the observer | |
observer.SetSubscription(subscription); | |
// returns the | |
return new FinalizerDisposable(subscription.Dispose); | |
}); | |
} | |
sealed class ObserverWeakWrapper<T> : IObserver<T> | |
{ | |
readonly FutureDisposable subscription; | |
readonly WeakReference observer; | |
public ObserverWeakWrapper( | |
IObserver<T> underlying) | |
{ | |
this.observer = new WeakReference(underlying); | |
this.subscription = new FutureDisposable(); | |
} | |
IObserver<T> Observer | |
{ | |
get { return (IObserver<T>) this.observer.Target; } | |
} | |
public void OnNext(T value) | |
{ | |
var o = Observer; | |
if (o != null) o.OnNext(value); | |
else this.subscription.Dispose(); | |
} | |
public void OnError(Exception exception) | |
{ | |
var o = Observer; | |
if (o != null) o.OnError(exception); | |
else this.subscription.Dispose(); | |
} | |
public void OnCompleted() | |
{ | |
var o = Observer; | |
if (o != null) o.OnCompleted(); | |
else this.subscription.Dispose(); | |
} | |
public void SetSubscription(IDisposable disposable) | |
{ | |
this.subscription.Set(disposable); | |
} | |
} | |
sealed class FinalizerDisposable : IDisposable | |
{ | |
readonly Action action; | |
public FinalizerDisposable(Action action) | |
{ | |
if (action == null) | |
throw new ArgumentNullException("action"); | |
this.action = action; | |
} | |
public void Dispose() | |
{ | |
this.action(); | |
GC.SuppressFinalize(this); | |
} | |
~FinalizerDisposable() | |
{ | |
this.action(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment