A Monad in C# for Simplifying WPF Multi-Threading for a More Responsive GUI
Join the DZone community and get the full member experience.
Join For FreeCode included here is over simplified for clarity, I hosted a better implementation code on CodePlex. These modifications change strictly nothing for the client code and are only an implementation detail. I use a continuation rather than a delay, and I chose to design a custom continuation class rather than using a delegate because of a type system limitations.
Most
GUI frameworks, including Silverlight and WPF, are shipped with a
fundamental problem: long use of the main thread causes the Window to
blackout, and using different threads requires you to get your hands
dirty with the Dispatcher stuff and freezable objects. Worse, you wont
learn the necessity to do so until you get a surprise of “The calling
thread cannot access this object because a different thread owns it.”
exception when all what you were doing is to use available methods on an
object that seemed you have access to, at lease it seemed until
runtime! This post illustrates a solution based on Monads abstraction
and LinQ syntax.
This is a problem you get often when applying Model View Presenter pattern [MVP]. There, your view (which is a WPF control) implements a contract IView that the presenter in its turn will use to extract values and then make operations on the view.
interface ISayHello { string GetName(); Unit SayHello(string name); } class MyLoginControl:Control,ISayHello { //this interface gets implemented by the corresponding control as texboxes and a text area… }
The problem appears when for doing any realistic responsive application, the presenter (representing business operations if you’d like) will have to run on a background thread to leave the main thread free for graphics rendering.
While working on a background thread, the presenter needs to access the view (having a reference to it through a contract) and there something wrong happen “The calling thread cannot access this object because a different thread owns it.” .
The problem here is simply that the view is giving quite a promise that it simply can not satisfy which is implementing the IView contract.
The view cannot satisfy the ISayHello contract under all circumstances, and not even under a commonly desirable condition (the presenter or business code running on another thread). This fact is simply not communicated through the type.
The solution I suggest to this problem that I implemented and employed in a production real world project is based on the LinQ syntax added to C# last year. In the solution I use two things: an extension method and a special type.
The type that I use is the monadic type (thanks to Wesdyer for his enlightening posts): View<T>
So in my case my type will be View<IView> which means that what I am offering here is a a type that acts under some special circumstances as the Contract ISayHello . If I want for example to extract the name from the WPF control, I need to do something with the View<ISayHello>.
Using the View<T> Monad:
And here comes the LinQ syntax for help. Having a reference to View<ISayHello>, the only way with which I can access the desired value or methods is using Linq:
Having the view contract:
public interface ISayHello { Unit SayHello(string Name); string GetName(); }
I can extract a view monad that I can pass to the presenter as View<ISayHello>
View<ISayHello> view = this.AsView<Window1, ISayHello>();
note the type View<ISayHello> which is somehow useless without the LinQ syntax:
(from v in view let name = v.GetName() select v.SayHello(name)).Do();
and you can also use several contracts in the same expression:
from v in view1 from v2 in view2
Implementation of the View<T> Monad:
View<T> is not really special. It is just a Delay<T> which is a Func<T>. And the Select implementation is the same one for functions and is not special at all:
public delegate R View<R>(); public enum Unit { Unit } public static class WPFMonadExtensions { public static View<U> SelectMany<T, U>(this View<T> m, Func<T, View<U>> k) { return () => k(m())(); } public static View<U> Select<T, U>(this View<T> m, Func<T, U> selector) { return () => selector(m()); } public static View<V> SelectMany<T, U, V>(this View<T> source, Func<T, View<U>> kSelector, Func<T, U, V> resultSelector) { return () => { var t = source(); var u = kSelector(t)(); return resultSelector(t, u); }; } public static Unit Do(this View<Unit> k) { return k(); }
The fact that I am defining a new delegate type here (View<T>) is because we don’t have type synonyms is C#. And because of this I had to reimplement all the Select methods for this type. Of course all of that Monad plumping code is invisible and all the user needs to do is use the LinQ syntax.
The only specific part about the View monad, is the way you extract it. For Wpf for example the .AsView implementation looks like:
public static View<TView> AsView<TWPF, TView>(this TWPF value) where TWPF : UIElement, TView { return value.ToWpfMonad<TWPF, TView>(); } public static View<Answer> ToWpfMonad<T, Answer>(this T value) where T : UIElement, Answer { return () => { Answer a = default(Answer); value.Dispatcher.Invoke(DispatcherPriority.Normal, (EventHandler)((sender, e) => { a = value; if (a is Freezable) { var result = ((Freezable)(object)a).Clone(); a = (Answer)(object)result; result.Freeze(); } }), null, null); return a; }; }
Which merely tells about how to execute the delay when applied.
This is the code responsible for calling on WPF windows using dispatcher and other plumping details. Again code here is simplified for clarity. In the same way one can implement an AsView extension method for Silverlight with no need to change the Select implementation. View<T> is a generic monad and contain nothing specific to GUI technology.
Conclusion
The Monad generalization provides a good solution for WPF/Silverlight thread problem. The solution is barely about communicating through the type system the fact that WPF/Silverlight controls are special, and using LinQ expression to operate on them leaving the plumping (Dispatcher.Invoke, Freezable) to the monad library implementers. Also this frees the caller from thread logic that is specific to the implementation technology.
In my project I didn’t work much on the freezing/unfreezing of Wpf controls (copy them and extract them to other threads) as it actually wasn’t necessary for my project. However, I think that a proper implementation of .AsView for WPF/Silverlight that manages Freezable and nested Freezable objects copying would be very interesting and would complete the API.
PS: I decided to finish this draft quickly as I am not having enough time to finish properly. Please tolerate typos and don’t hesitate to ask questions.
Published at DZone with permission of Sadek Drobi. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments