The aim of this tutorial is to explain how to use Elmish.Uno, building in complexity from start (what is MVU?) to end (using complex bindings and applying optimizations).
This tutorial is not directly related to the many samples in the Elmish.Uno repository, but complements them well. The samples are complete, fully functional apps demonstrating selected aspects of Elmish.Uno. The samples show; the tutorial explains.
This tutorial assumes working F# knowledge. If you’re new to F#, Scott Wlaschin’s blog F# for fun and profit is a great place to start (and continue) learning the ins and outs of F# and functional programming. His book Domain Modeling Made Functional is also a great resource for learning F# (and in particular how it can be used for domain modeling). You can find many more excellent resources at fsharp.org.
This tutorial also assumes some knowledge of WPF and MVVM.
Suggestions for improvements are welcome. For large changes, please open an issue. For small changes (e.g. typos), simply submit a PR.
- The MVU (Elm/Elmish) architecture
- Some MVU tips for beginners
- Getting started with Elmish.Uno
- The Elmish.Uno bindings
- Additional resources
MVU stands for Model-View-Update. It is a purely functional front-end architecture commonly used in Elm, a strongly typed pure functional language that compiles to JavaScript.
The “model” part of the MVU name refers to an immutable data structure that contains all the state in your app. By “all the state” we mean all the state that influences any kind of domain/business logic, and all the state that is needed to render the UI. By storing all the state in a single “atom”, data synchronization problems between different parts of the app are a thing of the past. Note that the model is concerned with domain concepts, not UI concepts. Ideally (if not always in practice), you should be able to use the same model to target different UIs using the MVU pattern (a WPF app, a React web app using Feliz, a console app using Terminal.Gui.Elmish, etc.)
For example, the type definition below may be the whole state for an app containing a single counter that you can increment/decrement by a customizable step size (the classic “hello world” of MVU apps):
type Model = {
Count: int
StepSize: int
}
The above model contains all the state that is needed to render the UI for such a simple counter app.
Additionally, in MVU, the model also comes with an init
function that simply returns the app’s initial state:
let init () = {
Count = 0
StepSize = 1
}
While not part of the MVU name, the message is a central component. It’s just a type that specifies everything that can happen in your app – all the reasons your state may change (all of the “events” in the app, if you will). It’s typically modelled by a discriminated union.
For example, the type definition below may describe all the possible things that can happen in the counter app described above:
type Msg =
| Increment
| Decrement
| SetStepSize of int
Messages are sent (known in the MVU world as “dispatched”) by the UI. We’ll get back to that.
As with the model, the message type is concerned with the domain, and is ideally unrelated to the UI platform.
The “update” part of the MVU name refers to the function that is responsible for updating your model in response to incoming messages. It has the signature 'msg -> 'model -> 'model
. In other words, it is a pure function that accepts a message (something that happened) and the old state, and returns the new state.
For example, for the counter app we have defined the model and message types for, the update
function will look like this:
let update (msg: Msg) (model: Model) : Model =
match msg with
| Increment -> { model with Count = model.Count + model.StepSize}
| Decrement -> { model with Count = model.Count - model.StepSize}
| SetStepSize i -> { model with StepSize = i }
This is where MVU frameworks will differ, since every UI technology is different.
At its core, view
is a function that accepts 1) a model and 2) a function to dispatch messages, and returns something that specifies how the UI will be rendered. This may in theory be the actual UI, though that would be very inefficient. Generally, view
returns a “shadow DOM” (a cheap object graph reflecting the UI) that the framework will intelligently compare with the actual UI so that only the changed parts of the UI will be updated.
In other words: In MVU, the UI is simply a function of the current model.
For example, the function below shows how the UI function might look like for the counter app above (using an imaginary UI library/syntax):
let view (model: Model) (dispatch: Msg -> unit) =
Container(
Children = [
Paragraph(Text = sprintf "Current count: %i" model.Count)
IntegerInput(
Label = "Step size",
Value = model.StepSize,
OnChange = fun value -> dispatch (SetStepSize value)
)
Button(
Text = "Decrement",
OnClick = fun () -> dispatch Decrement
)
Button(
Text = "Increment",
OnClick = fun () -> dispatch Increment
)
]
)
Note that the core Elmish library defines the following type alias:
type Dispatch<'msg> = 'msg -> unit
Therefore, you will normally see dispatch
typed as Dispatch<'msg>
instead of 'msg -> unit
.
The view
example above shows dynamic views, which is how “proper” MVU works. Creating views as a simple function of the model is a very powerful technique, is conceptually very simple, and allows for good composability.
In Elmish.Uno, however, the views are defined externally in XAML. The UI is static and is not defined or changed by the view
code; hence, Elmish.Uno is said to use static views.
You set up bindings in the XAML views as you normally would if using MVVM. Then, in the view
function, you use Elmish.Uno to declaratively create a “view model” of sorts that contain the data the view will bind to. Therefore the view
function is normally called bindings
in Elmish.Uno.
For example, the counter app may look like this:
let bindings () : Binding<Model, Msg> list = [
"CounterValue" |> Binding.oneWay (fun m -> m.Count)
"Increment" |> Binding.cmd Increment
"Decrement" |> Binding.cmd Decrement
"StepSize" |> Binding.twoWay(
(fun m -> float m.StepSize),
(int >> SetStepSize)
)
]
The actual bindings will be explained in detail later, but explained simply, the code above will create a view-model with:
- an
int
get-only propertyCounterValue
returningmodel.Count
- two get-only properties
Increment
andDecrement
that areICommand
s that can always execute and, when executed, dispatches theIncrement
andDecrement
messages, respectively - a
float
get-set propertyStepSize
returningmodel.StepSize
and which, when set, dispatches theSetStepSize
message with the number
Another important difference between normal MVU view
functions and Elmish.Uno’s update
function is that view
is called every time the model has been updated, whereas bindings
is only called once, when the “view model” is initialized. After that, it is the functions used in the bindings themselves that are called when the model is updated. Therefore, bindings
do not accept a model
or dispatch
parameter. The model
is instead passed separately in each binding, and the dispatch
isn’t visible at all; you simply specify the message to be dispatched, and Elmish.Uno will take care of dispatching the message.
This is yet another part of MVU that is not in the name. Not to be confused with WPF’s ICommand
, the command in MVU is the only way you do side effects.
Think about it: If the update function must be pure, how can we do side effects like making an HTTP call or reading from disk? Or alternatively, if we decided to make update
impure (which is possible in F#, but not in Elm) and do some long-running IO there, wouldn’t that block the whole app (since the update loop can only process one message at a time for concurrency reasons)?
The answer is that there are actually two variants of the update
function: For very simple apps, as shown above, you can use the simple update
version that just returns the new model. For more complex apps that need to use commands, the update
function can return both the new model and a command in a tuple:
update: 'msg -> 'model -> 'model * Cmd<'msg>
What is a command, you ask? It’s simply the “top level” of three type aliases:
type Dispatch<'msg> = 'msg -> unit
type Sub<'msg> = Dispatch<'msg> -> unit
type Cmd<'msg> = Sub<'msg> list
We have encountered Dispatch<'msg>
previously. It is the type of the dispatch
argument to the normal MVU view
function. It is simply an alias for a function that accepts a message and sends it to the MVU update loop so that it ends up in update
.
The next alias, Sub<'msg>
(short for “subscription”) is simply a function that accepts a dispatcher and returns unit
. This function can then dispatch whatever messages it wants when it wants, e.g. by setting up event subscriptions.
For example, here is such a function that, when called, will start dispatching a SetTime
message every second. The whole timerTick
function (without applying dispatch
) has the signature Sub<Msg>
:
let timerTick (dispatch: Dispatch<Msg>) =
let timer = new System.Timers.Timer(1000.)
timer.Elapsed.Add (fun _ -> dispatch (SetTime DateTimeOffset.Now))
timer.Start()
This is the kind of function that you pass to Program.withSubscription
, which allows you to start arbitrary non-UI message dispatchers when the app starts. For example, you can start timers (as shown above), subscribe to other non-UI events, start a MailboxProcessor
, etc.
The final alias, Cmd<'msg>
, is just a list of Sub<'msg>
, i.e. a list of Dispatch<'msg> -> unit
functions. In other words, the update
function can return a list of Dispatch<'msg> -> unit
functions that the MVU update loop will execute. These functions, as you saw above, can dispatch any message at any time. Therefore, if you need to do impure stuff such as calling a web API, you simply create a function accepting dispatch
, perform the call there (likely using async
), and use the dispatch
argument to dispatch a message when you receive a response.
In other words, you don’t call the impure functions yourself; the MVU library calls them for you. Furthermore, from the point of view of your model, everything happens asynchronously (in the sense that your app and update loop continues without waiting on a response, and reacts to the “response” message when it arrives).
For example:
- The user clicks a button to log in, which dispatches a
SignInRequested
message - The
update
function returns a new model with anIsBusy = true
value (which can be to show an animation such as a spinner) as well as a command that actually calls the API - The MVU loop calls the command
- The app continues to work normally - the spinner spins because
IsBusy = true
, and any other messages are processed as they would normally be. Note that you are of course free to process messages differently based on the fact thatIsBusy = true
; for example, you may choose to ignore additionalSignInRequested
messages. - When the API call returns, the function that called the API dispatches a suitable message based on the result (e.g.
SignInSuccessful
orSignInFailed
)
Elmish has several helpers in the Cmd
module to easily create commands from normal functions, but if they don’t suit your use-case, you can always write a command directly as a list of Dispatch<'msg> -> unit
functions.
It is generally recommended that you aggressively normalize your model. This is because everything is (normally) immutable, so if a single entity occurs multiple places in your model and that entity should be updated, it must be updated every place it occurs. This opens up for state synchronization bugs.
For example, say you have an app that can display a list of books, and you can click on a book in the list to open a detail view of that book. You might think to represent it with the following model:
type Model = {
Books: Book list
DetailView: Book option
}
However, what if you now want to edit a book? The book may exist in two places – both in the list, and in the DetailView
property.
A better solution is to have the list be the only place to store the Book
objects, and then simply refer to books by ID everywhere else:
type Model = {
Books: Book list
DetailView: BookId option
}
(You don’t have to use list
; often it will make sense to have Map<BookId, Book>
to easily and efficiently get a book by its ID.)
This principle also extends to data in messages: If you have a choice between passing an entity ID and a complete entity object in a message, using an entity ID will usually be the better choice (even if it may not be immediately obvious).
Keep the XAML (and any code-behind) focused on the view, keep bindings
focused on bindings, and keep your model and update
pure. If you need to do anything impure, that's what Command
is for, whether it's writing to disk, connecting to a DB, calling a web API, talking to actors, or anything else. All impure operations can be implemented using commands.
Note that there's nothing stopping you from having mutable state outside your model. For example, if you have persistent connections (e.g. SignalR) that you need to start and stop during the lifetime of your app, you can define them elsewhere and use them in commands from your update
. If you need an unknown number of them, such as one connection per item in a list in your model, you can store them in a dictionary or similar, keyed by the item's ID. This allows you to create, dispose, and remove items according to the data in your model.
When starting out with MVU, it’s easy to fall into the trap of thinking ahead and wondering “how can I split my model/message/update/view into separate components?” For example, if you have two separate “pages” in your app, you might be inclined to think that each page should have its own separate model, message, update, and view. While this technique is needed with many other non-MVU architectures, it is often counterproductive in MVU.
Before delving into the problems, let’s see how it’s done:
module Child =
type Model = { ... }
type Msg = ...
let update msg model = ...
module Parent =
type Model = {
...
Child: Child.Model
}
type Msg =
...
| ChildMsg of Child.Msg
let update msg model =
match msg with
...
| ChildMsg of childMsg -> { model with Child = Child.update childMsg model }
As you can see, there’s some boilerplate involved in the parent component: You must have a model field for the child model, a wrapping message case for the child message, and an update
branch that passes the child message on to the child model.
Now for the problems.
One important problem is that often, “child components” are not in fact separate from their parents, but need access to some of the parent state. Continuing the book example above, say that you want to split the app into a “list component” and a “detail component” with separate models. If you want to have auto-complete of author names when editing a book in the detail component, you need access to the list all books. The only way to accomplish that reliably is to have the complete book list in the child component, too. But that means that every time you update a book, you need to remember to update it in the child component, too. This incurs boilerplate for every piece of duplicated state (since you must have a child message case for updating each piece of duplicated state), and, again, easily causes state synchronization bugs.
Another important problem, again following from the fact that the components are often not separate, is that a child component might need to communicate with its parent. For example, when saving a book, the parent component needs to get the updated book in a message, but the child component can only dispatch its child message type. There are ways to solve this (e.g. make the child update
also return a separate “parent message” type, or have the parent intercept certain child messages), but all of them are usually unnecessary complications and not without drawbacks.
What should you do instead, then? The answer is, simply put, to scale the model/message/update/view separately, and only when needed. It is highly recommended that you read the following reddit thread replies by user rtfeldman
:
First: Never optimize prematurely. Only optimize if you can actually measure that a certain piece of code is giving you problems.
That said: Since everything in MVU is just (pure) functions, functions, and more functions, memoization is a technique that will allow you to easily skip work if inputs are equal. Memoization is simply about storing the inputs and outputs, and if the function is called with a known value, the already computed result is returned. If not, the result is computed by calling the actual function, and the result is stored to be reused later.
In general, there are several ways to memoize: You can memoize all inputs/outputs (may be memory heavy), or just the latest; you can memoize based on structural comparison of inputs (may be expensive), or use referential equality. In MVU architectures, you often need to ensure that when you use parts of your model to compute a result, you only compute the result once until the input changes. Specifically, you might not care about remembering old values, because generally these will never be used. In that case, you can often get very far with this general memoization implementation that memoizes only the last computed value and stores it using the input reference (which works because everything is normally immutable):
let memoize (f: 'a -> 'b) : 'a -> 'b =
let mutable inputOutput = None
fun x ->
match inputOutput with
| Some (x', res) when LanguagePrimitives.PhysicalEquality x x' -> res
| _ ->
let res = f x
inputOutput <- Some (x, res)
res
Usage:
let myExpensiveFun x = ...
let myExpensiveFunMemoized = memoize myExpensiveFun
Then you simply call myExpensiveFunMemoized
instead of myExpensiveFun
in the rest of your code.
It is important that myExpensiveFunMemoized
is defined without arguments to ensure that memoize
is applied only once. If you had written
let myExpensiveFunMemoized x = memoize myExpensiveFun x
then a new memoized version would be created each for each call, which defeats the purpose of memoizing it in the first place.
Furthermore, the implementation above only memoizes functions with a single input. If you need more parameters, you need to create memoize2
, memoize3
, etc. (You could also pass a single tuple argument, but that will never be referentially equal, so you’d need to use structural comparison instead. That might be prohibitively expensive if the input is, say, a large collection of domain objects. Alternatively you might use functionality similar to Elmish.Uno’s elmEq
helper, which is explained later.)
The readme has a “getting started” section that will have you up and running quickly with a simple skeleton solution.
The Elmish.Uno bindings can be categorized into the following types:
- One-way bindings, for when you want to bind to a simple value.
- Two-way bindings, for when you want to bind to a simple value as well as update this value by dispatching a message. Used for inputs, checkboxes, sliders, etc. Can optionally support validation (e.g. provide an error message using
INotifyDataErrorInfo
that can be displayed when an input is not valid). - Command bindings, for when you want a message to be dispatched when something happens (e.g. a button is clicked).
- Sub-model bindings, for when you want to bind to a complex object that has its own bindings.
- Sub-model window bindings, for when you want to control the opening/closing/hiding of new windows.
- Sub-model sequence bindings, for when you want to bind to a collection of complex objects, each of which has its own bindings.
- Other bindings not fitting into the categories above
- Lazy bindings, optimizations of various other bindings that allow skipping potentially expensive computations if the input is unchanged
Additionally, there is a section explaining how most dispatching bindings allow you to wrap the dispatcher to support debouncing/throttling etc.
Relevant sample: SingleCounter
One-way bindings are used when you want to bind to a simple value.
In the counter example mentioned previously, the binding to the counter value is a one-way binding:
"CounterValue" |> Binding.oneWay (fun m -> m.Count)
In XAML, the binding can look like this:
<TextBlock Text="{Binding CounterValue, StringFormat='Counter value: {0}'}" />
A one-way binding simply accepts a function get: 'model -> 'a
that retrieves the value to be displayed.
In F#, it’s common to model missing values using the Option
type. However, WPF uses null
and doesn’t know how to handle the F# Option
type. You could simply convert from Option
to null
(or Nullable<_>
) in the get
function using Option.toObj
(or Option.toNullable
), but this is such a common scenario that Elmish.Uno has a variant of the one-way binding called oneWayOpt
with this behavior built-in. The oneWayOpt
binding accepts a function get: 'model -> 'a option
. If it returns None
, the UI will receive null
. If it returns Some
, the UI will receive the inner value.
Relevant sample: SingleCounter
Two-way bindings are commonly used for any kind of input (textboxes, checkboxes, sliders, etc.). The two-way bindings accept two functions: A function get: 'model -> 'a
just like the one-way binding, and a function set: 'a -> 'model -> 'msg
that accepts the UI value to be set and the current model, and returns the message to be dispatched.
In the counter example above, the two-way binding to the slider value may look like this:
"StepSize" |> Binding.twoWay(
(fun m -> float m.StepSize),
(fun v m -> SetStepSize (int v))
)
The corresponding XAML may look like this:
<Slider
Value="{Binding StepSize}"
TickFrequency="1"
Minimum="1"
Maximum="10"
IsSnapToTickEnabled="True" />
The WPF slider’s value is a float
, but in the model we use an int
. Therefore the binding’s get
function must convert the model’s integer to a float, and conversely, the binding’s “setter” must convert the UI value from a float to an int.
You might think that the get
function doesn’t have to cast to float
. However, 'a
is the same in both get
and set
, and if you return int
in get
, then Elmish.Uno expects the value coming from the UI (which is obj
) to also be int
, and will try to unbox it to int
when being set. Since it actually is a float
, this will fail.
It’s common for the set
function to rely only on the value to be set, not on the model. Therefore, the two-way binding also has an overload where the set
function accepts only the value, not the model. This allows a more shorthand notation:
"StepSize" |> Binding.twoWay(
(fun m -> float m.StepSize),
(int >> SetStepSize)
)
Just like one-way bindings, there is a variant of the two-way binding for option
-wrapped values. The option
wrapping is used in both get
and set
. Elmish.Uno will convert both ways between a possibly null
raw value and an option
-wrapped value.
Relevant sample: Validation
You might want to display validation errors when the input is invalid. The best way to do this in WPF is through INotifyDataErrorInfo
. Elmish.Uno supports this directly through the twoWayValidate
bindings. In addition to get
and set
, this binding also accepts a third parameter that returns the error string to be displayed. This can be returned as string option
(where None
indicates no error), or Result<_, string>
(where Ok
indicates no error; this variant might allow you to easily reuse existing validation functions you have).
Keep in mind that by default, WPF controls do not display errors. To display errors, either use 3rd party controls/styles (such as MaterialDesignInXamlToolkit) or add your own styles (the Validation
sample in this repo demonstrates this).
There are also variants of the two-way validating bindings for option-wrapped values.
Relevant sample: SingleCounter
Command bindings are used whenever you use Command
/CommandParameter
in XAML, such as for button clicks.
For example, for the counter app we have been looking at, the XAML binding to execute a command when the “Increment” button is clicked might look like this:
<Button Command="{Binding Increment}" Content="+" />
The corresponding Elmish.Uno binding that dispatches Msg.Increment
when the command is executed generally looks like this:
"Increment" |> Binding.cmd (fun m -> Increment)
The binding accepts a single function exec: 'model -> 'msg
that accepts the current model and returns the message to be dispatched. Elmish.Uno will convert the message to an ICommand
that dispatches the message when the command is invoked.
For convenience, if you don’t need the model, there is also an overload that directly accepts the message (instead of a model-accepting function). The above can therefore be written like this:
"Increment" |> Binding.cmd Increment
Relevant sample: SingleCounter
A command may not always be executable. As you might know, WPF’s ICommand
interface contains a CanExecute
method that, if false
, will cause WPF to disable the bound control (e.g. the button).
In the counter example, we might want to prohibit negative numbers, disabling the Decrement
button when the model.Count = 0
. This can be written using cmdIf
:
"Decrement" |> Binding.cmdIf (
(fun m -> Decrement),
fun m -> m.Count >= 0
)
There are several ways to indicate that a command can‘t execute. The cmdIf
binding has overloads for the following:
exec: 'model -> 'msg option
, where the command is disabled ifexec
returnsNone
exec: 'model -> Result<'msg, _>
, where the command is disabled ifexec
returnsError
exec: 'model -> 'msg * canExec: 'model -> bool
(as the example above shows), where the command is disabled ifcanExec
returnsfalse
(and as withcmd
, there is also an overload whereexec
is simply the message to dispatch)
Relevant sample: UiBoundCmdParam
There may be times you need to use the XAML CommandParameter
property. You then need to use Elmish.Uno’s cmdParam
binding, which works exactly like cmd
but where exec
function accepts the command parameter as its first parameter.
There is also cmdParamIf
which combines cmdParam
and cmdIf
, allowing you to override the command’s CanExecute
.
Relevant sample: SubModel
Sub-model bindings are used when you want to bind to a complex object that has its own bindings. In MVVM, this happens when one of your view-model properties is another view model with its own properties the UI can bind to.
Perhaps the most compelling use-case for sub-models is when binding the ItemsSource
of a ListView
or similar. Each item in the collection you bind to is a view-model with its own properties that is used when rendering each item. However, the same principles apply when there’s only a single sub-model. Collections are treated later; this section focuses on a single sub-model.
The subModel
binding has three overloads, increasing in complexity depending on how much you need to customize the sub-bindings.
This is sufficient for many purposes. The overload accepts two parameters:
getSubModel: 'model -> 'subModel
to obtain the sub-modelbindings: unit -> Binding<'model * 'subModel, 'msg> list
, the bindings for the sub-model
In other words, inside the sub-bindings, the model parameter (in each binding) is a tuple with the parent model and the sub-model.
For example, let’s say that we have an app where a counter is a part of the app. We might do this:
"Counter" |> Binding.subModel(
(fun m -> m.Counter), // Counter is an object with Count, StepSize, etc.
(fun () -> [
"CounterValue" |> Binding.oneWay (fun (parent, counter) -> counter.Count)
"Increment" |> Binding.cmd IncrementCounter
])
)
As you can see, inside the sub-bindings (which could be extracted to their own bindings
function), the model parameter is a tuple containing the parent state as well as the sub-model state. This is a good default because it’s the most general signature, allowing you access to everything from the parent as well as the sub-model you are binding to. (This is particularly important for sub-model sequence bindings, which are described later.)
Note also that the sub-bindings still use the top-level message type. There is no separate child message type for the sub-model; IncrementCounter
is a case of the parent message type. This is also a good default for the reasons described in the earlier “child components and scaling” section.
This overload is just like the first one except it has an additional parameter to transform the message type:
getSubModel: 'model -> 'subModel
to obtain the sub-modeltoMsg: 'subMsg -> 'msg
to wrap the child message in a parent messagebindings: unit -> Binding<'model * 'subModel, 'subMsg> list
, the bindings for the sub-model
This is useful if you want to use a separate message type in the sub-model bindings. For the toMsg
parameter, you would typically pass a parent message case that wraps the child message type. For example:
"Counter" |> Binding.subModel(
(fun m -> m.Counter),
CounterMsg,
(fun () -> [
"CounterValue" |> Binding.oneWay (fun (parent, counter) -> counter.Count)
"Increment" |> Binding.cmd Increment
])
)
Here, Increment
is a case of the child message type, and CounterMsg
is a parent message case that wraps the counter message type.
If you had passed id
as the toMsg
parameter, you would have the same behavior as the previous simpler overload with no toMsg
.
This is the most complex one, and is required for the following cases:
- recursive models
- proper “child components” with their own model/update/bindings unrelated to their parent
The reasons it’s required for these cases are described further below.
It’s also nice to have if you simply want to “clean up” or otherwise customize the model used in the bindings (e.g. if you don’t need the parent model, only the child model).
Compared to the “level 2” overload, it has one additional parameter, toBindingModel
. All the parameters are:
getSubModel: 'model -> 'subModel
to obtain the sub-modeltoBindingModel: ('model * 'subModel) -> 'bindingModel
to transform the default binding model to whatever you wanttoMsg: 'bindingMsg -> 'msg
to wrap the message used in the sub-model bindings in a parent messagebindings: unit -> Binding<'bindingModel, 'bindingMsg> list
, the bindings for the sub-model
Continuing with the counter example above, it could look like this:
"Counter" |> Binding.subModel(
(fun m -> m.Counter),
(fun (parent, counter) -> counter)
CounterMsg,
(fun () -> [
"CounterValue" |> Binding.oneWay (fun counter -> counter.Count)
"Increment" |> Binding.cmd Increment
])
)
As you see, we transform the default (parent, counter)
tuple into just the counter
, so that the model used in the sub-bindings is only the 'subModel
. Otherwise the example is the same. If you had passed id
to toBindingModel
and toMsg
, you would end up with the same behavior as the simplest variant without toBindingModel
and toMsg
.
The model transformation allowed by this overload is required for a proper, separate “child component” with its own model/message/bindings, because the child component’s bindings would of course not know anything about any parent model. I.e., as demonstrated above, you need the model to be just 'subModel
and not 'model * 'subModel
The model transformation is also required for recursive bindings. Imagine that a counter can contain another counter (in a ChildCounter
property). You would define the (recursive) counter bindings as:
let rec counterBindings () : Binding<CounterModel, CounterMsg> list = [
"CounterValue" |> Binding.oneWay (fun m -> m.Count)
"Increment" |> Binding.cmd Increment
"ChildCounter" |> Binding.subModel(
(fun m -> m.ChildCounter),
(fun (parent, counter) -> counter)
ChildMsg,
counterBindings
])
If you could not transform (parent, counter)
“back” to counter
, you could not reuse the same bindings, and hence not create recursive bindings.
Recursive bindings are demonstrated in the SubModelSeq
sample.
You now have the power to create child components. Use it with great care; as mentioned in the earlier “child components and scaling” section, such separation will often do more harm than good.
Relevant sample: SubModelOpt
You can also use the subModelOpt
binding. The signature is the same as the variants described above, except that getSubModel
returns 'subModel option
. The UI will receive null
when the sub-model is None
.
Additionally, these bindings have an optional sticky: bool
parameter. If true
, Elmish.Uno will “remember” and return the most recent non-null sub-model when the getSubModel
returns None
. This can be useful for example when you want to animate away the UI for the sub-component when it’s set to None
. If you do not use sticky
, the UI will be cleared at the start of the animation, which may look weird.
Relevant sample: NewWindow
The subModelWin
binding is a variant of subModelOpt
that allows you to control the opening/closing/hiding of new windows. It has the same overloads as subModel
and subModelOpt
, with two key differences: First, the sub-model is wrapped in a custom type called WindowState
that is defined like this:
[<RequireQualifiedAccess>]
type WindowState<'model> =
| Closed
| Hidden of 'model
| Visible of 'model
By wrapping the sub-model in WindowState.Hidden
or WindowState.Visible
or returning WindowState.Closed
, you control the opening, closing, showing, and hiding of a window whose DataContext
will be automatically set to the wrapped model. Check out the NewWindow
sample to see it in action.
Secondly, all overloads have the following parameter:
getWindow: 'model -> Dispatch<'msg> -> #Window
This is what’s actually called to create the window. You have access to the current model as well as the dispatch
in case you need to set up message-dispatching event subscriptions for the window.
Additionally, all subModelWin
overloads have two optional parameters. The first is ?onCloseRequested: 'msg
. Returning WindowState.Closed
is the only way to close the window. In order to support closing using external mechanisms (the Close/X button, Alt+F4, or System Menu -> Close), this parameter allows you to specify a message that will be dispatched for these events. You can then react to this message by updating your state so that the binding returns WindowState.Closed
The second optional parameter is ?isModal: bool
. This specifies whether the window will be shown modally (using window.ShowDialog
, blocking the rest of the UI) or non-modally (using window.Show
).
Again, check out the NewWindow
sample to see subModelWin
in action.
Relevant sample: SubModelSeq
If you understand subModel
, then subModelSeq
isn’t much more complex. It has similar overloads, but instead of returning a single sub-model, you return #seq<'subModel>
. Furthermore, all overloads have an additional parameter getId
(which for the “level 1” and “level 2” overloads has signature 'subModel -> 'id
) that gets a unique identifier for each model. This identifier must be unique among all sub-models in the collection, and is used to know which items to add, remove, re-order, and update.
The toMsg
parameter in the “level 2” and “level 3” overloads has the signature 'id * 'subMsg -> 'msg
(compared with just 'subMsg -> 'msg
for subModel
). For this parameter you would typically use a parent message case that wraps both the child ID and the child message. You need the ID to know which sub-model it came from, and thus which sub-model to pass the message along to.
Finally, in the “level 3” overload that allows you to transform the model used for the bindings, the getId
parameter has signature 'bindingModel -> 'id
(instead of 'subModel -> 'id
for the two simpler overloads).
There are two special bindings not yet covered.
Relevant sample: SubModelSelectedItem
The section on model normalization made it clear that it’s better to use IDs than complex objects in messages. This means that for bindings to the selected value of a ListBox
or similar, you’ll likely have better luck using SelectedValue
and SelectedValuePath
rather than SelectedItem
.
Unfortunately some selection-enabled WPF controls only have SelectedItem
and do not support SelectedValue
and SelectedValuePath
. Using SelectedItem
is particularly cumbersome in Elmish.Uno since the value is not your sub-model, but an instance of the Elmish.Uno view-model. To help with this, Elmish.Uno provides the subModelSelectedItem
binding.
This binding works together with a subModelSeq
binding in the same binding list, and allows you to use the subModelSeq
binding’s IDs in your model while still using SelectedItem
from XAML. For example, if you use subModelSeq
to display a list of books identified by a BookId
, the subModelSelectedItem
binding allows you to use SelectedBook: BookId
in your model.
The subModelSelectedItem
binding has the following parameters:
subModelSeqBindingName: string
, where you identify the binding name for the correspondingsubModelSeq
bindingget: 'model -> 'id option
, where you return the ID of the sub-model in thesubModelSeq
binding that should be selectedset: 'id option -> 'msg
, where you return the message to dispatch when the selected item changes (typically this will be a message case wrapping the ID).
You bind the SelectedItem
of a control to the subModelSelectedItem
binding. Then, Elmish.Uno will take care of the following:
- When the UI retrieves the selected item, Elmish.Uno gets the ID using
get
, looks up the correct view-model in thesubModelSeq
binding identified bysubModelSeqBindingName
, and returns that view-model to the UI. - When the UI sets the selected item (which it sets to an Elmish.Uno view-model), Elmish.Uno calls
set
with the ID of the sub-model corresponding to that view-model.
Relevant sample: OneWaySeq
In some cases, you might want to have a one-way binding not to a single, simple value, but to a potentially large collection of simple values. If you use oneWay
for this, the entire list will be replaced and re-rendered each time the model updates.
In the special case that you want to bind to a collection of simple (can be bound to directly) and distinct values, you can use oneWaySeq
. This will ensure that only changed items are replaced/moved.
The oneWaySeq
binding has the following parameters:
get: 'model -> #seq<'a>
, to retrieve the collectionitemEquals: 'a -> 'a -> bool
, to determine whether an item has changedgetId: 'a -> 'id
, to track which items are added, removed, re-ordered, and changed
If the values are not simple (e.g. not strings or numbers), then you can instead use subModelSeq
to set up separate bindings for each item. And if the values are not distinct (i.e., can not be uniquely identified in the collection), then Elmish.Uno won’t be able to track which items are moved, and you can’t use this optimization.
Note that you can always use subModelSeq
instead of oneWaySeq
(the opposite is not true.) The oneWaySeq
binding is slightly simpler than subModelSeq
if the elements are simple values that can be bound to directly.
Note: Lazy bindings may get a complete overhaul soon; see #143.
You may find yourself doing potentially expensive work in one-way bindings. To facilitate simple optimization in these cases, Elmish.Uno provides the bindings oneWayLazy
, oneWayOptLazy
, and oneWaySeqLazy
. The difference between these and their non-lazy counterparts is that they have two extra parameters: equals
and map
.
The optimization is done at two levels. The first optimization is for the update process. As with the non-lazy bindings, the initial get
function is called. For the lazy bindings, this should be cheap; it should basically just return what you need from from the model (e.g. a single item or a tuple or record with multiple items). Then, equals
is used to compare the output of get
with the previous output of get
. If equals
returns true
, the rest of the update process is skipped entirely. If equals
returns false
, the output of get
is passed to map
, which may be expensive, and then the binding is updated normally.
The second optimization is when the UI retrieves the value. The output of map
is cached, so if the UI attempts to retrieve a value multiple times, map
is still only called once. Contrast this the non-lazy bindings, where get
is called each time the value is retrieved by the UI.
Elmish.Uno provides two helpers you can often use as the equals
parameter: refEq
and elmEq
.
refEq
is a good choice ifget
returns a single item (not an inline-created tuple, record, or other wrapper) from your model. It is simply an alias forLanguagePrimitives.PhysicalEquality
(which is essentiallyObject.ReferenceEquals
with better typing). Since the Elmish model is generally immutable, a reference equality check for the output ofget
is a very efficient way to short-circuit the update process. It may cause false negatives if two values are structurally equal but not referentially equal, but this should not be a common case, and structural equality may be prohibitively expensive if comparing e.g. large lists, defeating the purpose.elmEq
is a good choice ifget
returns multiple items from the model wrapped inline in a tuple or record. It will compare each member of theget
return value separately (i.e. each record field, or each tuple item). Reference-typed members will be compared using reference equality, and string members and value-typed members will be compared using structural equality.
You may pass any function you want for equals
; it does not have to be one of the above. For example, if you want structural comparison (note the caveat above however), you can pass (=)
.
Note: This is an advanced optimization that should not be necessary in most cases.
Occasionally you may want to limit the frequency of dispatches from a particular binding. You may therefore want to apply some kind of throttling or debouncing (dispatch at most one message every X millisecond, or only dispatch the latest message after there have been no messages for at least X milliseconds).
To facilitate this, all twoWay
and cmd
bindings as well as subModelSelectedItem
have an optional wrapDispatch
parameter with the signature Dispatch<'msg> -> Dispatch<'msg>
.
This is completely general and allows you to implement all manner of dispatch modifications. There are currently no built-in throttling or debouncing wrappers, but you can write your own.
To show you how you can write them from scratch, here is a throttling wrapper that dispatches the latest message every X milliseconds:
let throttle (durationMs: int) (dispatch: Dispatch<'msg>) : Dispatch<'msg> =
let locker = obj()
let mutable lastMessage = ValueNone
let timer = new System.Timers.Timer(Interval = float durationMs)
timer.Elapsed.Add (fun _ ->
lock locker (fun () ->
lastMessage |> ValueOption.iter dispatch
lastMessage <- ValueNone
)
)
timer.Start()
fun msg ->
async {
lock locker (fun () ->
lastMessage <- ValueSome msg
)
} |> Async.Start
Note the use of Async.Start
. It seems to be required in order to avoid deadlocks, see #114 (comment).
If you want a more general solution, you can make use of FSharp.Control.Reactive (which provides an F#-friendly wrapper over System.Reactive, which you can also just use directly). You can write a general helper function to convert any IObservable
combinator/extension to a dispatch wrapper:
open FSharp.Control.Reactive
let asDispatchWrapper
(configure: IObservable<'msg> -> IObservable<'msg>)
(dispatch: Dispatch<'msg>)
: Dispatch<'msg> =
let subject = Subject.broadcast
subject |> configure |> Observable.add dispatch
fun msg -> async { return subject.OnNext msg } |> Async.Start
Usage:
"SliderValue" |> Binding.twoWay(
(fun m -> float m.SliderValue),
int >> SetSliderValue,
(Observable.sample (TimeSpan.FromMilliseconds 50.) |> asDispatchWrapper))
Note that since the binding
function is only called once (or once per sub-model for the sub-model bindings), you can inline the wrapper creation as shown above. (For a “normal” Elmish architecture where view
is called for each update, you’d have to define it outside).
Note also that throttling as demonstrated above may cause suboptimal behavior for two-way bindings due to the value shown in the UI being locked to the value returned by get
, which isn’t updated until the message is dispatched. For sliders, the slider will “lag” behind the mouse cursor when dragging it, only moving when a message is dispatched and the model updated. For text boxes, throttling will make the cursor jump to the start of the text box while typing, and only some of the characters will be entered.
- The Elmish.Uno readme contains
- a “getting started” section that will get you quickly up and running
- a FAQ with miscellaneous useful information