-
Notifications
You must be signed in to change notification settings - Fork 20
Visual Studio 2012
It's important to distinguish two kind of changes that come with Visual Studio 2012: new 3.0 version of F# language (which still can be compiled down to .NET 4.0) and new 4.5 version of .NET runtime.
The first of two new majors features is Query Expressions. This enables F# to be on a par with Haskell - now it has both imperative-style Computation Expressions (do notation in Haskell) and Query Expressions (List comprehension in Haskell).
A query builder must be available for a data source to be used inside Query Expression. Out of the box F# 3.0 comes with Linq.QueryBuilder which provides support for all IEnumerable
and IQueryable
based sources.
There is one place where we can use query expression:
type StopWatchObservable(frequency, failureFrequencyInSeconds) =
...
interface IObservable<TimeSpan> with
member this.Subscribe observer =
Observable.Interval(period = frequency)
.Where(fun _ -> not !paused)
.Select(fun _ ->
if !generateFailures && watch.Elapsed.TotalSeconds % failureFrequencyInSeconds < 1.0
then failwithf "failing every %.1f secs" failureFrequencyInSeconds
else watch.Elapsed)
.Subscribe(observer)
It would be nice to re-write this piece as query expression (as we would use LINQ query in C#). We need to roll out query builder implementation for IObservable<'T>
. A simplified version of it below:
...
[<RequireQualifiedAccess>]
module Observable =
...
open System.Reactive.Linq
...
type QueryBuilder internal() =
member this.For(source : IObservable<_>, selector : _ -> IObservable<_>) = source.SelectMany(selector)
member this.Zero() = Observable.Empty()
[<CustomOperation("where", MaintainsVariableSpace = true, AllowIntoPattern = true)>]
member this.Where(source : IObservable<_>, [<ProjectionParameter>] predicate : _ -> bool ) = source.Where(predicate)
member this.Yield value = Observable.Return value
[<CustomOperation("select", AllowIntoPattern = true)>]
member this.Select(source : IObservable<_>, [<ProjectionParameter>] selector : _ -> _) = source.Select(selector)
let query = QueryBuilder()
Usage:
type StopWatchObservable(frequency, failureFrequencyInSeconds) =
...
interface IObservable<TimeSpan> with
member this.Subscribe observer =
let xs = Observable.query {
for _ in Observable.Interval(period = frequency) do
where (not !paused)
select (if !generateFailures && watch.Elapsed.TotalSeconds % failureFrequencyInSeconds < 1.0
then failwithf "failing every %.1f secs" failureFrequencyInSeconds
else watch.Elapsed)
}
xs.Subscribe(observer)
Note that F# allows adding custom operators to query expressions and you get syntax highlighting for them! Awesome. See CustomOperationAttribute MSDN documentation for details.
F# 3.0 introduced a ground-shaking feature: type providers. For details watch Don's introductionary Build 2011 presentation or the latest one from Progressive F# Tutorials 2012 conference.
One place in sample application that begs for WSDL Type Provider usage is access to w3schools temperature converter service.
[<AutoOpen>]
[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module FSharp.Windows.Sample
open Microsoft.FSharp.Data.TypeProviders
type TempConvert = WsdlService<"http://www.w3schools.com/webservices/tempconvert.asmx?WSDL">
[<Measure>] type celsius
[<Measure>] type fahrenheit
type TempConvert.ServiceTypes.SimpleDataContextTypes.TempConvertSoapClient with
member this.AsyncCelsiusToFahrenheit(value : float<celsius>) =
async {
let! response = value |> string |> this.CelsiusToFahrenheitAsync |> Async.AwaitTask
return response.Body.CelsiusToFahrenheitResult |> float |> LanguagePrimitives.FloatWithMeasure<fahrenheit>
}
member this.AsyncFahrenheitToCelsius(value : float<fahrenheit>) =
async {
let! response = value |> string |> this.FahrenheitToCelsiusAsync |> Async.AwaitTask
return response.Body.FahrenheitToCelsiusResult |> float |> LanguagePrimitives.FloatWithMeasure<celsius>
}
We made two additions to types generated by WSDL type provider:
- Two extension methods that return
Async
type instead of generated by defaultTask
to smooth usage from F#. - Units of Measure were added to input/output values. Even though we don't have any operations on those values that can benefit from it, the immediate gain is a better documented code. Also, units of measure were moved from F# !PowerPack to standard F# 3.0 language distribution. It makes them particularly attractive to use.
XAML type provider was intended to be a key to pure F# solution.
To start, we convert HexConverter
MVC-triple to use XAML type provider. Below is a relevant code excerpt:
...
open FSharpx
module HexConverter =
type HexConverterWindow = XAML<"View\HexConverterWindow.xaml">
let view() =
let window = HexConverterWindow()
let value = window.Value
value.ShowErrorInTooltip()
let result = {
new View<Events, Model, Window>(window.Root) with
member this.EventStreams =
[
window.Value.PreviewTextInput |> Observable.map(fun args -> ValueChanging(args.Text, fun() -> args.Handled <- true))
]
member this.SetBindings model =
Binding.FromExpression
<@
window.Value.Text <- model.HexValue
@>
}
result.CancelButton <- window.Cancel
result.DefaultOKButton <- window.OK
result
...
module FSharp.Windows.Sample.Extensions
...
open System.Windows.Data
open System.Windows.Controls
type TextBox with
member this.ShowErrorInTooltip() =
let binding = Binding("(Validation.Errors).CurrentItem.ErrorContent", RelativeSource = RelativeSource.Self)
this.SetBinding(TextBox.ToolTipProperty, binding) |> ignore
Let's compare it to the mixed F#/C# approach:
- For data binding DSL to work all accessed controls need to be saved as local values (
let value = window.Value
). This is because XAML Type Provider emits erased types. It means that what you see is not what you get in compiled code. It's like a static micro expansion system. For example,window.Value.Text <- model.HexValue
will be translated to(window.GetChild "Value").Text <- model.HexValue
and binding cannot operate on this. -
value.ShowErrorInTooltip()
binds tooltip content to associated error message. Previously, we had it defined in shared resource style but XAML Type Provider doesn't support resource dictionaries. - Generated window type has
Root
property of typeWindow
. Child controls defined as siblings to it. It means we cannot pass windows as a root and fetch children off it. Very unfortunate design decision.
It gets even worse with MVC-triples that define view type explicitly (which is typical case). Let's convert StockPicker
triple as example.
First we build helper base class View:
...
open FSharpx.TypeProviders.XamlProvider
[<AbstractClass>]
type XamlProviderView<'Events, 'Model>(window : Window) =
let mutable isOK = false
interface IView<'Events, 'Model> with
member this.Subscribe observer =
let xs = this.EventStreams |> List.reduce Observable.merge
xs.Subscribe observer
member this.SetBindings model =
window.DataContext <- model
this.SetBindings model
member this.ShowDialog() =
window.ShowDialog() |> ignore
isOK
member this.Show() =
window.Show()
window.Closed |> Event.map (fun _ -> isOK) |> Async.AwaitEvent
member this.Close isOK' =
isOK <- isOK'
window.Close()
abstract EventStreams : IObservable<'Events> list
abstract SetBindings : 'Model -> unit
It is generalized only by event and model types because Root
property of generated type is always Window
. Here is derived StockPickerView
:
open FSharpx
type StockPickerWindow = XAML<"View\StockPickerWindow.xaml">
type StockPickerView(xaml : StockPickerWindow) as this =
inherit XamlProviderView<unit, StockInfoModel>(xaml.Root)
let companyName = xaml.CompanyName
let addToChart = xaml.AddToChart
let retrieve = xaml.Retrieve
let symbol = xaml.Symbol
do
symbol.CharacterCasing <- CharacterCasing.Upper
this.CancelButton <- xaml.CloseButton
this.DefaultOKButton <- xaml.AddToChart
symbol.ShowErrorInTooltip()
override this.EventStreams =
[
xaml.Retrieve.Click |> Observable.mapTo()
]
override this.SetBindings model =
Binding.FromExpression
<@
companyName.Text <- model.CompanyName
addToChart.IsEnabled <- model.AddToChartEnabled
retrieve.IsEnabled <- isNotNull model.Symbol
@>
Binding.UpdateSourceOnChange <@ symbol.Text <- model.Symbol @>
let converter = BooleanToVisibilityConverter()
Binding.FromExpression
<@
addToChart.Visibility <- converter.Apply model.AddToChartEnabled
@>
It has all same problems as view for HexConverter
. Additionally, instance of window has to be passed, therefore it needs to be created explicitly at calling site:
type StockPricesChartController() =
...
override this.Dispatcher = fun() ->
Async <| fun model ->
async {
let window = StockPickerWindow()
let view = StockPickerView(window)
...
}
An attempt to convert StockPricesChart
failed because I could not get XAML type provider to work with reference to MSChart in xaml:
<UserControl
...
xmlns:WinFormsChart = "clr-namespace:System.Windows.Forms.DataVisualization.Charting;assembly=System.Windows.Forms.DataVisualization"
... />
This post claims it should work. May be I'm missing something.
Another issue is that the standard way to embed user controls doesn't work either:
<Window x:Class="FSharp.Windows.UIElements.MainWindow"
...
xmlns:local="clr-namespace:FSharp.Windows.UIElements">
....
<TabControl x:Name="Tabs" x:FieldModifier="public">
<TabItem Header="Calculator">
<local:CalculatorControl x:Name="Calculator" x:FieldModifier="public"/>
</TabItem>
<TabItem Header="Async Temperature Converter">
<local:TempConveterControl x:Name="TempConveterControl" x:FieldModifier="public"/>
</TabItem>
<TabItem Header="Stock Prices">
<local:StockPricesChartControl x:Name="StockPricesChart" x:FieldModifier="public"/>
</TabItem>
</TabControl>
...
It means that our MainView
has to be composed manually in code and full designer visualization is lost.
After all we have two views HexConverter
and StockPicker
that use XAML type provider. The rest stays C# generation-based.
As much as I appreciate the effort of the guys working on FSharpx XAML Type Provider it is not yet ready for production use. But make your own judgment, I might be wrong.
Starting 4.5 WPF supports INotifyDataErrorInfo interface. It has been available for a while in Silverlight. Basically it makes error notification more natural. I'm not going to delve into boring implementation details. Follow the link above to see MSDN implementation guidelines or look at source code for the chapter. It is worth nothing that semantic has changed a little: zero or many errors can be associated with single property. It is a deviation from IDataErrorInfo
where one to one relationship exists.
Model.SetError
method was renamed to AddError
to better reflect semantic, as a result member constraint for Validation module changed too.
Support for re-throwing exception with original stack trace was a long awaited feature. Finally ExceptionDispatchInfo has arrived. Changes to Mvc.OnException
are minimal:
...
open System.Runtime.ExceptionServices
...
type Mvc ... =
...
abstract OnException : 'Events * exn -> unit
default this.OnException(_, exn) = ExceptionDispatchInfo.Capture(exn).Throw()
...