Using the Reactive Extensions with WinForms

Background

The project I’m working right now is a WinForms project. In order to make it more testable and esier to change, we’ve settled on an MVP pattern, implementd as a variation of the one Mark Nijhof describes. We’ve implemented an Event Aggregator on top of the Reactive Extensions(RX), similar to the one José F. Romaniello describes, to enable decoupled, messaging between presenter instances. Presenters are responsible to subscribing to the types of events, optionally meeting some condition, that they are interrested in, and then performing some action when such an event arrives.

A typical scenario that leverages this functionality is a Master-Detail relationship. One presenter displays a list of something. The user can then bring up a Detail view of that item and make changes to it. When the user saves the detail, the detail presenter publishes a message indicating which item has changed. The master presenter, which was subscribed to receive such events, then issues a query to refresh that single item (rather than the entire list) and update the master view.

The Problem

Now, originally, when we were subscribing to events in the presenter, the event aggregator required that we pass in an instance of an IScheduler. In our case, since this was a WinForms app, we would create an instance of a ControlScheduler in the Shown event, and then raise a standard .NET event with this scheduler instance as an argument. We use the ControlScheduler because it handles the Invoke/BeginInvoke stuff for us when we use it to schedule some work to be done* The presenter, which is wired up to receive this event, would then use this instance to pass to the Event Aggregator to subscribe.

Everything Worked on My Machine™, but when our testers got a hold of it, they always received an

1
InvalidOperationException: Invoke or BeginInvoke cannot be called on a control until the window handle has been created.
Needless to say, this left me scratching my head. We raise our event long after the Handle had been created. Why did the ControlScheduler instance think that the Handle had not been created?

Well, as it turns out, the Handle that the ControlScheduler had a reference to had been disposed. In fact, something in the way this form interacted with the application shell (probably adding it to a TabPage of a TabControl and hiding the ControlBox) had actually caused the win32 Window to be destroyed and recreated at least once. As Kevin Dente pointed out to me:

OK, so we can’t depend on an instance of a ControlScheduler being valid all the time. Which means that we can’t rely on it in a call to ObserveOn, which our implementation of the Event Aggregator was doing when we called Subscribe on it. In fact, RX guru Paul Betts hinted as much when I was groping about in the dark on twitter:

So, time to refactor!

The Solution

Once I understood the problem, the solution became evident: any time the underlying Handle on the form changed, we’d need to notify the presenter. That way, when we’d actually need to schedule work to be done, we’d have a valid instance. This also meant that we’d need to forego having the EventAggregator call ObserveOn when we subscribed to events. Finally, when we wanted to interact with the View, we should use IScheduler.Schedule for complex updates, or ensure that the View performed the necessary switch for BeginInvoke for simple assignments.

Step 1 - Get a valid IScheduler to the presenter

Fortunately, the Form class has an event to let us know that a Handle has been created, or recreated, for the form: HandleCreated. The documentation is a little misleading, as HandleCreated is raised not only when the form is first Shown, but anytime that the Handle is created. Instead of creating a one-off ControlScheduler in the form’s Shown event, we’ll add an IScheduler property to the form:

1
public IScheduler Scheduler { get; private set; }

Then, we hook into the form’s HandleCreated event so we can create a new, valid ControlScheduler instance when we need to:

1
2
3
4
HandleCreated += (s,e) => {
    Scheduler = new ControlScheduler(this);
    OnReady.Raise(Scheduler);
};

Step 2 - Stop Subscribing with ObserveOn

Pretty self-explanatory. Remove the call to ObserveOn in the Event Aggregator’s Subscribe method:

1
2
3
public IDisposable Subscribe<TEvent>(Action<TEvent> onEvent) {
    return GetEvent<TEvent>().Subscribe(onEvent);
}

Step 3 - Use IScheduler.Schedule for Complex UI updates

Typically, you can use an extension method, like the one Derick Bailey recommended to me to ensure that you can safely update UI controls from the non-UI thread. In some cases, though, this is simply not enough. The specific case that caused me trouble was BindingList<T>, specifically when it raised ListChangedEvent. This could potentially happen on a background thread, which would result in cross-thread exceptions. I solved this by setting RaiseListChangedEvents to false for my binding list, updating the list, then using the IScheduler instance to raise ListChangedEvents on the UI thread:

1
_scheduler.Schedule(() => _model.SomeBindingList.ResetBindings());

And there you have it! I hope someone else finds this useful, since most people using RX seem to be doing it with WPF and not WinForms.

Comments

comments powered by Disqus