Skip to content

Commit 6cf0b52

Browse files
author
Tyson Williams
committed
updated tutorial
1 parent 1863cd0 commit 6cf0b52

File tree

1 file changed

+41
-47
lines changed

1 file changed

+41
-47
lines changed

TUTORIAL.md

+41-47
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ Table of contents
4949
- [`subModelSeqSelectedItem`](#submodelseqselecteditem)
5050
- [`oneWaySeq`](#onewayseq)
5151
+ [Lazy bindings](#lazy-bindings)
52-
+ [Wrapping dispatch (debouncing/throttling etc.)](#wrapping-dispatch-debouncingthrottling-etc)
52+
+ [Mapping bindings](#mapping-bindings)
53+
- [Example use of `mapModel` and `mapMsg`](#example-use-of-mapModel-and-mapMsg)
54+
- [Theory behind `mapModel` and `mapMsg`](#theory-behind-mapModel-and-mapMsg)
5355
* [Additional resources](#additional-resources)
5456

5557
The MVU (Elm/Elmish) architecture
@@ -728,66 +730,58 @@ Elmish.WPF provides two helpers you can often use as the `equals` parameter: `re
728730

729731
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 `(=)`.
730732

731-
### Wrapping dispatch (debouncing/throttling etc.)
733+
### Mapping Bindings
732734

733-
*Note: This is an advanced optimization that should not be necessary in most cases.*
735+
Sometimes duplicate mapping code exists across several bindings. The duplicate mappings could be from the parent model to a common child model or it could be the wrapping of a child message in a parent message, which might even depend on the parent model. The duplicate mapping code can be extracted and written once using the mapping functions `mapModel`, `mapMsg`, and `mapModelWithMsg`.
734736

735-
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).
737+
#### Example use of `mapModel` and `mapMsg`
736738

737-
To facilitate this, all `twoWay` and `cmd` bindings as well as `subModelSelectedItem` have an optional `wrapDispatch` parameter with the signature `Dispatch<'msg> -> Dispatch<'msg>`.
739+
Here is a simple example that uses these model and message types.
740+
```F#
741+
type ChildModel =
742+
{ GrandChild1: GrandChild1
743+
GrandChild2: GrandChild2 }
738744
739-
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.
745+
type ChildMsg =
746+
| SetGrandChild1 of GrandChild1
747+
| SetGrandChild2 of GrandChild2
740748
741-
To show you how you can write them from scratch, here is a throttling wrapper that dispatches the latest message every X milliseconds:
742-
743-
```f#
744-
let throttle (durationMs: int) (dispatch: Dispatch<'msg>) : Dispatch<'msg> =
745-
let locker = obj()
746-
let mutable lastMessage = ValueNone
747-
let timer = new System.Timers.Timer(Interval = float durationMs)
748-
timer.Elapsed.Add (fun _ ->
749-
lock locker (fun () ->
750-
lastMessage |> ValueOption.iter dispatch
751-
lastMessage <- ValueNone
752-
)
753-
)
754-
timer.Start()
755-
fun msg ->
756-
async {
757-
lock locker (fun () ->
758-
lastMessage <- ValueSome msg
759-
)
760-
} |> Async.Start
749+
type ParentModel =
750+
{ Child: ChildModel }
751+
752+
type ParentMsg =
753+
| ChildMsg of ChildMsg
761754
```
762755

763-
Note the use of `Async.Start`. It seems to be required in order to avoid deadlocks, see [#114 (comment)](https://github.com/elmish/Elmish.WPF/issues/114#issuecomment-532481275).
764-
765-
If you want a more general solution, you can make use of [FSharp.Control.Reactive](http://fsprojects.github.io/FSharp.Control.Reactive/) (which provides an F#-friendly wrapper over [System.Reactive](https://www.nuget.org/packages/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:
756+
It is possible to create bindings from the parent to the two grandchild fields, but there is duplicate mapping code.
766757

767-
```f#
768-
open FSharp.Control.Reactive
769-
770-
let asDispatchWrapper
771-
(configure: IObservable<'msg> -> IObservable<'msg>)
772-
(dispatch: Dispatch<'msg>)
773-
: Dispatch<'msg> =
774-
let subject = Subject.broadcast
775-
subject |> configure |> Observable.add dispatch
776-
fun msg -> async { return subject.OnNext msg } |> Async.Start
758+
```F#
759+
let parentBindings () : Binding<ParentModel, ParentMsg> list = [
760+
"GrandChild1" |> Binding.twoWay((fun parent -> parent.Child.GrandChild1), SetGrandChild1 >> ChildMsg)
761+
"GrandChild2" |> Binding.twoWay((fun parent -> parent.Child.GrandChild2), SetGrandChild2 >> ChildMsg)
762+
]
777763
```
778764

779-
Usage:
765+
The functions `mapModel` and `mapMsg` can remove this duplication.
766+
```F#
767+
let childBindings () : Binding<ChildModel, ChildMsg> list = [
768+
"GrandChild1" |> Binding.twoWay((fun child -> child.GrandChild1), SetGrandChild1)
769+
"GrandChild2" |> Binding.twoWay((fun child -> child.GrandChild2), SetGrandChild2)
770+
]
780771
781-
```f#
782-
"SliderValue" |> Binding.twoWay(
783-
(fun m -> float m.SliderValue),
784-
int >> SetSliderValue,
785-
(Observable.sample (TimeSpan.FromMilliseconds 50.) |> asDispatchWrapper))
772+
let parentBindings () : Binding<ParentModel, ParentMsg> list =
773+
childBindings ()
774+
|> Bindings.mapModel (fun parent -> parent.Child)
775+
|> Bindings.mapMsg ChildMsg
786776
```
787777

788-
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).
778+
See the `SubModelSeq` sample for a another example use of `mapModel` and a use of `mapModelWithMsg`.
779+
780+
#### Theory behind `mapModel` and `mapMsg`
789781

790-
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.
782+
A binding in Elmish.WPF is represented by an instance of type `Binding<'model, 'msg>`. It is a functor in both type parameters. More specifically,
783+
- it a contravariant functor in `'model`, and `mapModel` is the corresponding mapping function for this functor; and
784+
- it is a covariant functor in `'msg`, and `mapMsg` is the corresponding mapping function for this functor.
791785

792786
Additional resources
793787
--------------------

0 commit comments

Comments
 (0)