Welcome! I am Loïc Denuzière aka "Tarmil", and this is my technical blog. I'm a French developer who's quite passionate about functional programming, and I've been using F# for most of my work for over ten years.

I've been one of the main developers of WebSharper, the F# web framework with both server-side and JavaScript-compiled client-side tools. Recently I've been focusing more on Bolero, a library that runs F# applications in WebAssembly using Blazor, and other personal projects.

Follow me on Twitter, Mastodon and find me on GitHub.

Managing page-specific models in Elmish

This article is part of F# Advent Calendar 2019.

GUI applications (web or otherwise) often display their content in one of a number of pages. You can have a login page, a dashboard page, a details page for a type of item, and so on. Whichever page is currently displayed generally has some state (or model, in Elmish parlance) that only makes sense for this page, and can be dropped when switching to a different page. In this article we'll look into a few ways that such a page-specific model can be represented in an Elmish application. The code uses Bolero, but the ideas can apply to any Elmish-based framework, like Fable-Elmish or Fabulous.

Our running example will be a simple book collection app with two pages: a list of books with a text input to filter the books by title, and a book details page.

type Page =
    | List
    | Book of isbn: string

type Book =
    { isbn: string // We use the ISBN as unique identifier
      title: string
      publishDate: DateTime
      author: string }

Note that there can also be some state which, although only used by one page, should be stored in the main model anyway because it needs to persist between page switches. For example, in our application, we don't want to reload the list of all book summaries whenever we switch back to the List page, so we will always store it in the main model.

Page model in the main model

One way to go is to just store each page's model as a field of the main application model. However, we quickly encounter a problem: the state for all pages needs to be initialized from the beginning, not just the initial page.

type ListPageModel =
    { filter: string }
    
type BookPageModel = Book

type Model =
    { page: Page
      books: Book list
      list: ListPageModel
      book: BookPageModel }

let initialModel =
    { page = List
      books = []
      list = { filter = "" }
      book = ??? // What should we put here?
    }

We can of course use an option:

type Model =
    { // We don't need to store the page anymore,
      // since that is determined by which model is Some.
      // page: Page
      books: Book list
      list: ListPageModel option
      book: BookPageModel option }

let initialModel =
    { books = []
      list = Some { filter = "" }
      book = None }

But this violates a principle that I would rather keep true: illegal states should be unrepresentable. What this means is that it is possible to put the application in a nonsensical state, where the page is Book but the book is None. Our update and view functions will have to deal with this using partial functions (ie. functions that aren't correct for all possible input values, and throw exceptions otherwise) such as Option.get.

type BookMsg =
    | SetAuthor of string
    // ...

type Msg =
    | Goto of Page
    | ListMsg of ListMsg
    | BookMsg of BookMsg
    // ...

let update msg model =
    match msg with
    | BookMsg bookMsg ->
        let bookModel = Option.get model.book // !!!!
        let bookModel, cmd = updateBook bookMsg bookModel
        { model with book = Some bookModel }, Cmd.map BookMsg cmd
    // ...

let view model dispatch =
    match model.page with
    | Book isbn ->
        let bookModel = Option.get model.book // !!!!
        bookView bookModel (dispatch << BookMsg)
    // ...

Additionally, when switching pages, in addition to initializing the state of the new page, we may need to make sure that we set the model of other pages to None. In this particular example, each model is very light, so it doesn't really matter; but if there are many different pages and some of their models are large in memory, this can become a concern.

let update msg model =
    match msg with
    | Goto List ->
        { model with
            list = Some { filter = "" }
            book = None // Don't forget this!
        }, Cmd.none
    | Goto (Book isbn) ->
        match model.books |> List.tryFind (fun book -> book.isbn = isbn) with
        | Some book ->
            { model with
                list = None // Don't forget this!
                book = Some book
            }, Cmd.none
        | None ->
            model, Cmd.ofMsg (Error ("Unknown book: " + isbn))
    // ...

Despite these inconvenients, this style is a good choice for an application whose page are organized in a stack, where each page is only accessed directly from a parent page. Actually, the fact that the model can contain several page states becomes an advantage when doing page transition animations, since during the animation, two pages are in fact displayed on the screen. In particular, this is quite common for mobile applications. Because of this, it is a recommended style in Fabulous, as shown by the sample application FabulousContacts.

Page model in a union

Separate page union and page model union

An alternative is to store the page model as a union, with one case per page just like the Page union, but with models as arguments.

type PageModel =
    | List of ListPageModel
    | Book of BookPageModel

type Model =
    { page: PageModel
      books: Book list }

let initialModel =
    { page = PageModel.List { filter = "" }
      books = [] }

The model is now correct by construction: it is not possible to accidentally construct an inconsistent state.

Unfortunately the types still allow receiving eg. a BookMsg when the current page is not Book; but such messages can just be ignored. A nice way to do this is to match on the message and the page together:

let update msg model =
    match msg, model.page with
    | ListMsg listMsg, List listModel ->
        let listModel, cmd = updateList listMsg listModel
        { model with page = List listModel }, Cmd.map ListMsg cmd
    | ListMsg _, _ -> model, Cmd.none // Ignore irrelevant message
    | BookMsg bookMsg, Book bookModel ->
        let bookModel, cmd = updateBook bookMsg bookModel
        { model with page = Book bookModel }, Cmd.map BookMsg cmd
    | BookMsg _, _ -> model, Cmd.none // Ignore irrelevant message
    // ...

Note: we could handle all irrelevant messages at once in a final | _ -> model, Cmd.none, but then we would lose the exhaustiveness check on msg. So if later we add a message but forget to handle it, the compiler wouldn't warn us.

As before, when switching to a page, the initial model is decided in the update handler for the Goto message.

let update msg model =
    match msg with
    | Goto Page.List ->
        let pageModel = PageModel.List { filter = "" }
        { model with page = pageModel }, Cmd.none
    | Goto (Page.Book isbn) ->
        match model.books |> List.tryFind (fun book -> book.isbn = isbn) with
        | Some book ->
            { model with page = PageModel.Book book }, Cmd.none
        | None ->
            model, Cmd.ofMsg (Error ("Unknown book: " + isbn))
    // ...

Bolero's PageModel<'T>

Bolero contains a facility to handle such a page model style. It is essentially the same as the previous style, with some internal magic to avoid the need for a separate union type while still playing nice with Bolero's automatic URL routing system.

Separate Elmish program

Finally, I have recently been experimenting with a way to sidestep the whole question of how to embed the messages and models of pages into the main message and model entirely: make each page a separate Elmish program.

This is a style that I haven't seen used in Fable or Fabulous, and in fact I have no idea whether it is possible to use it in those frameworks. In Bolero, while it is still buggy and requires changes to the library itself, I hope to be able to make it available soon.

The idea is that we will have a root Program that will contain the common model (here, the list of books) and dispatch page switches to a nested ProgramComponent. Each page is a different ProgramComponent, with its own model and message types.

Of course, each page still needs to be able to receive a model from the parent program (the list of books for List, and the book as initial model for Book), and to dispatch messages to the main update. These two values can be passed to the component as Blazor parameters. This is the base type that will be implemented by our page components:

[<AbstractClass>]
type NestedProgramComponent<'inModel, 'rootMsg, 'model, 'msg>() =
    inherit ProgramComponent<'model, 'msg>()

    let mutable oldInModel = Unchecked.defaultof<'inModel>

    [<Parameter>]
    member val InModel = oldInModel with get, set
    [<Parameter>]
    member val RootDispatch = Unchecked.defaultof<Dispatch<'rootMsg>> with get, set

    override this.OnParametersSet() =
        if not <| obj.ReferenceEquals (oldInModel, this.InModel) then
            oldInModel <- this.InModel
            this.Rerender()

For example, the Book component is implemented as follows:

type BookComponent() as this =
    inherit NestedProgramComponent<BookModel, Msg, BookModel, BookMsg>()
    
    let update message model =
        // Use this.RootDispatch to send messages to the root program
        // ...
        
    let view model dispatch =
        // ...

    override this.Program =
        Program.mkProgram (fun _ -> this.InModel, Cmd.none) update view

and with a convenience function to instantiate nested program components:

module Html =
    open Bolero.Html

    let ncomp<'T, 'inModel, 'rootMsg, 'model, 'msg
                when 'T :> NestedProgramComponent<'inModel, 'rootMsg, 'model, 'msg>>
            (inModel: 'inModel) (rootDispatch: Dispatch<'rootMsg>) =
        comp<'T> ["InModel" => inModel; "RootDispatch" => rootDispatch] []

we can include the appropriate page component inside the main view:

let view model dispatch =
    cond model.page <| function
    | List ->
        ncomp<ListComponent,_,_,_,_> model.books dispatch
    | Book isbn ->
        cond (model.books |> List.tryFind (fun book -> book.isbn = isbn)) <| function
        | Some book ->
            ncomp<BookComponent,_,_,_,_> book dispatch
        | None ->
            textf "Unknown book: %s" isbn

Conclusion

The above approaches each have their advantages and inconvenients. They can even be mixed and matched, depending on how persistent different pages' models needs to be across page switches. Don't be afraid to experiment!

By Loïc "Tarmil" Denuzière on Tuesday, December 17, 2019

fsharp fsbolero elmish Tweet Permalink

Desktop applications with Bolero and WebWindow

Steve Sanderson recently published WebWindow: a library that runs a web page in a desktop window, piloted from .NET. In particular, it can run Blazor applications natively on the desktop with minimal changes to their code. Unlike client-side Blazor, this doesn't involve any WebAssembly: the Blazor code runs in .NET and interacts directly with the web page.

The Blazor sample app running on WebWindows

This is pretty cool. Although it is contained in a web window like an Electron application, it runs with the speed of a native .NET application and comes in a much smaller package.

Obviously, as soon as I saw it, I had to try to use it with Bolero, my own F# layer for Blazor. As it turns out, it runs quite well! Here's a simple working application; let's see how to create it from scratch.

Creating a Bolero app on WebWindow, step by step

First, if you don't have it yet, install the .NET Core 3.0 SDK and the Bolero project template:

dotnet new -i Bolero.Templates

We can now create a Bolero application.

dotnet new bolero-app --minimal --server=false -o MyBoleroWebWindowApp
cd MyBoleroWebWindowApp

The full template contains a few pages and uses things like remoting that we would need to untangle for this example, so we'll go for the --minimal template instead. Also, we don't want to create an ASP.NET Core host application, so we use --server=false.

We now have a solution with a single project, src/MyBoleroWebWindowApp.Client, which will directly be our executable. Let's fixup the project file MyBoleroWebWindowApp.Client.fsproj.

  • First, this is not a web project:

     <?xml version="1.0" encoding="utf-8"?>
    -<Project Sdk="Microsoft.NET.Sdk.Web">
    +<Project Sdk="Microsoft.NET.Sdk">
    
  • Second, we need to target .NET Core 3.0 and create an executable:

       <PropertyGroup>
    -    <TargetFramework>netstandard2.0</TargetFramework>
    +    <TargetFramework>netcoreapp3.0</TargetFramework>
    +    <OutputType>WinExe</OutputType>
       </PropertyGroup>
    
  • Now that we removed the Web SDK, the wwwroot will not automatically included in the published application anymore. But we still need our assets!

       <ItemGroup>
    +    <Content Include="wwwroot\**">
    +      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    +    </Content>
         <Compile Include="Main.fs" />
         <Compile Include="Startup.fs" />
       </ItemGroup>
    
  • Finally, the NuGet references. We need to remove the Blazor build packages that compile our project into a WebAssembly application, and instead add WebWindow.

       <ItemGroup>
         <PackageReference Include="Bolero" Version="0.10.1-preview9" />
    -    <PackageReference Include="Bolero.Build" Version="0.10.1-preview9" />
    -    <PackageReference Include="Microsoft.AspNetCore.Blazor.Build" Version="3.0-preview9.*" />
    -    <PackageReference Include="Microsoft.AspNetCore.Blazor.DevServer" Version="3.0-preview9.*" />
    +    <PackageReference Include="WebWindow.Blazor" Version="0.1.0-20191120.6" />
       </ItemGroup>
     </Project>
    

The main program, Startup.fs, needs a bit of change to start as a WebWindow application rather than a WebAssembly one. Luckily, Steve made this very easy:

 module Program =
+    open WebWindows.Blazor
 
     [<EntryPoint>]
     let Main args =
-        BlazorWebAssemblyHost.CreateDefaultBuilder()
-            .UseBlazorStartup<Startup>()
-            .Build()
-            .Run()
+        ComponentsDesktop.Run<Startup>("My Bolero app", "wwwroot/index.html")
         0

And finally, the small JavaScript script that boots Bolero is in a different location, so we need to touch wwwroot/index.html:

-    <script src="_framework/blazor.webassembly.js"></script>
+    <script src="framework://blazor.desktop.js"></script>

And with this, we're all set! Run the application using your IDE or from the command line:

dotnet run -p src/MyBoleroWebWindowApp.Client

Note: if you're using Visual Studio, make sure to remove the file Properties/launchSettings.json it may have created while the SDK was still Web; otherwise, it will try (and fail) to run your project with IIS Express.

The Bolero minimal app running on WebWindow

We're on our way! Although since we created a project using the --minimal template, this is pretty empty. Quite literally, if you look at Main.fs:

let view model dispatch =
    empty

Because of this empty view, we're only seeing the banner that is present statically in wwwroot/index.html. Let's make sure that Bolero is indeed running by implementing the "hello world" of the Elmish world, the Counter app, in Main.fs:

module MyBoleroWebWindowApp.Client.Main

open Elmish
open Bolero
open Bolero.Html

type Model = { counter: int }

let initModel = { counter = 0 }

type Message =
    | Increment
    | Decrement

let update message model =
    match message with
    | Increment -> { model with counter = model.counter + 1 }
    | Decrement -> { model with counter = model.counter - 1 }

let view model dispatch =
    concat [
        button [on.click (fun _ -> dispatch Decrement)] [text "-"]
        textf " %i " model.counter
        button [on.click (fun _ -> dispatch Increment)] [text "+"]
    ]

type MyApp() =
    inherit ProgramComponent<Model, Message>()

    override this.Program =
        Program.mkSimple (fun _ -> initModel) update view

And now, if we run again:

The Bolero counter app running on WebWindow

Hurray!

What next?

This is just an experiment to see if Bolero would "just work" with WebWindow and, well, it pretty much does. As Steve said in his blog article, WebWindow itself is an experiment with no promises of developing it into a proper product. But it is still pretty cool, and I want to see how far we can combine it with Bolero. What about remoting with an ASP.NET Core server? Or HTML template hot reloading? These will probably need some adjustments to work nicely with WebWindow, and I think I'll experiment some more with these.

By Loïc "Tarmil" Denuzière on Sunday, November 24, 2019

fsharp fsbolero Tweet Permalink