Tracking spread trades in F# (and WPF MVVM) – Part II

-

I wanted to ex­per­i­ment with MVVM and WPF in F#, so I de­cided to cre­ate a lit­tle graph­i­cal in­ter­face for the csv file that dri­ves the spread track­ing ap­pli­ca­tion. When I started I thought I needed some kind of a grid with Submit/Cancel but­tons, but the more I thought about it, the more I re­al­ized that I would­n’t need them.

See, I’ve al­ways be one to com­plain about our cur­rent par­a­digm of Open File / Close File / Save File ar­gu­ing that the user should­n’t know about an en­tity called file’. He should­n’t be ex­posed to the fact that the ap­pli­ca­tion is just an in-mem­ory copy of an hard disk ar­ti­fact. His men­tal model should sim­ply be: I open a doc­u­ment, I work on it, I close it, if needed I can make a copy; if I have prob­lems I can re­vert to a pre­vi­ous ver­sion of the same doc­u­ment; If I make an er­ror I can use undo’ to re­vert it. There are no files/​save/​sub­mit/​can­cel in such par­a­digm. There is no file sys­tem.

On the tech­ni­cal side I wanted to ex­per­i­ment with MVVM, even if in this case, the par­a­digm is overkilled (can re­ally use this word?), given the sim­plic­ity of the ap­pli­ca­tion.

In any case, the ViewModel is in F#. It uses two util­ity classes:

// TODO: refactor to remove code repetition below
[<AbstractClass>]
type ViewModelBase () =
    let propertyChanged = new Event<PropertyChangedEventHandler, PropertyChangedEventArgs>()
    interface INotifyPropertyChanged with
        [<CLIEvent>]
        member this.PropertyChanged = propertyChanged.Publish
    member internal this.RaisePropertyChangedEvent(propertyName:string) =
        if not(propertyName = null) then
            let e = new PropertyChangedEventArgs(propertyName)
            let i = this :> INotifyPropertyChanged
            propertyChanged.Trigger(this, e)
type ObservableCollectionWithChanges<'a when 'a :> INotifyPropertyChanged> () =
    inherit ObservableCollection<'a> ()
    let propertyChanged = new Event<PropertyChangedEventHandler, PropertyChangedEventArgs>()
    member c.PropertyChanged = propertyChanged.Publish
    member private c.RaisePropertyChangedEvent(propertyName:string) =
        if not(propertyName = null) then
            let e = new PropertyChangedEventArgs(propertyName)
            let i = c :> INotifyPropertyChanged
            propertyChanged.Trigger(c, e)
    member c.Add(o) =
        base.Add(o)
        o.PropertyChanged.Add(fun x -> c.RaisePropertyChangedEvent(""))

The first one is used as a base for all the view­model en­ti­ties in the ap­pli­ca­tion, the sec­ond one serves as the base for all the col­lec­tions. They both de­fine the cus­tom­ary PropertyChanged event. The lat­ter adds it­self as an ob­server to each ob­ject added to the col­lec­tion so that, when­ever one changes, it gets no­ti­fied and can no­tify its own ob­servers. Look at the c.Add method. A lot of repet­i­tive code here, I would heed the ad­vice of the com­ment on top if this were pro­duc­tion code.

Each line in the csv file is rep­re­sented as a ResultViewModel, hence the fol­low­ing:

type ResultViewModel (d:DateTime, sLong, sShort, tStop) =
    inherit ViewModelBase ()
    let mutable date = d
    let mutable stockLong = sLong
    let mutable stockShort = sShort
    let mutable trailingStop = tStop
    new () = new ResultViewModel(DateTime.Today, "", "", 0)
    member r.Date with get() = date
                       and set newValue =
                            date <- newValue
                            base.RaisePropertyChangedEvent("Date")
    member r.StockLong with get() = stockLong
                       and set newValue =
                            stockLong <- newValue
                            base.RaisePropertyChangedEvent("StockLong")
    member r.StockShort with get() = stockShort
                        and set newValue =
                            stockShort <- newValue
                            base.RaisePropertyChangedEvent("StockShort")
    member r.TrailingStop with get() =
                                trailingStop
                          and set newValue =
                                trailingStop <- newValue
                                base.RaisePropertyChangedEvent("TrailingStop")
    member r.IsThereAnError = r.TrailingStop < 0 || r.TrailingStop > 100

I need the empty con­struc­tor to be able to hook up to the DataGrid add-new ca­pa­bil­ity. There might be an event I could use in­stead, but this is sim­ple enough (even if a bit goofy).

The main view model class then looks like the fol­low­ing:

type MainViewModel (fileName:string) as self =
    inherit ViewModelBase ()
    let mutable results = new ObservableCollectionWithChanges<ResultViewModel>()
    let loadResults () =
        parseFile fileName
        |> Array.iter (fun (d,sl, ss, ts) ->
                        results.Add(new ResultViewModel(d, sl, ss, ts)))
    do
        loadResults ()
        results.CollectionChanged.Add(fun e -> self.WriteResults())
        results.PropertyChanged.Add(fun e -> self.WriteResults())
    member m.Results with get() = results
                     and set newValue =
                        results <- newValue
                        base.RaisePropertyChangedEvent("Results")
    member m.WriteResults () =
        let rs = results
                 |> Seq.map (fun r -> r.Date, r.StockLong, r.StockShort, r.TrailingStop)
        let thereAreErrors = results |> Seq.exists (fun r -> r.IsThereAnError)
        if not thereAreErrors then
            writeFile fileName rs

Things here are more in­ter­est­ing. First of all, in the con­struc­tor I load the re­sults call­ing my model (which I cre­ated in Part I of this se­ries). I then sub­scribe to both the events fired by the col­lec­tion of re­sults. The for­mer is trig­gered when an ob­ject is added/​re­moved, the lat­ter is trig­gered when an ob­ject changes one of its prop­er­ties. When one of them fires, I sim­ply write the new state back to the file. This al­lows me to get rid of Submit/Cancel but­tons. What the user sees on the screen is syn­chro­nized with the disk at all times. The user does­n’t need to know about the file sys­tem.

If this were real, I would also im­ple­ment an undo/​redo mech­a­nism. In such case, my re­liance on ob­ject events might be un­wise. I would prob­a­bly route all the user changes through a com­mand mech­a­nism, so that they can be undo more eas­ily.

That’s it for the mod­elview. The View it­self is as fol­lows:

<Window x:Class="SpreadTradingWPF.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:spreadTrading="clr-namespace:SpreadTradingWPF"
        Title="Spread Trading" Height="350" Width="525" SizeToContent="WidthAndHeight">
        <Window.Resources>
            <spreadTrading:DateToShortStringConverter x:Key="DateToShortStringC" />
            <LinearGradientBrush x:Key="BlueLightGradientBrush" StartPoint="0,0" EndPoint="0,1">
                    <GradientStop Offset="0" Color="#FFEAF3FF"/>
                    <GradientStop Offset="0.654" Color="#FFC0DEFF"/>
                    <GradientStop Offset="1" Color="#FFC0D9FB"/>
            </LinearGradientBrush>
            <Style TargetType="{x:Type DataGrid}">
                <Setter Property="Margin" Value="5" />
                <Setter Property="Background" Value="{StaticResource BlueLightGradientBrush}" />
                <Setter Property="BorderBrush" Value="#FFA6CCF2" />
                <Setter Property="RowBackground" Value="White" />
                <Setter Property="AlternatingRowBackground" Value="#FDFFD0" />
                <Setter Property="HorizontalGridLinesBrush" Value="Transparent" />
                <Setter Property="VerticalGridLinesBrush" Value="#FFD3D0" />
                <Setter Property="RowHeaderWidth" Value="20" />
            </Style>
    </Window.Resources>
        <StackPanel HorizontalAlignment="Center" Name="stackPanel1" VerticalAlignment="Top" Margin="20">
            <TextBlock Text="Spread Trading" Width="135" HorizontalAlignment="Center" FontSize="18" FontWeight="Bold" FontStretch="ExtraExpanded" />
            <DataGrid Height="Auto" Width="Auto" Margin="5" ItemsSource="{Binding Results}" CanUserAddRows ="True" CanUserDeleteRows="True" AutoGenerateColumns="False">
                <DataGrid.RowValidationRules>
                    <spreadTrading:ResultValidationRule ValidationStep="UpdatedValue"/>
                </DataGrid.RowValidationRules>
                <DataGrid.RowValidationErrorTemplate>
                    <ControlTemplate>
                        <Grid Margin="0,-2,0,-2"
                              ToolTip="{Binding RelativeSource={RelativeSource
                              FindAncestor, AncestorType={x:Type DataGridRow}},
                              Path=(Validation.Errors)[].ErrorContent}">
                            <Ellipse StrokeThickness="0" Fill="Red"
                                Width="{TemplateBinding FontSize}"
                                Height="{TemplateBinding FontSize}" />
                            <TextBlock Text="!" FontSize="{TemplateBinding FontSize}"
                                FontWeight="Bold" Foreground="White"
                                HorizontalAlignment="Center"  />
                        </Grid>
                    </ControlTemplate>
                </DataGrid.RowValidationErrorTemplate>
            <DataGrid.Columns>
                    <DataGridTextColumn Header="Date" Binding="{Binding Date, Converter= {StaticResource DateToShortStringC}}"  IsReadOnly="false"/>
                    <DataGridTextColumn Header="Long" Binding="{Binding StockLong}"/>
                    <DataGridTextColumn Header="Short" Binding="{Binding StockShort}" />
                    <DataGridTextColumn Header="Stop" Binding="{Binding TrailingStop}" />
            </DataGrid.Columns>
            </DataGrid>
        </StackPanel>
</Window>

Notice that I styled the grid and I used the right in­can­ta­tions to get val­i­da­tion er­rors and to bind things prop­erly. The DateToShortString con­verter used for the date field might be mildly in­ter­est­ing. It’s in the Utilities.cs file to­gether with a val­i­da­tion rule that just del­e­gates to the IsThereAnError method on each en­tity. In a big­ger ap­pli­ca­tion, you could write this code in a much more reusable way.

[ValueConversion(typeof (DateTime), typeof (string))]
public class DateToShortStringConverter : IValueConverter
{
public Object Convert(
        Object value,
        Type targetType,
        Object parameter,
        CultureInfo culture)
{
    var date = (DateTime) value;
    return date.ToShortDateString();
}
public object ConvertBack(
    object value,
    Type targetType,
    object parameter,
    CultureInfo culture)
    {
        string strValue = value as string;
        DateTime resultDateTime;
        if (DateTime.TryParse(strValue, out resultDateTime))
        {
            return resultDateTime;
        }
        return DependencyProperty.UnsetValue;
    }
}
public class ResultValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var result = (value as BindingGroup).Items[0] as ResultViewModel;
        if(result.IsThereAnError)
            return new ValidationResult(false, "TrailingStop must be between 0 and 100");
        else
            return ValidationResult.ValidResult;
    }
}

After all these niceties, this is what you get.

image

Tags