Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V4 #337

Merged
merged 79 commits into from
Feb 11, 2021
Merged

V4 #337

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
dd21a24
removed obsolete things
Aug 9, 2020
f70c620
Merge pull request #257 from bender2k14/remove_obsolete
cmeeren Aug 9, 2020
9a6c45c
removed wrap dispatch feature
Jul 22, 2020
fca16e5
add mapMsg
Jul 21, 2020
9db3b24
removed boxMsg
Jul 21, 2020
e18d468
changed OnCloseRequested to be a function that accepts the current model
Jul 22, 2020
36cb345
changed ToMsg to also accept the current model
Jul 22, 2020
d144d4b
added mapMsgWithModel
Jul 22, 2020
f8a35ed
made public the binding mapping functions
Aug 10, 2020
149047d
now have composable bindings in SubModelSeq sample
Aug 5, 2020
ceefe6f
added design-time view model for Counter.xaml
Aug 5, 2020
1863cd0
updated the release notes
Aug 9, 2020
09e8e72
updated tutorial
Aug 10, 2020
f459057
added documentation to the mapping functions
Aug 10, 2020
6d4ac57
Merge pull request #256 from bender2k14/mapMsgWithModel
TysonMN Aug 10, 2020
815dc23
Log using Microsoft.Extensions.Logging.Abstractions; implement WpfPro…
cmeeren Aug 11, 2020
4397f52
swapped order of oneWaySeq and oneWaySeqLazy
Aug 11, 2020
35e4796
moved Utils up in list of files
Aug 11, 2020
77d15a1
now implementing Binding.oneWaySeq by calling oneWaySeqLazy with refE…
Aug 11, 2020
1d1fa03
changed the oneWaySeqLazy documentation to be more precise
Aug 11, 2020
05b6e1d
adjusted the line lengths in the oneWaySeqLazy documentation
Aug 11, 2020
aacb142
adjusted first failing test to match new behavior of oneWaySeqLazy
Aug 11, 2020
54b0a19
adjusted second failing test to match new behavior of oneWaySeqLazy
Aug 11, 2020
d249345
replaced third failing with two tests to match new behavior of oneWay…
Aug 11, 2020
5b65a5e
updated release notes
Aug 11, 2020
acc1f1b
Merge pull request #258 from bender2k14/#157_OneWaySeq_via_OneWaySeqLazy
TysonMN Aug 11, 2020
f4b4f2e
added functions to map type parameters other than 'model and 'msg
Aug 12, 2020
798a7a6
added a box function for each type of binding data
Aug 12, 2020
4ed0cd3
now using new boxing functions
Aug 12, 2020
a033f68
moved elmStyleMerge
Aug 12, 2020
d51ac8b
added a function mapping function for each type of binding data
Aug 12, 2020
477c775
renamed mapOther to mapMinorTypes
Aug 12, 2020
1bd6e84
added a performance measuring function for each type of binding data
Aug 12, 2020
d0beed5
now using new measuring functions
Aug 12, 2020
83994f6
moved OneWay behavior to OneWayData methods
Aug 13, 2020
84d466e
moved OneWayLazy behavior to OneWayLazyData methods
Aug 13, 2020
f522dcd
moved OneWaySeq behavior to OneWaySeqLazyData methods
Aug 13, 2020
e124a6e
moved TwoWay behavior to TwoWayData methods
Aug 13, 2020
b870835
moved TwoWayValidate behavior to TwoWayValidateData methods
Aug 13, 2020
16dae51
changed updateValidationError to take fewer arguments
Aug 13, 2020
52946b0
simplified SubModelBinding and uses
Aug 13, 2020
5ef72bb
simplified SubModelWinBinding and uses
Aug 13, 2020
95974b8
moved SubModelSeq behavior to SubModelSeqData methods
Aug 13, 2020
c7ada9a
moved SubModelSelectedItem behavior to SubModelSelectedItemData methods
Aug 13, 2020
78ae2c1
Merge pull request #259 from bender2k14/stronger_typing
TysonMN Aug 15, 2020
8046b4c
Merge branch 'master' into v4
Aug 28, 2020
dc1a163
updated tutorial to match improved SubModelSeq sample
Aug 28, 2020
fe66c34
fixed typo in tutorial
Aug 28, 2020
1e00ba0
Merge pull request #272 from bender2k14/merge_master_into_v4
TysonMN Aug 28, 2020
9f49927
added two comments to elmStyleMerge
Sep 8, 2020
992c880
the SubModelSeq sample no longer uses mapModelWithMsg
Sep 29, 2020
38d3ac2
now pointing out that Binding<,> is a profunctor
Sep 29, 2020
ff1794e
fixed typo by replacing "mapModelWithMsg" with "mapMsgWithModel"
Sep 29, 2020
219b00c
Merge branch 'master' into v4
Dec 3, 2020
ab5646d
fixed compile error
Dec 3, 2020
6836050
Merge branch 'master' into v4
Dec 4, 2020
c534e34
Merge branch 'master' into v4
Dec 4, 2020
a1f30fa
simplified use of OutMsg iva mapMsg
Dec 4, 2020
a4608af
moved InternalsVisibleToAttribute to fsproj file #309
Dec 15, 2020
d9f6493
added support for multiple validation errors #314
TysonMN Feb 10, 2021
b3df734
Use list comprehension directly
cmeeren Jan 6, 2021
55d5ca0
Use the same error style for both text boxes
cmeeren Jan 6, 2021
5f33f32
Cleanup
cmeeren Jan 6, 2021
c4c6a41
Fix width
cmeeren Jan 6, 2021
66a45f4
Change Elmish version constraint (#317)
TysonMN Feb 10, 2021
cb92c9b
don't require files to end with a new line
Jan 16, 2021
622ec35
improved text about workaround of .NET Core 3 bug for design time sup…
Jan 25, 2021
252f42a
Use App.xaml in samples (#320)
TysonMN Feb 10, 2021
7f4484d
Fix formatting
cmeeren Jan 30, 2021
8335741
Rename FileDialogs.CmdMsg sample to FileDialogsCmdMsg
cmeeren Jan 30, 2021
a7c8a82
Fix sample name in readme
cmeeren Jan 30, 2021
eac82cd
Reorganize sample suffixes (.Views vs. .Core)
cmeeren Jan 30, 2021
8a39b3e
Fix design-time models after #323
cmeeren Feb 2, 2021
ece0457
Fix NewWindow shutdown behavior
cmeeren Feb 3, 2021
120ecc4
Release 3.5.7
TysonMN Feb 5, 2021
376628f
documentation copy-edits
TysonMN Feb 10, 2021
d18e7da
Release 4.0.0-beta-1
TysonMN Feb 10, 2021
f3e3f5e
Merge branch 'master' into v4
TysonMN Feb 11, 2021
532b119
fix compiler errors from incorrect merge
TysonMN Feb 11, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,29 @@ let save text =
#### Can I bind to events and use behaviors?

Sure! Check out the [EventBindingsAndBehaviors sample](https://github.com/elmish/Elmish.WPF/tree/master/src/Samples). Note that you have to install the NuGet package `Microsoft.Xaml.Behaviors.Wpf`.

#### How can I control logging?

Elmish.WPF uses `Microsoft.Extensions.Logging`. To see Elmish.WPF output in your favorite logging framework, use `WpfProgram.withLogger` to pass an `ILoggerFactory`:

```f#
WpfProgram.mkSimple init update bindings
|> WpfProgram.withLogger yourLoggerFactory
|> WpfProgram.runWindow window
```

For example, in Serilog, you need to install Serilog.Extensions.Logging and instantiate `SerilogLoggerFactory`. The samples demonstrate this.

Elmish.WPF logs to these categories:

* `Elmish.WPF.Update`: Logs exceptions (Error level) and messages/models (Trace/Verbose level) during `update`.
* `Elmish.WPF.Bindings`: Logs events related to bindings. Some logging is done at the Error level (e.g. developer errors such as duplicated binding names, using non-existent bindings in XAML, etc.), but otherwise it’s generally just Trace/Verbose for when you really want to see everything that’s happening (triggering `PropertyChanged`, WPF getting/setting bindings, etc.)
* `Elmish.WPF.Performance`: Logs the performance of the functions you pass when creating bindings (`get`, `set`, `map`, `equals`, etc.) at the Trace/Verbose level. Use `WpfProgram.withPerformanceLogThreshold` to set the minimum duration to log.

The specific method of controlling what Elmish.WPF logs depends on your logging framework. For Serilog you can use `.MinimumLevel.Override(...)` to specify the minimum log level per category, like this:

```f#
myLoggerConfiguration
.MinimumLevel.Override("Elmish.WPF.Bindings", LogEventLevel.Verbose)
...
```
19 changes: 19 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
#### 4.0.0-beta-1

* **Breaking:** Removed the obsolete binding functions in the `BindingFn` module
* **Breaking:** Removed the obsolete function `Elmish.WPF.Cmd.showWindow`
* **Breaking:** Removed all occurrences of the argument `wrapDispatch` from the methods used to create a binding. There is currently no migration path. Please create an issue if this is a negative impact for you.
* **Breaking:** App initialization is now done using the `WpfProgram` module instead of the `Program` module
* **Breaking:** Removed `ElmConfig`. For controlling logging, see below. For specifying a binding performance log threshold (corresponding to the old `ElmConfig.MeasureLimitMs` field), use `WpfProgram.withBindingPerformanceLogThreshold`
* **Breaking:** The method `Binding.oneWaySeq` is implemented by calling the method `Binding.oneWaySeqLazy` with `equals` = `refEq` and `map` = `id`. This is a breaking change when using a mutable data structure for the sequence. Compensate by directly calling `Binding.oneWaySeqLazy` with `equals` = `fun _ _ = false`.
* **Breaking:** Some calls to `Binding` methods now include an equality constraint. This only is only breaking if the corresponding type included the `NoEquality` attribute.
* Added binding mapping functions
* Added `mapModel`, `mapMsg`, and `mapMsgWithModel` in both the `Binding` and `Bindings` modules
* These functions enable common model and message mapping logic to be extracted
* See the `SubModelSeq` sample for an excellent use of `mapModel` and `mapMsg`
* Improved logging:
* Now uses `Microsoft.Extensions.Logging` for wide compatibility and easy integration into common log frameworks
* Use `WpfProgram.WithLogger` to pass an `ILoggerFactory` for your chosen log framework
* Can control specific log categories
* See the samples for a demonstration using Serilog

#### 3.5.7
* Excluded 4.* prereleases from possibilities for version of Elmish dependency
* Added support for multiple validation errors
Expand Down
88 changes: 42 additions & 46 deletions TUTORIAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ Table of contents
- [`subModelSeqSelectedItem`](#submodelseqselecteditem)
- [`oneWaySeq`](#onewayseq)
+ [Lazy bindings](#lazy-bindings)
+ [Wrapping dispatch (debouncing/throttling etc.)](#wrapping-dispatch-debouncingthrottling-etc)
+ [Mapping bindings](#mapping-bindings)
- [Example use of `mapModel` and `mapMsg`](#example-use-of-mapModel-and-mapMsg)
- [Theory behind `mapModel` and `mapMsg`](#theory-behind-mapModel-and-mapMsg)
* [Additional resources](#additional-resources)

The MVU (Elm/Elmish) architecture
Expand Down Expand Up @@ -730,66 +732,60 @@ Elmish.WPF provides two helpers you can often use as the `equals` parameter: `re

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

### Wrapping dispatch (debouncing/throttling etc.)
### Mapping Bindings

*Note: This is an advanced optimization that should not be necessary in most cases.*
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 depend on the parent model. The duplicate mapping code can be extracted and written once using the mapping functions `mapModel`, `mapMsg`, and `mapMsgWithModel`.

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

To facilitate this, all `twoWay` and `cmd` bindings as well as `subModelSelectedItem` have an optional `wrapDispatch` parameter with the signature `Dispatch<'msg> -> Dispatch<'msg>`.
Here is a simple example that uses these model and message types.
```F#
type ChildModel =
{ GrandChild1: GrandChild1
GrandChild2: GrandChild2 }

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.
type ChildMsg =
| SetGrandChild1 of GrandChild1
| SetGrandChild2 of GrandChild2

To show you how you can write them from scratch, here is a throttling wrapper that dispatches the latest message every X milliseconds:

```f#
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
type ParentModel =
{ Child: ChildModel }

type ParentMsg =
| ChildMsg of ChildMsg
```

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).
It is possible to create bindings from the parent to the two grandchild fields, but there is duplicate mapping code.

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:
```F#
let parentBindings () : Binding<ParentModel, ParentMsg> list = [
"GrandChild1" |> Binding.twoWay((fun parent -> parent.Child.GrandChild1), SetGrandChild1 >> ChildMsg)
"GrandChild2" |> Binding.twoWay((fun parent -> parent.Child.GrandChild2), SetGrandChild2 >> ChildMsg)
]
```

```f#
open FSharp.Control.Reactive
The functions `mapModel` and `mapMsg` can remove this duplication.
```F#
let childBindings () : Binding<ChildModel, ChildMsg> list = [
"GrandChild1" |> Binding.twoWay((fun child -> child.GrandChild1), SetGrandChild1)
"GrandChild2" |> Binding.twoWay((fun child -> child.GrandChild2), SetGrandChild2)
]

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
let parentBindings () : Binding<ParentModel, ParentMsg> list =
childBindings ()
|> Bindings.mapModel (fun parent -> parent.Child)
|> Bindings.mapMsg ChildMsg
```

Usage:
#### Benefit for design-time view models

```f#
"SliderValue" |> Binding.twoWay(
(fun m -> float m.SliderValue),
int >> SetSliderValue,
(Observable.sample (TimeSpan.FromMilliseconds 50.) |> asDispatchWrapper))
```
With such duplicate mapping code extracted, it is easier to create a design-time view model for the XAML code containing the bindings to `GrandChild1` and `GrandChild2`. Specifically, instead of creating the design-time view model from the `parentBindings` bindings, it can now be created from the `childBindings` bindings. The `SubModelSeq` sample uses this benefit to create a design-time view model for `Counter.xaml`.

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).
#### Theory behind `mapModel` and `mapMsg`

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.
A binding in Elmish.WPF is represented by an instance of type `Binding<'model, 'msg>`. It is a profunctor, which means that
- it is a contravariant functor in `'model` with `mapModel` as the corresponding mapping function for this functor and
- it is a covariant functor in `'msg` with `mapMsg` as the corresponding mapping function for this functor.

Additional resources
--------------------
Expand Down
Loading