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) |> ignoreThe 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.
Great post - I was hoping someone would make a template for this!
ReplyDeleteMark Pearl
Great post - I was hoping someone would make a template for this!
ReplyDeleteMark Pearl
Loved rreading this thanks
ReplyDelete