Intro
In the previous post, I discussed my continued journey of learning by building a Xamarin.Forms app using F#.
In that post, I discussed the overall architecture:
The architecture above is my attempt at practicing Robert C. Martin’s Clean Architecture. Specifically I make the features of the app the focus of the architecture and I treat everything else as details that are abstracted away via folders.
I treat the integration points of an application as details:
The Integration folder above harbors the modules that integrate with my domain. Such modules would be the application, services, and runtime dependencies.
Types
I began code construction of the application by writing functions to support trade transactions.
The following functions were implemented:
module ManageTradeImpl open Services open Core.Entities open TestAPI let buyShares (service:IBroker) context balance = (context , balance) ||> service.TryPurchase let sellShares (service:IBroker) context = context |> service.TrySell
Amongst, the various types that were flushed out as I wrote functions, I wrote a Broker interface that will eventually rely on IO.
The broker interface was added to my Services library:
The Broker Interface is the following:
namespace Services open Core.Entities type IBroker = abstract member GetInfo : string -> StockInfo option abstract member TryPurchase : Shares -> decimal -> PurchaseResult abstract member TrySell : Shares -> SellResult abstract member InvestmentsOf : string -> SharesWithPrice seq
Here’s how the buyShares function can get executed:
(*Test*) let context = { AccountId= "Bizmonger" Symbol= "ROK" Qty= 100 } let result = buyShares (MockBroker()) context 5000m
Here’s the result:
val result : PurchaseResult = InsufficientFunds {PurchaseAttempt = {AccountId = "Bizmonger"; Symbol = "ROK"; Quantity = 100;}; Balance = 5000M; StockPrice = 300M;}
Here’s another example for the buyShares function:
(*Test*) let context = { AccountId= "Bizmonger" Symbol= "ROK" Qty= 5 } let result = buyShares (MockBroker()) context 5000m
Here’s the result:
val result : PurchaseResult = PurchaseRequested {AccountId = "Bizmonger"; Symbol = "ROK"; Quantity = 5;}
The following types have been defined so far for making trades:
module Core.Entities type StockInfo = { Symbol : string Price : decimal DayLow : decimal DayHigh : decimal } type Shares = { AccountId : string Symbol : string Qty : int } type SharesWithPrice ={ Shares : Shares PricePerShare : decimal Total : decimal } type PurchaseInfo = { AccountId : string Symbol : string Quantity : int } type Owner = { AccountId:string ; Symbol:string } type SellInfo = { AccountId : string Symbol : string Quantity : int } type InsufficientFunds = { PurchaseAttempt : PurchaseInfo Balance : decimal StockPrice : decimal } type PurchaseResult = | PurchaseRequested of PurchaseInfo | UnknownSymbol of PurchaseInfo | InvalidQuantity of PurchaseInfo | InsufficientFunds of InsufficientFunds type SellResult = | SellRequested of SellInfo | InsufficientQuantity of SellInfo
User Interface
A domain is useless if there’s no interaction with it.
Here’s a sample UI that was built in XAML:
Here’s the XAML that resulted in the above UI:
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:converters="clr-namespace:FnTrade.Converters;assembly=FnTrade" xmlns:home="clr-namespace:Home.ViewModels;assembly=Home" x:Class="FnTrade.HomePage"> <ContentPage.BindingContext> <home:HomeViewModel /> </ContentPage.BindingContext> <ContentPage.Resources> <ResourceDictionary> <converters:InstanceToBoolConverter x:Key="InstanceToBoolConverter" /> </ResourceDictionary> </ContentPage.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <ListView x:Name="SharesListView" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" ItemsSource="{Binding Investments}" HorizontalOptions="Center" VerticalOptions="Center"> <ListView.Header> <Grid > <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Label Grid.Row="0" Grid.Column="0" Text="Symbol" FontAttributes="Bold" HorizontalTextAlignment="Center" /> <Label Grid.Row="0" Grid.Column="1" Text="Qty" FontAttributes="Bold" HorizontalTextAlignment="Center" /> <Label Grid.Row="0" Grid.Column="2" Text="Price" FontAttributes="Bold" HorizontalTextAlignment="Center" /> <Label Grid.Row="0" Grid.Column="3" Text="Total" FontAttributes="Bold" HorizontalTextAlignment="Center" /> <BoxView Grid.Row="1" Grid.ColumnSpan="4" Color="Gray" Margin="5,0,5,5" HorizontalOptions="FillAndExpand" HeightRequest="1" /> </Grid> </ListView.Header> <ListView.ItemTemplate> <DataTemplate> <ViewCell> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Label Grid.Column="0" Text="{Binding Shares.Symbol}" HorizontalTextAlignment="Center" /> <Label Grid.Column="1" Text="{Binding Shares.Qty}" HorizontalTextAlignment="Center" /> <Label Grid.Column="2" Text="{Binding PricePerShare, StringFormat='{}{0:c}'}" HorizontalTextAlignment="Center" /> <Label Grid.Column="3" Text="{Binding Total, StringFormat='{}{0:c}'}" HorizontalTextAlignment="Center" /> </Grid> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> <Button Grid.Row="1" Grid.Column="0" Text="Buy More" Command="{Binding Buy}" CommandParameter="{Binding Source={x:Reference SharesListView}, Path=SelectedItem.Shares.Symbol}" IsEnabled="{Binding Source={x:Reference SharesListView}, Path=SelectedItem, Converter={StaticResource InstanceToBoolConverter}}" /> <Button Grid.Row="1" Grid.Column="1" Text="Sell" Command="{Binding Sell}" CommandParameter="{Binding Source={x:Reference SharesListView}, Path=SelectedItem.Shares.Symbol}" IsEnabled="{Binding Source={x:Reference SharesListView}, Path=SelectedItem, Converter={StaticResource InstanceToBoolConverter}}" /> </Grid> </ContentPage>
ViewModel
In order for the UI to present dynamic content, I had to implement a viewmodel.
The following viewmodel was implemented:
namespace Home.ViewModels open System.Windows.Input open Core.IntegrationLogic open Core.Entities open Integration.Factories open Services open Search open System.Collections.ObjectModel type HomeViewModel() as this = inherit ViewModelBase() let dispatcher = getDispatcher() let accountId = getAccountId() let broker = getBroker() :> IBroker let searchCommand = DelegateCommand( (fun _ -> this.StockInfo <- getQuote broker this.Symbol) , fun _ -> true) :> ICommand let sellCommand = DelegateCommand( (fun o -> dispatcher.Sell { Owner.AccountId=accountId ; Symbol=o :?> string} ) , fun _ -> true) :> ICommand let buyCommand = DelegateCommand( (fun o -> dispatcher.Buy { Owner.AccountId=accountId ; Symbol=o :?> string} ) , fun _ -> true) :> ICommand let mutable symbol = "" let mutable stockInfo = None let mutable investments = ObservableCollection<SharesWithPrice>() member this.Load() = let result = broker.InvestmentsOf accountId this.Investments <- ObservableCollection<SharesWithPrice>(result) member this.Symbol with get() = symbol and set(value) = symbol <- value base.NotifyPropertyChanged(<@ this.Symbol @>) member this.StockInfo with get() = stockInfo and set(value) = stockInfo <- value base.NotifyPropertyChanged(<@ this.StockInfo @>) member this.Investments with get() = investments and set(value) = investments <- value base.NotifyPropertyChanged(<@ this.Investments @>) member this.Search = searchCommand member this.Sell = sellCommand member this.Buy = buyCommand
Conclusion
This solution, which is a Work In Process, can be found here on GitHub.