Friday, June 18, 2010

F#, WPF and XAML, oh my!

Anyone why tries to use WPF with F# will know, or at very least, soon find out, that F# doesn't get along with WPF very well. Specifically, binding F# code to a XAML control is a nasty ordeal.

F# doesn't currently have any support for codeDOM, which is needed to create the magic glue between the XAML and the code. Thus, we must create this glue ourselves.

Tearing into a C# WPF project revealed the secrets of the glue - the XAML is compiled into a BAML resource in the final executable, which is then bound to the control class through Application.LoadComponent(). At first glance, it would seem that this would be easily reproducible on F# - simply include the XAML as a resource, get its Uri, and pass that into the LoadComponent. This, however, will not work. Why? LoadComponent requires that the XAML be compiled into BAML, which doesn't happen just from putting the XAML into a resource. In fact, any attempt to compile the XAML during build will likely fail, since doing so involves codeDOM, which F# doesn't support.

So what do we do? We could use a XamlReader to load the GUI from a loose XAML file, but then we loose the capability to use code-behind, which means we have to bind all the controls the hard way. Or do we?...

As it turns out, it *is* possible to use code-behind with loose XAML. It just takes some clever workarounds as well as very explicit namespace definitions in the XAML. This is the subject of this post.

The first thing to do is modify out XAML to be able to locate the code-behind in our executable. In order to do this, we have to explicitly list both the namespace and the assembly, as well as bind directly to the specific derived class. This basically requires that we change this:

<Window x:Class="Flam4GUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:Flam4GUI"
....


into this:

<app:MainWindow
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:app="clr-namespace:Flam4GUI;assembly=Flam4OCL"
....


This brings us to our second problem: The standard convention is to call LoadComponent() from the constructor of the control class. However, this doesn't work with XamlReader.Load() since it creates a new control class and binds that to the XAML. Thus, if one were to call XamlReader.Load() inside the constructor, it would callback to new ControlClass(), which would call XamlReader.Load(), which would call new ControlClass(), and so forth until the stack overflowed. Thus, we need to create a new static method in the control class, which would call XamlReader.Load(), which would call new ControlClass(), which would return a neatly bound ControlClass object. Here's the code for this:


type MainWindow() as s=
inherit Window()


let mutable _contentLoaded = false
do s.InitializeComponent()

member s.InitializeComponent()=
if _contentLoaded = false then
_contentLoaded <- true
()

static member Create() =
let fs = new FileStream(@".\Resources\MainWindow.xaml",FileMode.Open)
System.Windows.Markup.XamlReader.Load(fs) :?> (MainWindow)


We can now open the window like so:


let window = MainWindow.Create()
(new Application()).Run(window)


The best part is that any methods in MainWindow can be bound to in the XAML, just as you would expect. Thus, we have reached our goal, using WPF + XAML with code-behind, with nothing but F#.

No comments:

Post a Comment