Skip to content

Instantly share code, notes, and snippets.

@jrgcubano
Created March 11, 2015 14:15
Show Gist options
  • Save jrgcubano/7e447d6cd26d55ecff82 to your computer and use it in GitHub Desktop.
Save jrgcubano/7e447d6cd26d55ecff82 to your computer and use it in GitHub Desktop.
WPF Weak events pattern using Rx
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