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

Log using Microsoft.Extensions.Logging.Abstractions; implement WpfProgram #255

Merged
merged 16 commits into from
Aug 11, 2020
Merged
Changes from 14 commits
Commits
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
@@ -296,3 +296,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)
...
```
7 changes: 7 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -3,10 +3,17 @@
* **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`
* 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 `mapMsgWithMsg`
* 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.6

3 changes: 2 additions & 1 deletion src/Elmish.WPF.Tests/ViewModelTests.fs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ open System.Collections.ObjectModel
open System.Collections.Specialized
open System.ComponentModel
open System.Windows.Input
open Microsoft.Extensions.Logging.Abstractions
open FSharp.Interop.Dynamic
open Xunit
open Hedgehog
@@ -26,7 +27,7 @@ module Extensions =


type internal TestVm<'model, 'msg>(model, bindings) as this =
inherit ViewModel<'model, 'msg>(model, (fun x -> this.Dispatch x), bindings, ElmConfig.Default, "")
inherit ViewModel<'model, 'msg>(model, (fun x -> this.Dispatch x), bindings, 1, "", NullLogger.Instance, NullLogger.Instance)

let pcTriggers = ConcurrentDictionary<string, int>()
let ecTriggers = ConcurrentDictionary<string, int>()
17 changes: 0 additions & 17 deletions src/Elmish.WPF/Config.fs

This file was deleted.

4 changes: 2 additions & 2 deletions src/Elmish.WPF/Elmish.WPF.fsproj
Original file line number Diff line number Diff line change
@@ -33,16 +33,16 @@
<Compile Include="AssemblyInfo.fs" />
<Compile Include="InternalUtils.fs" />
<Compile Include="InternalTypes.fs" />
<Compile Include="Config.fs" />
<Compile Include="Binding.fs" />
<Compile Include="Utils.fs" />
<Compile Include="ViewModel.fs" />
<Compile Include="ViewModelModule.fs" />
<Compile Include="Program.fs" />
<Compile Include="WpfProgram.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Elmish" Version="[3.0.3, 4)" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.1.6" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net461'">
101 changes: 0 additions & 101 deletions src/Elmish.WPF/Program.fs

This file was deleted.

85 changes: 42 additions & 43 deletions src/Elmish.WPF/ViewModel.fs
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ open System.Collections.Generic
open System.Collections.ObjectModel
open System.ComponentModel
open System.Windows
open Microsoft.Extensions.Logging

open Elmish

@@ -273,8 +274,10 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
( initialModel: 'model,
dispatch: 'msg -> unit,
bindings: Binding<'model, 'msg> list,
config: ElmConfig,
propNameChain: string)
performanceLogThresholdMs: int,
propNameChain: string,
log: ILogger,
logPerformance: ILogger)
as this =
inherit DynamicObject()

@@ -290,20 +293,14 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
let withCaching b = Cached { Binding = b; Cache = ref None }


let log fmt =
let innerLog (str: string) =
if config.LogConsole then Console.WriteLine(str)
if config.LogTrace then Diagnostics.Trace.WriteLine(str)
Printf.kprintf innerLog fmt

let getPropChainFor bindingName =
sprintf "%s.%s" propNameChain bindingName

let getPropChainForItem collectionBindingName itemId =
sprintf "%s.%s.%s" propNameChain collectionBindingName itemId

let notifyPropertyChanged propName =
log "[%s] PropertyChanged \"%s\"" propNameChain propName
log.LogTrace("[{BindingNameChain}] PropertyChanged \"{BindingName}\"", propNameChain, propName)
propertyChanged.Trigger(this, PropertyChangedEventArgs propName)

let raiseCanExecuteChanged (cmd: Command) =
@@ -313,13 +310,13 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
match errors.TryGetValue propName with
| true, err when err = error -> ()
| _ ->
log "[%s] ErrorsChanged \"%s\"" propNameChain propName
log.LogTrace("[{BindingNameChain}] ErrorsChanged \"{BindingName}\"", propNameChain, propName)
errors.[propName] <- error
errorsChanged.Trigger([| box this; box <| DataErrorsChangedEventArgs propName |])

let removeError propName =
if errors.Remove propName then
log "[%s] ErrorsChanged \"%s\"" propNameChain propName
log.LogTrace("[{BindingNameChain}] ErrorsChanged \"{BindingName}\"", propNameChain, propName)
errorsChanged.Trigger([| box this; box <| DataErrorsChangedEventArgs propName |])

let rec updateValidationError model name = function
@@ -340,18 +337,18 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
| Cached b -> updateValidationError model name b.Binding

let measure name callName f =
if not config.Measure then f
if not <| logPerformance.IsEnabled(LogLevel.Trace) then f
else
fun x ->
let sw = System.Diagnostics.Stopwatch.StartNew ()
let r = f x
sw.Stop ()
if sw.ElapsedMilliseconds >= int64 config.MeasureLimitMs then
log "[%s] %s (%ims): %s" propNameChain callName sw.ElapsedMilliseconds name
if sw.ElapsedMilliseconds >= int64 performanceLogThresholdMs then
logPerformance.LogTrace("[{BindingNameChain}] {CallName} ({Elapsed}ms): {MeasureName}", propNameChain, callName, sw.ElapsedMilliseconds, name)
r

let measure2 name callName f =
if not config.Measure then f
if not <| logPerformance.IsEnabled(LogLevel.Trace) then f
else fun x -> measure name callName (f x)

let showNewWindow
@@ -447,7 +444,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
Vm = ref ValueNone }
| ValueSome m ->
let chain = getPropChainFor name
let vm = ViewModel(m, toMsg1 >> dispatch, getBindings (), config, chain)
let vm = ViewModel(m, toMsg1 >> dispatch, getBindings (), performanceLogThresholdMs, chain, log, logPerformance)
Some <| SubModel {
GetModel = getModel
GetBindings = getBindings
@@ -475,10 +472,10 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
}
| WindowState.Hidden m ->
let chain = getPropChainFor name
let vm = ViewModel(m, toMsg1 >> dispatch, getBindings (), config, chain)
let vm = ViewModel(m, toMsg1 >> dispatch, getBindings (), performanceLogThresholdMs, chain, log, logPerformance)
let winRef = WeakReference<_>(null)
let preventClose = ref true
log "[%s] Creating hidden window" chain
log.LogTrace("[{BindingNameChain}] Creating hidden window", chain)
showNewWindow
winRef d.GetWindow vm d.IsModal onCloseRequested
preventClose Visibility.Hidden
@@ -495,10 +492,10 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
}
| WindowState.Visible m ->
let chain = getPropChainFor name
let vm = ViewModel(m, toMsg1 >> dispatch, getBindings (), config, chain)
let vm = ViewModel(m, toMsg1 >> dispatch, getBindings (), performanceLogThresholdMs, chain, log, logPerformance)
let winRef = WeakReference<_>(null)
let preventClose = ref true
log "[%s] Creating and opening window" chain
log.LogTrace("[{BindingNameChain}] Creating and opening window", chain)
showNewWindow
winRef d.GetWindow vm d.IsModal onCloseRequested
preventClose Visibility.Visible
@@ -523,7 +520,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
getModels initialModel
|> Seq.map (fun m ->
let chain = getPropChainForItem name (getId m |> string)
ViewModel(m, (fun msg -> toMsg1 (getId m, msg) |> dispatch), getBindings (), config, chain)
ViewModel(m, (fun msg -> toMsg1 (getId m, msg) |> dispatch), getBindings (), performanceLogThresholdMs, chain, log, logPerformance)
)
|> ObservableCollection
Some <| SubModelSeq {
@@ -543,11 +540,11 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
SubModelSeqBinding = b
} |> withCaching |> Some
| _ ->
log "subModelSelectedItem binding referenced binding '%s', but no compatible binding was found with that name" d.SubModelSeqBindingName
log.LogError("subModelSelectedItem binding referenced binding '{SubModelSeqBindingName}', but no compatible binding was found with that name", d.SubModelSeqBindingName)
None

let bindings =
log "[%s] Initializing bindings" propNameChain
log.LogTrace("[{BindingNameChain}] Initializing bindings", propNameChain)
let dict = Dictionary<string, VmBinding<'model, 'msg>>(bindings.Length)
let dictAsFunc name =
match dict.TryGetValue name with
@@ -556,7 +553,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
let sortedBindings = bindings |> List.sortWith Binding.subModelSelectedItemLast
for b in sortedBindings do
if dict.ContainsKey b.Name then
log "Binding name '%s' is duplicated. Only the first occurrence will be used." b.Name
log.LogError("Binding name '{BindingName}' is duplicated. Only the first occurrence will be used.", b.Name)
else
initializeBinding b.Name b.Data dictAsFunc
|> Option.iter (fun binding ->
@@ -596,7 +593,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
true
| ValueNone, ValueSome m ->
let toMsg1 = fun msg -> b.ToMsg currentModel msg
b.Vm := ValueSome <| ViewModel(m, toMsg1 >> dispatch, b.GetBindings (), config, getPropChainFor bindingName)
b.Vm := ValueSome <| ViewModel(m, toMsg1 >> dispatch, b.GetBindings (), performanceLogThresholdMs, getPropChainFor bindingName, log, logPerformance)
true
| ValueSome vm, ValueSome m ->
vm.UpdateModel m
@@ -608,27 +605,27 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
b.PreventClose := false
match b.WinRef.TryGetTarget () with
| false, _ ->
log "[%s] Attempted to close window, but did not find window reference" winPropChain
log.LogError("[{BindingNameChain}] Attempted to close window, but did not find window reference", winPropChain)
| true, w ->
log "[%s] Closing window" winPropChain
log.LogTrace("[{BindingNameChain}] Closing window", winPropChain)
b.WinRef.SetTarget null
w.Dispatcher.Invoke(fun () -> w.Close ())
b.WinRef.SetTarget null

let hide () =
match b.WinRef.TryGetTarget () with
| false, _ ->
log "[%s] Attempted to hide window, but did not find window reference" winPropChain
log.LogError("[{BindingNameChain}] Attempted to hide window, but did not find window reference", winPropChain)
| true, w ->
log "[%s] Hiding window" winPropChain
log.LogTrace("[{BindingNameChain}] Hiding window", winPropChain)
w.Dispatcher.Invoke(fun () -> w.Visibility <- Visibility.Hidden)

let showHidden () =
match b.WinRef.TryGetTarget () with
| false, _ ->
log "[%s] Attempted to show existing hidden window, but did not find window reference" winPropChain
log.LogError("[{BindingNameChain}] Attempted to show existing hidden window, but did not find window reference", winPropChain)
| true, w ->
log "[%s] Showing existing hidden window" winPropChain
log.LogTrace("[{BindingNameChain}] Showing existing hidden window", winPropChain)
w.Dispatcher.Invoke(fun () -> w.Visibility <- Visibility.Visible)

let showNew vm initialVisibility =
@@ -639,7 +636,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>

let newVm model =
let toMsg1 = fun msg -> b.ToMsg currentModel msg
ViewModel(model, toMsg1 >> dispatch, b.GetBindings (), config, getPropChainFor bindingName)
ViewModel(model, toMsg1 >> dispatch, b.GetBindings (), performanceLogThresholdMs, getPropChainFor bindingName, log, logPerformance)

match !b.VmWinState, b.GetState newModel with
| WindowState.Closed, WindowState.Closed ->
@@ -651,7 +648,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
true
| WindowState.Closed, WindowState.Hidden m ->
let vm = newVm m
log "[%s] Creating hidden window" winPropChain
log.LogTrace("[{BindingNameChain}] Creating hidden window", winPropChain)
showNew vm Visibility.Hidden
b.VmWinState := WindowState.Hidden vm
true
@@ -665,7 +662,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
false
| WindowState.Closed, WindowState.Visible m ->
let vm = newVm m
log "[%s] Creating and opening window" winPropChain
log.LogTrace("[{BindingNameChain}] Creating and opening window", winPropChain)
showNew vm Visibility.Visible
b.VmWinState := WindowState.Visible vm
true
@@ -682,7 +679,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
let create m id =
let toMsg1 = fun msg -> b.ToMsg currentModel msg
let chain = getPropChainForItem bindingName (id |> string)
ViewModel(m, (fun msg -> toMsg1 (id, msg) |> dispatch), b.GetBindings (), config, chain)
ViewModel(m, (fun msg -> toMsg1 (id, msg) |> dispatch), b.GetBindings (), performanceLogThresholdMs, chain, log, logPerformance)
let update (vm: ViewModel<_, _>) m _ = vm.UpdateModel m
let newSubModels = newModel |> b.GetModels |> Seq.toArray
elmStyleMerge b.GetId getTargetId create update b.Vms newSubModels
@@ -740,9 +737,11 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
b.SubModelSeqBinding.Vms
|> Seq.tryFind (fun (vm: ViewModel<obj, obj>) ->
selectedId = ValueSome (b.SubModelSeqBinding.GetId vm.CurrentModel))
log "[%s] Setting selected VM to %A"
propNameChain
log.LogTrace(
"[{BindingNameChain}] Setting selected VM to {SubModelId}",
propNameChain,
(selected |> Option.map (fun vm -> b.SubModelSeqBinding.GetId vm.CurrentModel))
)
selected |> Option.toObj |> box
| Cached b ->
match !b.Cache with
@@ -798,25 +797,25 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
updateValidationError currentModel name binding

override __.TryGetMember (binder, result) =
log "[%s] TryGetMember %s" propNameChain binder.Name
log.LogTrace("[{BindingNameChain}] TryGetMember {BindingName}", propNameChain, binder.Name)
match bindings.TryGetValue binder.Name with
| false, _ ->
log "[%s] TryGetMember FAILED: Property %s doesn't exist" propNameChain binder.Name
log.LogError("[{BindingNameChain}] TryGetMember FAILED: Property {BindingName} doesn't exist", propNameChain, binder.Name)
false
| true, binding ->
result <- tryGetMember currentModel binding
true

override __.TrySetMember (binder, value) =
log "[%s] TrySetMember %s" propNameChain binder.Name
log.LogTrace("[{BindingNameChain}] TrySetMember {BindingName}", propNameChain, binder.Name)
match bindings.TryGetValue binder.Name with
| false, _ ->
log "[%s] TrySetMember FAILED: Property %s doesn't exist" propNameChain binder.Name
log.LogError("[{BindingNameChain}] TrySetMember FAILED: Property {BindingName} doesn't exist", propNameChain, binder.Name)
false
| true, binding ->
let success = trySetMember currentModel value binding
if not success then
log "[%s] TrySetMember FAILED: Binding %s is read-only" propNameChain binder.Name
log.LogError("[{BindingNameChain}] TrySetMember FAILED: Binding {BindingName} is read-only", propNameChain, binder.Name)
success


@@ -830,7 +829,7 @@ and [<AllowNullLiteral>] internal ViewModel<'model, 'msg>
member __.HasErrors =
errors.Count > 0
member __.GetErrors propName =
log "[%s] GetErrors %s" propNameChain (propName |> Option.ofObj |> Option.defaultValue "<null>")
log.LogTrace("[{BindingNameChain}] GetErrors {BindingName}", propNameChain, (propName |> Option.ofObj |> Option.defaultValue "<null>"))
match errors.TryGetValue propName with
| true, err -> upcast [err]
| false, _ -> upcast []
4 changes: 3 additions & 1 deletion src/Elmish.WPF/ViewModelModule.fs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
module Elmish.WPF.ViewModel

open Microsoft.Extensions.Logging.Abstractions

/// Creates a design-time view model using the given model and bindings.
let designInstance (model: 'model) (bindings: Binding<'model, 'msg> list) =
ViewModel(model, ignore, bindings, ElmConfig.Default, "main") |> box
ViewModel(model, ignore, bindings, 1, "main", NullLogger.Instance, NullLogger.Instance) |> box
137 changes: 137 additions & 0 deletions src/Elmish.WPF/WpfProgram.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
namespace Elmish.WPF

open System.Windows
open Microsoft.Extensions.Logging
open Microsoft.Extensions.Logging.Abstractions
open Elmish


type WpfProgram<'model, 'msg> =
internal {
ElmishProgram: Program<unit, 'model, 'msg, unit>
Bindings: Binding<'model, 'msg> list
LoggerFactory: ILoggerFactory
/// Only log calls that take at least this many milliseconds. Default 1.
PerformanceLogThreshold: int
}


[<RequireQualifiedAccess>]
module WpfProgram =


let private create getBindings program =
{ ElmishProgram = program
Bindings = getBindings ()
LoggerFactory = NullLoggerFactory.Instance
PerformanceLogThreshold = 1 }


/// Creates a WpfProgram that does not use commands.
let mkSimple
(init: unit -> 'model)
(update: 'msg -> 'model -> 'model)
(bindings: unit -> Binding<'model, 'msg> list) =
Program.mkSimple init update (fun _ _ -> ())
|> create bindings


/// Creates a WpfProgram that uses commands
let mkProgram
(init: unit -> 'model * Cmd<'msg>)
(update: 'msg -> 'model -> 'model * Cmd<'msg>)
(bindings: unit -> Binding<'model, 'msg> list) =
Program.mkProgram init update (fun _ _ -> ())
|> create bindings


/// Starts the Elmish dispatch loop, setting the bindings as the DataContext
/// for the specified FrameworkElement. Non-blocking. This is a low-level function;
/// for normal usage, see runWindow and runWindowWithConfig.
let startElmishLoop
(element: FrameworkElement)
(program: WpfProgram<'model, 'msg>) =
let mutable lastModel = None

let updateLogger = program.LoggerFactory.CreateLogger("Elmish.WPF.Update")
let bindingsLogger = program.LoggerFactory.CreateLogger("Elmish.WPF.Bindings")
let performanceLogger = program.LoggerFactory.CreateLogger("Elmish.WPF.Performance")

let setState model dispatch =
match lastModel with
| None ->
let vm = ViewModel<'model, 'msg>(model, dispatch, program.Bindings, program.PerformanceLogThreshold, "main", bindingsLogger, performanceLogger)
element.DataContext <- vm
lastModel <- Some vm
| Some vm ->
vm.UpdateModel model

let uiDispatch (innerDispatch: Dispatch<'msg>) : Dispatch<'msg> =
fun msg -> element.Dispatcher.Invoke(fun () -> innerDispatch msg)

let logMsgAndModel (msg: 'msg) (model: 'model) =
updateLogger.LogTrace("New message: {Message}\nUpdated state:\n{Model}", msg, model)

let logError (msg: string, ex: exn) =
updateLogger.LogError(ex, msg)

program.ElmishProgram
|> if updateLogger.IsEnabled LogLevel.Trace then Program.withTrace logMsgAndModel else id
|> Program.withErrorHandler logError
|> Program.withSetState setState
|> Program.withSyncDispatch uiDispatch
|> Program.run


/// Instantiates Application and sets its MainWindow if it is not already
/// running.
let private initializeApplication window =
if isNull Application.Current then
Application () |> ignore
Application.Current.MainWindow <- window


/// Starts the Elmish and WPF dispatch loops. Will instantiate Application and
/// set its MainWindow if it is not already running, and then run the specified
/// window. This is a blocking function.
let runWindow window program =
initializeApplication window
window.Show ()
startElmishLoop window program
Application.Current.Run window


/// Same as mkProgram, except that init and update doesn't return Cmd<'msg>
/// directly, but instead return a CmdMsg discriminated union that is converted
/// to Cmd<'msg> using toCmd. This means that the init and update functions
/// return only data, and thus are easier to unit test. The CmdMsg pattern is
/// general; this is just a trivial convenience function that automatically
/// converts CmdMsg to Cmd<'msg> for you in init and update
let mkProgramWithCmdMsg
(init: unit -> 'model * 'cmdMsg list)
(update: 'msg -> 'model -> 'model * 'cmdMsg list)
(bindings: unit -> Binding<'model, 'msg> list)
(toCmd: 'cmdMsg -> Cmd<'msg>) =
let convert (model, cmdMsgs) =
model, (cmdMsgs |> List.map toCmd |> Cmd.batch)
mkProgram
(init >> convert)
(fun msg model -> update msg model |> convert)
bindings


/// Uses the specified ILoggerFactory for logging.
let withLogger loggerFactory program =
{ program with LoggerFactory = loggerFactory }


/// Subscribe to an external source of events. The subscribe function is called once,
/// with the initial model, but can dispatch messages at any time.
let withSubscription subscribe program =
{ program with ElmishProgram = program.ElmishProgram |> Program.withSubscription subscribe }


/// Only logs binding performance for calls taking longer than the specified number of
/// milliseconds. The default is 1ms.
let withPerformanceLogThreshold threshold program =
{ program with PerformanceLogThreshold = threshold }
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
23 changes: 15 additions & 8 deletions src/Samples/EventBindingsAndBehaviors/Program.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
module Elmish.WPF.Samples.EventBindingsAndBehaviors.Program

open Elmish
open Elmish.WPF
open System.Windows
open System.Windows.Input
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF


type Position = { X: int; Y: int }
@@ -68,10 +69,16 @@ let bindings () : Binding<Model, Msg> list = [

let designVm = ViewModel.designInstance (init ()) (bindings ())


let main window =
Program.mkSimpleWpf init update bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple init update bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/FileDialogs.CmdMsg/FileDialogs.CmdMsg.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
21 changes: 15 additions & 6 deletions src/Samples/FileDialogs.CmdMsg/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Elmish.WPF.Samples.FileDialogs.CmdMsg.Program

open System
open Serilog
open Serilog.Extensions.Logging
open Elmish
open Elmish.WPF

@@ -123,9 +125,16 @@ let timerTick dispatch =


let main window =
Program.mkProgramWpfWithCmdMsg init update bindings toCmd
|> Program.withSubscription (fun _ -> Cmd.ofSub timerTick)
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkProgramWithCmdMsg init update bindings toCmd
|> WpfProgram.withSubscription (fun _ -> Cmd.ofSub timerTick)
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/FileDialogs/FileDialogs.fsproj
Original file line number Diff line number Diff line change
@@ -10,6 +10,12 @@
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
</ItemGroup>
21 changes: 15 additions & 6 deletions src/Samples/FileDialogs/Program.fs
Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@ open System
open System.IO
open System.Threading
open System.Windows
open Serilog
open Serilog.Extensions.Logging
open Elmish
open Elmish.WPF

@@ -99,9 +101,16 @@ let timerTick dispatch =


let main window =
Program.mkProgramWpf init update bindings
|> Program.withSubscription (fun _ -> Cmd.ofSub timerTick)
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkProgram init update bindings
|> WpfProgram.withSubscription (fun _ -> Cmd.ofSub timerTick)
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/NewWindow/NewWindow.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
20 changes: 14 additions & 6 deletions src/Samples/NewWindow/Program.fs
Original file line number Diff line number Diff line change
@@ -2,7 +2,8 @@

open System
open System.Windows
open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF

module App =
@@ -119,14 +120,21 @@ let window2DesignVm = ViewModel.designInstance App.initWindow2 (App.window2Bindi


let main mainWindow (createWindow1: Func<#Window>) (createWindow2: Func<#Window>) =

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

let createWindow1 () = createWindow1.Invoke()
let createWindow2 () =
let window = createWindow2.Invoke()
window.Owner <- mainWindow
window
let bindings = App.mainBindings createWindow1 createWindow2
Program.mkSimpleWpf App.init App.update bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
mainWindow
WpfProgram.mkSimple App.init App.update bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow mainWindow
6 changes: 6 additions & 0 deletions src/Samples/OneWaySeq/OneWaySeq.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
20 changes: 14 additions & 6 deletions src/Samples/OneWaySeq/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Elmish.WPF.Samples.OneWaySeq.Program

open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF


@@ -31,8 +32,15 @@ let bindings () : Binding<Model, Msg> list = [
let designVm = ViewModel.designInstance (init ()) (bindings ())

let main window =
Program.mkSimpleWpf init update bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple init update bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
21 changes: 14 additions & 7 deletions src/Samples/SingleCounter/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Elmish.WPF.Samples.SingleCounter.Program

open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF

type Model =
@@ -38,10 +39,16 @@ let bindings () : Binding<Model, Msg> list = [

let designVm = ViewModel.designInstance init (bindings ())


let main window =
Program.mkSimpleWpf (fun () -> init) update bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple (fun () -> init) update bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/SingleCounter/SingleCounter.fsproj
Original file line number Diff line number Diff line change
@@ -10,6 +10,12 @@
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
</ItemGroup>
21 changes: 15 additions & 6 deletions src/Samples/SubModel/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module Elmish.WPF.Samples.SubModel.Program

open System
open Serilog
open Serilog.Extensions.Logging
open Elmish
open Elmish.WPF

@@ -149,9 +151,16 @@ let timerTick dispatch =


let main window =
Program.mkSimpleWpf App.init App.update App.bindings
|> Program.withSubscription (fun _ -> Cmd.ofSub timerTick)
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple App.init App.update App.bindings
|> WpfProgram.withSubscription (fun _ -> Cmd.ofSub timerTick)
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/SubModel/SubModel.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
20 changes: 14 additions & 6 deletions src/Samples/SubModelOpt/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Elmish.WPF.Samples.SubModelOpt.Program

open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF


@@ -121,8 +122,15 @@ let mainDesignVm = ViewModel.designInstance (App.init ()) (App.bindings ())


let main window =
Program.mkSimpleWpf App.init App.update App.bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple App.init App.update App.bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/SubModelOpt/SubModelOpt.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
20 changes: 14 additions & 6 deletions src/Samples/SubModelSelectedItem/Program.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module Elmish.WPF.Samples.SubModelSelectedItem.Program

open System
open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF

type Entity =
@@ -43,8 +44,15 @@ let bindings () : Binding<Model, Msg> list = [
let designVm = ViewModel.designInstance (init ()) (bindings ())

let main window =
Program.mkSimpleWpf init update bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple init update bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/SubModelSelectedItem/SubModelSelectedItem.fsproj
Original file line number Diff line number Diff line change
@@ -10,6 +10,12 @@
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
</ItemGroup>
20 changes: 14 additions & 6 deletions src/Samples/SubModelSeq/Program.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module Elmish.WPF.Samples.SubModelSeq.Program

open System
open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF


@@ -265,8 +266,15 @@ let counterDesignVm = ViewModel.designInstance Counter.init (Counter.bindings ()
let mainDesignVm = ViewModel.designInstance (App.init ()) (Bindings.rootBindings ())

let main window =
Program.mkSimpleWpf App.init App.update Bindings.rootBindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple App.init App.update Bindings.rootBindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/SubModelSeq/SubModelSeq.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
21 changes: 14 additions & 7 deletions src/Samples/UiBoundCmdParam/Program.fs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Elmish.WPF.Samples.UiBoundCmdParam.Program

open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF


@@ -32,10 +33,16 @@ let bindings () : Binding<Model, Msg> list = [

let designVm = ViewModel.designInstance (init ()) (bindings ())


let main window =
Program.mkSimpleWpf init update bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple init update bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/UiBoundCmdParam/UiBoundCmdParam.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />
21 changes: 14 additions & 7 deletions src/Samples/Validation/Program.fs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
module Elmish.WPF.Samples.Validation.Program

open System
open Elmish
open Serilog
open Serilog.Extensions.Logging
open Elmish.WPF


@@ -48,10 +49,16 @@ let bindings () : Binding<Model, Msg> list = [

let designVm = ViewModel.designInstance (init ()) (bindings ())


let main window =
Program.mkSimpleWpf init update bindings
|> Program.withConsoleTrace
|> Program.runWindowWithConfig
{ ElmConfig.Default with LogConsole = true; Measure = true }
window

Log.Logger <-
LoggerConfiguration()
.MinimumLevel.Override("Elmish.WPF.Update", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Bindings", Events.LogEventLevel.Verbose)
.MinimumLevel.Override("Elmish.WPF.Performance", Events.LogEventLevel.Verbose)
.WriteTo.Console()
.CreateLogger()

WpfProgram.mkSimple init update bindings
|> WpfProgram.withLogger (new SerilogLoggerFactory())
|> WpfProgram.runWindow window
6 changes: 6 additions & 0 deletions src/Samples/Validation/Validation.fsproj
Original file line number Diff line number Diff line change
@@ -9,6 +9,12 @@
<ItemGroup>
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="2.9.0" />
<PackageReference Include="Serilog.Extensions.Logging" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\Elmish.WPF\Elmish.WPF.fsproj" />