Skip to content

Latest commit

 

History

History
535 lines (356 loc) · 33.1 KB

REFERENCE.md

File metadata and controls

535 lines (356 loc) · 33.1 KB

Elmish.WPF Reference

Table of contents

The Elmish.WPF bindings

The Elmish.WPF 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.

One-way bindings

Relevant sample: SingleCounter - (XAML views and F# core)

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.

Binding to option-wrapped values

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.WPF 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.

Two-way bindings

Relevant sample: SingleCounter - (XAML views and F# core)

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.WPF 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)
)

Binding to option-wrapped values

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.WPF will convert both ways between a possibly null raw value and an option-wrapped value.

Using validation with two-way bindings

Relevant sample: Validation - (XAML views and F# core)

You might want to display validation errors when the input is invalid. The best way to do this in WPF is through INotifyDataErrorInfo. Elmish.WPF 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.

Command bindings

Relevant sample: SingleCounter - (XAML views and F# core)

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.WPF 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.WPF 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

Conditional commands (where you control CanExecute)

Relevant sample: SingleCounter - (XAML views and F# core)

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 if exec returns None
  • exec: 'model -> Result<'msg, _>, where the command is disabled if exec returns Error
  • exec: 'model -> 'msg * canExec: 'model -> bool (as the example above shows), where the command is disabled if canExec returns false (and as with cmd, there is also an overload where exec is simply the message to dispatch)

Using the CommandParameter

Relevant sample: UiBoundCmdParam - (XAML views and F# core)

There may be times you need to use the XAML CommandParameter property. You then need to use Elmish.WPF’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.

Sub-model bindings

Relevant sample: SubModel - (XAML views and F# core)

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.

Level 1: No separate message type or customization of model for sub-bindings

This is sufficient for many purposes. The overload accepts two parameters:

  • getSubModel: 'model -> 'subModel to obtain the sub-model
  • bindings: 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.

Level 2: Separate message type but no customization of model for sub-bindings

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-model
  • toMsg: 'subMsg -> 'msg to wrap the child message in a parent message
  • bindings: 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.

Level 3: Separate message type and arbitrary customization of model for sub-bindings

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-model
  • toBindingModel: ('model * 'subModel) -> 'bindingModel to transform the default binding model to whatever you want
  • toMsg: 'bindingMsg -> 'msg to wrap the message used in the sub-model bindings in a parent message
  • bindings: 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.

Optional and “sticky” sub-model bindings

Relevant sample: SubModelOpt - (XAML views and F# core)

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.WPF 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.

Sub-model window bindings

Relevant sample: NewWindow - (XAML views and F# core)

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.

Sub-model sequence bindings

Relevant sample: SubModelSeq - (XAML views and F# core)

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).

Other bindings

There are two special bindings not yet covered.

subModelSelectedItem

Relevant sample: SubModelSelectedItem - (XAML views and F# core)

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.WPF since the value is not your sub-model, but an instance of the Elmish.WPF view-model. To help with this, Elmish.WPF 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 corresponding subModelSeq binding
  • get: 'model -> 'id option, where you return the ID of the sub-model in the subModelSeq binding that should be selected
  • set: '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.WPF will take care of the following:

  • When the UI retrieves the selected item, Elmish.WPF gets the ID using get, looks up the correct view-model in the subModelSeq binding identified by subModelSeqBindingName, and returns that view-model to the UI.
  • When the UI sets the selected item (which it sets to an Elmish.WPF view-model), Elmish.WPF calls set with the ID of the sub-model corresponding to that view-model.

oneWaySeq

Relevant sample: OneWaySeq - (XAML views and F# core)

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 collection
  • itemEquals: 'a -> 'a -> bool, to determine whether an item has changed
  • getId: '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.WPF 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.

Lazy bindings

You may find yourself doing potentially expensive work in one-way bindings. To facilitate simple optimization in these cases, Elmish.WPF provides the bindings oneWayLazy, oneWayOptLazy, and oneWaySeqLazy, which add lazy updating and caching. These have two extra parameters: equals and map.

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). Lazy updating is evaluated based on the value from get. Only if the binding updates is the output of get passed to map, which may be expensive.

Modifying bindings

Lazy updating

For performance optimization, Binding.addLazy allows you to add an equals parameter to update the viewmodel only when necessary.

equals is used to compare the current model with the previous. If equals returns true, the rest of the update process is skipped entirely. If equals returns false, the binding is updated normally.

Elmish.WPF provides two helpers you can often use as the equals parameter: refEq and elmEq.

  • refEq is a good choice if get returns a single item (not an inline-created tuple, record, or other wrapper) from your model. It is simply an alias for LanguagePrimitives.PhysicalEquality (which is essentially Object.ReferenceEquals with better typing). Since the Elmish model is generally immutable, a reference equality check for the output of get 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 if get returns multiple items from the model wrapped inline in a tuple or record. It will compare each member of the get 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 (=).

Caching

For performance optimization, Binding.addCaching caches viewmodel values.

Cached bindings store values retrieved using get, so if the view tries to get a value that has not yet been updated, the binding will return the previous value rather than calling get again. Non-cached bindings call get every time.

Mapping bindings

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.

Example use of mapModel and mapMsg

Here is a simple example that uses these model and message types.

type ChildModel =
  { GrandChild1: GrandChild1
    GrandChild2: GrandChild2 }

type ChildMsg =
  | SetGrandChild1 of GrandChild1
  | SetGrandChild2 of GrandChild2

type ParentModel =
  { Child: ChildModel }

type ParentMsg =
  | ChildMsg of ChildMsg

It is possible to create bindings from the parent to the two grandchild fields, but there is duplicate mapping code.

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)
]

The functions mapModel and mapMsg can remove this duplication.

let childBindings () : Binding<ChildModel, ChildMsg> list = [
  "GrandChild1" |> Binding.twoWay((fun child -> child.GrandChild1), SetGrandChild1)
  "GrandChild2" |> Binding.twoWay((fun child -> child.GrandChild2), SetGrandChild2)
]

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

Benefit for design-time view models

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.

Theory behind mapModel and mapMsg

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.

Statically-Typed View Models

Inherit from ViewModelBase<'model, 'msg>

If you want full design-time support for your view models, consider defining your view models as a class rather than as a list of bindings. This will give you lots of quality-of-life improvements when working in the XAML, such as type checking for errors, auto-completion, and static types to mention in DataTemplate.DataType and similar properties for template matching.

type [<AllowNullLiteral>] CounterViewModel (args) =
  inherit ViewModelBase<Counter.Model, Counter.Msg>(args)

  new() = CounterViewModel(Counter.init |> ViewModelArgs.simple)

  member _.StepSize
    with get() = base.Get() (Binding.OneWayT.id >> Binding.addLazy (=) >> Binding.mapModel (fun m -> m.StepSize))
    and set(v) = base.Set(v) (Binding.OneWayToSourceT.id >> Binding.mapMsg Counter.Msg.SetStepSize)
  member _.CounterValue = base.Get() (Binding.OneWayT.id >> Binding.addLazy (=) >> Binding.mapModel (fun m -> m.Count))
  member _.Increment = base.Get() (Binding.CmdT.setAlways Counter.Increment)
  member _.Decrement = base.Get() (Binding.CmdT.setAlways Counter.Decrement)
  member _.Reset = base.Get() (Binding.CmdT.set Counter.canReset Counter.Reset)

Typed Bindings

When creating a list of bindings, the output type of each property must be boxed (to obj) in order to insert them into the same list. With individually declared bindings on a class member, this restriction is lifted. Therefore we have a set of bindings (denoted with the T suffix on the function or the containing module) that do not box the output type.

Typed One-way Bindings

These bindings work very similarly to their non-T counterparts, except they make exclusive use of the composable api.

  • Binding.OneWayT.id
  • Binding.OneWayToSource.id
  • Binding.CmdT.setAlways

Typed SubModel Bindings

You can create strongly-typed SubModels in much the same way as you can create normal SubModels - simply replace the Binding<'model, 'msg> list with the constructor for a type that implements ViewModelBase<'model, 'msg>(args) and takes in that args parameter, and use one of the following functions to create the binding:

  • Binding.SubModelT.req
  • Binding.SubModelSeqUnkeyedT.id
  • Binding.SubModelSeqKeyedT.id
  • Binding.SubModelWinT.id

Typed WpfProgram Bindings

Something very similar to the above transformation can also be accomplished at the top level by using one of the following T functions to create the program:

  • WpfProgram.mkSimpleT
  • WpfProgram.mkProgramT
  • WpfProgram.mkProgramWithCmdMsgT

Mixing and matching bindings

When migrating to a statically-typed view model, you can use all of the existing bindings as-is, with the exception of two-way bindings (must be split up into OneWay and OneWayToSource bindings). These will type as obj until you upgrade them to the equivalent T bindings.

When intruducing a statically-typed view model as a submodel to a binding defined by the boxed binding list, simply use one of the new Typed SubModel Bindings and then call Binding.boxT to map the output type. (e.g., "TypedSubModel" |> Binding.SubModelT.req TypedSubModel >> Binding.boxT).