Using WPF Elements from F#

WPF and F#? It’s not a natural mix — Visual Studio doesn’t provide templates or tooling for F# WPF projects, and WPF’s implicit architecture normally implies mutable view models which are at odds with F#’s preference for immutable data types. But sometimes a project comes along which seems like a great fit for F#, and for a WPF front end. A great example is data visualisation. Retrieving and processing the data is exactly what F# does well, and innovative ways of presenting data are much easier to implement on WPF than on any other platform; and in a visualisation scenario the main view model is typically immutable.

I’m going to look at two ways of combining F# and WPF. I’ll be using the Chart control from WPF Elements as my main example, but a lot of what I talk about here is equally applicable to any other WPF control, built-in or third-party.

Solution 1: Hybrid C# and F# Solution

The traditional way of using F# in a WPF application is to put the F# code into a class library, then invoke that class library from a C# or Visual Basic WPF application. This works very easily — a C# application can call a F# class library just as easily as it can a C# class library, with full intellisense and debugging and everything else you’d expect.

For this example, I’m going to keep the F# class library very simple. In reality, you probably wouldn’t create a separate class library for something this basic. F# really starts to shine when you’re doing a bit more processing on your data, but to keep things simple I’m not going to do much more than download it.

The data I’m going to use is the London Borough Profiles from the Windows Azure Datamarket. This is dead easy to use in F# 3.0 thanks to the OData type provider:

open Microsoft.FSharp.Data.TypeProviders
 
type Boroughs = ODataService<"https://api.datamarket.azure.com/GreaterLondonAuthority/LondonBoroughProfiles/">
 
let dataContext = Boroughs.GetDataContext()
dataContext.Credentials <- System.Net.NetworkCredential(YourUserName, YourAccountKey)

For my view model, I want to extract just a subset of this data, so I define a couple of F# types to surface it:

open System.Collections.Generic
 
type BoroughInfo = {
  Area : string
  MaleLifeExpectancy : float
  FemaleLifeExpectancy : float
  Employment : float
}
 
type ViewModel = {
  Boroughs : List<BoroughInfo>    // not BoroughInfo list -- see below
  National : BoroughInfo
}

These are F# immutable record types. We don’t need to modify the data, so immutable types are easier to write and we don’t need to muck around with all that tedious INotifyPropertyChanged stuff. The view model is going to include a list of entries for individual boroughs, plus a ‘dummy’ borough representing national averages so I can display them alongside the per-borough data.

One gotcha is that the WPF Elements charting DataSeries expects an IList as its ItemsSource. The F# list type doesn’t implement IList, so we have to be sure to make ViewModel.Boroughs a BCL List instead of a F# list.

Now we can build our view model:

let (national, boroughs) =
  dataContext.LondonBoroughProfiles
  |> Seq.filter (fun r -> r.MaleLifeExpectancy20072009.HasValue && r.FemaleLifeExpectancy20072009.HasValue)
  |> Seq.map (fun r -> { Area = r.Area; MaleLifeExpectancy = r.MaleLifeExpectancy20072009.Value; FemaleLifeExpectancy = r.FemaleLifeExpectancy20072009.Value; Employment = r.EmploymentRate2009.Value })
  |> Seq.toList
  |> List.partition (fun b -> b.Area = "National Comparator")
 
let viewModel = { Boroughs = List<_>(boroughs); National = List.head national }

Most of this should be self-explanatory — we are downloading all the borough data from the data context provided by the OData type provider, filtering out a couple of containers that don’t have data, and mapping away the nullable values. (We could have done some of this using F# query expressions, but it’s not worth it for this data set.) The only thing that may be unfamiliar is List.partition, which splits a list into two lists, the first containing things that satisfy the predicate and the second containing things that don’t. The assignment therefore puts the ‘National Comparator’ pseudo-borough into the ‘national’ list, and the real boroughs into the ‘boroughs’ list.

The end result of all this is a value called ‘viewModel’ of type ViewModel, and we can now consume this from a C# application:

// Code-behind for MainWindow.xaml -- or of course you could automatically
// wire it up using Caliburn Micro.
 
public partial class MainWindow : Window
{
  public MainWindow()
  {
    InitializeComponent();
 
    DataContext = Data.viewModel;
  }
}
<e:Chart Margin="12">
  <e:LineSeries ItemsSource="{Binding Boroughs}" XBinding="{Binding Area}" YBinding="{Binding MaleLifeExpectancy}" SeriesBrush="Navy" />
  <e:LineSeries ItemsSource="{Binding Boroughs}" XBinding="{Binding Area}" YBinding="{Binding FemaleLifeExpectancy}" SeriesBrush="Red" />
  <e:LineSeries ItemsSource="{Binding Boroughs}" XBinding="{Binding Area}" YBinding="{Binding Employment}" YAxisTitle="Employment (%)" SeriesBrush="Green" />
  <e:Chart.XAxis>
    <e:ChartAxis LabelRotation="90" LabelStep="1" MajorTickSpacing="1" />
  </e:Chart.XAxis>
  <e:Chart.YAxis>
    <e:ChartAxis Minimum="65" Maximum="90" Title="Life Expectancy (yr)" />
  </e:Chart.YAxis>
  <e:Chart.AlternativeYAxes>
    <e:ChartAxis Minimum="50" Maximum="100" Title="Employment (%)" />
  </e:Chart.AlternativeYAxes>
</e:Chart>

Solution 2: All F# Solution

With F# 3.0, though, it’s now feasible to build WPF applications entirely in F#, without needing to put the user interface into C#. In practice I know most readers of this blog will probably feel more at home in C# than in F# so they’ll want to use F# only for the core data processing anyway — but I think it’s interesting to know how F# 3.0 enables XAML support, plus there are a couple of things that may not be quite so obvious! (This example is based on Steffen Forkmann’s WPF Designer for F# announcement, which is in turn based on Johann Deneux’ XAML type provider.)

The first thing we need to do is create a Windows Application F# project. There isn’t a template for this, but we can take our class library project, go into Properties > Application, and change the Output Type to Windows Application (and add a Program.fs file to contain the application code). Or we can create a new F# Application project and change the Output Type from Console Application to Windows Application.

Next we need the XAML type provider. This is available as a NuGet package from the FSharpx folks. Right-click the F# project, choose Manage NuGet Packages, search for FSharpx.TypeProviders and click Install. (Learn more about FSharpx here.)

We also need to add references to the WPF assemblies (WindowsBase, PresentationCore, PresentationFramework and System.Xaml) and to the WPF Elements DLL.

The last piece of plumbing we need is to handle the licensed controls in WPF Elements. Copy the licenses.licx file from the WPF Elements Sample Explorer to the F# project directory, include it in the F# project and set its Build Action to Embedded Resource.

Now we’re ready to roll, where by ‘roll’ I mean create an actual WPF window.

Right-click the project and choose Add New Item. There’s no XAML template but just choose XML File and change the file extension to .xaml, e.g. MainWindow.xaml. Replace the contents of the .xaml file with the following:

<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:e="clr-namespace:Mindscape.WpfElements.Charting;assembly=Mindscape.WpfElements"
        Title="MainWindow" Height="500" Width="900">
 
  <e:Chart>
    <e:LineSeries ItemsSource="{Binding Boroughs}" XBinding="{Binding Area}" YBinding="{Binding MaleLifeExpectancy}" SeriesBrush="Navy" />
    <e:LineSeries ItemsSource="{Binding Boroughs}" XBinding="{Binding Area}" YBinding="{Binding FemaleLifeExpectancy}" SeriesBrush="Red" />
    <e:LineSeries ItemsSource="{Binding Boroughs}" XBinding="{Binding Area}" YBinding="{Binding Employment}" YAxisTitle="Employment" SeriesBrush="Green" />
    <e:Chart.XAxis>
      <e:ChartAxis LabelRotation="90" LabelStep="1" MajorTickSpacing="1" />
    </e:Chart.XAxis>
    <e:Chart.YAxis>
      <e:ChartAxis Minimum="65" Maximum="90" />
    </e:Chart.YAxis>
    <e:Chart.AlternativeYAxes>
      <e:ChartAxis Minimum="50" Maximum="100" Title="Employment" />
    </e:Chart.AlternativeYAxes>
  </e:Chart>
 
</Window>

One little nasty to watch out for: the F# XAML type provider currently handles only ‘clr-namespace’ namespaces. So we have to refer to the Mindscape.WpfElements.Charting namespace by name, instead of using the URI. Other than that this is the same XAML as in the C# application.

Finally we need to write some code to launch this window and associate it with our view model. In C#, the project template handled this for us but in F# we need to do it ourselves. Fortunately all the hard work is done by the XAML type provider:

open System
open System.Windows
open FSharpx
 
type MainWindow = XAML<"MainWindow.xaml">  // creates the MainWindow type from the MainWindow.xaml file
 
let loadWindow() =
  let window = MainWindow()  // creates a new instance of MainWindow
  window.Root.DataContext <- Data.viewModel
  window.Root
 
[<STAThread>]
(new Application()).Run(loadWindow())
|> ignore

You can now build and run the application, and you’ll find it works just like the C# one. Of course, it has taken a bit more effort to set up, but that’s a one-off, and you can now enjoy using F# for your interaction logic as well as your business logic.

You can download the source for the F# WPF Elements demo project here. You will need Visual Studio 11 Beta, and a copy of Mindscape WPF Elements (download the free edition including a 60-day trial of the charting controls). (To run the program, you’ll also need to sign up for the free London Borough Profiles data set on Azure Datamarket.) If you have any questions or problems, post in the comments or the support forum!

Tagged as F#, WPF Elements

Leave a Reply

Archives

Join our mailer

You should join our newsletter! Sent monthly:

Back to Top