Monday, June 21, 2010

An F# WPF MVVM Project Template

I've been planning for a while to create an F# WPF MVVM Template to add to the other templates that have been announced on this blog.  A resent post by Mark Pearl provided a great simple example which helped kick me into gear and bring this plan to fruition.

To get us up to date, here are the links to the other templates that I have created:

- Standard WCF Template
- Standard ASP.NET MVC 2 Template

This particular template is slightly different than the others.  While the others had most or all of the code written in F#, the views or endpoints were still provided via a C# project.  In contrast, this F# WPF MVVM template contains only F# projects.

What to Expect:

The code provided by this template creates an application that is loosely based on ExpenseIt (A simple expense report app. defined on this MSDN page). The following screenshot displays the produced application in action:


The Model:

The model for this application is comprised of two simple records:
type Expense =
    { ExpenseType : string
      ExpenseAmount : string}

type ExpenseReport =
    { Name : string
      Department : string
      ExpenseLineItems : seq<expense>}
The View Models:

The view models are fairly standard. Each view model inherits from a ViewModelBase class. The ExpenseItHomeViewModel class contains most of the code. Since these are the two most interesting classes associated with view models, they will be the only two shown.

ViewModelBase.fs
namespace FSharpWpfMvvmTemplate.ViewModel

open System
open System.Windows
open System.Windows.Input
open System.ComponentModel

type ViewModelBase() =
    let propertyChangedEvent = new DelegateEvent<PropertyChangedEventHandler>()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member x.PropertyChanged = propertyChangedEvent.Publish
    member x.OnPropertyChanged propertyName = 
        propertyChangedEvent.Trigger([| x; new PropertyChangedEventArgs(propertyName) |])
ExpenseItHomeViewModel.fs
namespace FSharpWpfMvvmTemplate.ViewModel

open System
open System.Xml
open System.Windows
open System.Windows.Data
open System.Windows.Input
open System.ComponentModel
open System.Collections.ObjectModel
open FSharpWpfMvvmTemplate.Model

type ExpenseItHomeViewModel =   
    [<DefaultValue(false)>] val mutable _collectionView : ICollectionView
    [<DefaultValue(false)>] val mutable _expenseReports : ObservableCollection<ExpenseReport>
    new () as x = {_expenseReports = new ObservableCollection<ExpenseReport>(); 
                   _collectionView = null} then x.Initialize()
    inherit ViewModelBase
    member x.Initialize() =
        x._expenseReports <- x.BuildExpenseReports()
        x._collectionView <- CollectionViewSource.GetDefaultView(x._expenseReports)
        x._collectionView.CurrentChanged.AddHandler(
            new EventHandler(fun s e -> x.OnPropertyChanged "SelectedExpenseReport")) 
    member x.BuildExpenseReports() = 
        let collection = new ObservableCollection<ExpenseReport>()
        let mike = {Name="Mike" 
                    Department="Legal" 
                    ExpenseLineItems = 
                        [{ExpenseType="Lunch" 
                          ExpenseAmount="50"};
                        {ExpenseType="Transportation" 
                         ExpenseAmount="50"}]}
        collection.Add(mike)
        let lisa = {Name="Lisa"
                    Department="Marketing" 
                    ExpenseLineItems = 
                        [{ExpenseType="Document printing" 
                          ExpenseAmount="50"};
                        {ExpenseType="Gift" 
                         ExpenseAmount="125"}]}
        collection.Add(lisa)
        let john = {Name="John" 
                    Department="Engineering"
                    ExpenseLineItems = 
                        [{ExpenseType="Magazine subscription" 
                          ExpenseAmount="50"};
                        {ExpenseType="New machine" 
                         ExpenseAmount="600"};
                        {ExpenseType="Software" 
                         ExpenseAmount="500"}]}
        collection.Add(john)
        let mary = {Name="Mary"
                    Department="Finance"
                    ExpenseLineItems = 
                        [{ExpenseType="Dinner" 
                          ExpenseAmount="100"}]}
        collection.Add(mary)
        collection
    member x.ExpenseReports : ObservableCollection<ExpenseReport> = x._expenseReports
    member x.ApproveExpenseReportCommand = 
        new RelayCommand ((fun canExecute -> true), (fun action -> x.ApproveExpenseReport)) 
    member x.SelectedExpenseReport =
        x._collectionView.CurrentItem :?> ExpenseReport
    member x.ApproveExpenseReport = 
        MessageBox.Show(sprintf "Expense report approved for %s" x.SelectedExpenseReport.Name) |> ignore
The Views:

The views are similar to views used in any WPF MVVM application. The solution has three XAML files: ApplicationResources.xaml, MainWindow.xaml, and ExpenseItHome.xaml. Since ExpenseItHome.xaml is the most interesting of these three, it is provided below:
<UserControl xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase" 
      xmlns:ViewModel="clr-namespace:FSharpWpfMvvmTemplate.ViewModel;assembly=FSharpWpfMvvmTemplate.ViewModel" mc:Ignorable="d" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" d:DesignWidth="424">
    <UserControl.DataContext>
        <ViewModel:ExpenseItHomeViewModel></ViewModel:ExpenseItHomeViewModel>
    </UserControl.DataContext>
    <UserControl.Resources>
        <ResourceDictionary Source="ApplicationResources.xaml" />
    </UserControl.Resources>
    <Grid Margin="10,0,10,10" VerticalAlignment="Stretch">

        <Grid.Resources>
            <!-- Name item template -->
            <DataTemplate x:Key="nameItemTemplate">
                <Label Content="{Binding Path=Name}"/>
            </DataTemplate>
            <!-- Expense Type template -->
            <DataTemplate x:Key="typeItemTemplate">
                <Label Content="{Binding Path=ExpenseType}"/>
            </DataTemplate>
            <!-- Amount item template -->
            <DataTemplate x:Key="amountItemTemplate">
                <Label Content="{Binding Path=ExpenseAmount}"/>
            </DataTemplate>

        </Grid.Resources>

        <Grid.Background>
            <ImageBrush ImageSource="watermark.png"  />
        </Grid.Background>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
        </Grid.RowDefinitions>
        <!-- People list -->
        <Label Grid.Row="0" Grid.ColumnSpan="2" Style="{StaticResource headerTextStyle}" >
            View Expense Report
        </Label>
        <Grid Margin="10" Grid.Column="0" Grid.Row="1" VerticalAlignment="Top">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <Border Grid.Row="1" Style="{StaticResource listHeaderStyle}">
                <Label Style="{StaticResource listHeaderTextStyle}">Names</Label>
            </Border>

            <ListBox Name="peopleListBox" Grid.Row="2" 
                 ItemsSource="{Binding Path=ExpenseReports}"
                 ItemTemplate="{StaticResource nameItemTemplate}"
                 IsSynchronizedWithCurrentItem="True">
            </ListBox>

            <!-- View report button -->
        </Grid>
        <Grid Margin="10" Grid.Column="1" Grid.Row="1" DataContext="{Binding SelectedExpenseReport}" VerticalAlignment="Top">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="57*" />
                <ColumnDefinition Width="125*" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>
            <!-- Name -->
            <StackPanel Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" Orientation="Horizontal">
                <Label Style="{StaticResource labelStyle}">Name:</Label>
                <Label Style="{StaticResource labelStyle}" Content="{Binding Path=Name}"></Label>
            </StackPanel>
            <!-- Department -->
            <StackPanel Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="1" Orientation="Horizontal">
                <Label Style="{StaticResource labelStyle}">Department:</Label>
                <Label Style="{StaticResource labelStyle}" Content="{Binding Path=Department}"></Label>
            </StackPanel>
            <Grid Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="2" VerticalAlignment="Top" HorizontalAlignment="Left">
                <!-- Expense type and Amount table -->
                <DataGrid ItemsSource="{Binding Path=ExpenseLineItems}" ColumnHeaderStyle="{StaticResource columnHeaderStyle}" AutoGenerateColumns="False" RowHeaderWidth="0" Margin="0,0,-139,0">
                    <DataGrid.Columns>
                        <DataGridTextColumn Header="ExpenseType" Binding="{Binding Path=ExpenseType}"  />
                        <DataGridTextColumn Header="Amount" Binding="{Binding Path=ExpenseAmount}" />
                    </DataGrid.Columns>
                </DataGrid>
            </Grid>
        </Grid>
        <Button Grid.Row="2" Command="{Binding ApproveExpenseReportCommand}" Style="{StaticResource buttonStyle}" Grid.Column="1" Margin="0,10,53,0">Approve</Button>
    </Grid>
</UserControl>
Conclusion:

You can download the template installer here and find the full source at http://github.com/dmohl/FSharpWpfMvvmTemplate.  I did run into a few limitations with having the views in an F# project.  Because of these limitations, I would likely use a polyglot approach with a C# project as the view container and F# projects for the model and view model containers for solutions that are any more complex than this example.  I plan to provide a template for the polyglot approach in my next blog post. 

3 comments:

  1. Great post - I was hoping someone would make a template for this!
    Mark Pearl

    ReplyDelete
  2. Great post - I was hoping someone would make a template for this!
    Mark Pearl

    ReplyDelete