A dynamic Elmish UI library for WPF
See also Elmish.WPF for a robust “half-elmish” solution with static XAML views.
Elmish.WPF.Dynamic is a library that allows you to write dynamic WPF views using an Elmish architecture.
It is superficially similar to Fabulous in using a generator and config file. However, the view syntax is different, and under the hood, the “shadow DOM” is more strongly typed in Elmish.WPF.Dynamic.
Check out the TestApp project in this repo for a complete working app. Some familiarity with Elmish is assumed.
The view syntax is based on mutable properties. Note that this is just a syntax for creating shadow DOM elements; it has no impact on the immutability of your model.
For example, using normal property initialization syntax:
TextBlock(
Text = model.MyText,
IsHyphenationEnabled = true
)There are also constructor overloads taking an initializer function. This provides better discoverability since you can “dot into” the types to discover the available properties. It also allows you to factor out common setters.
TextBlock(fun tb ->
setCommonTextBlockProps tb
tb.Text <- model.MyText
tb.IsHyphenationEnabled <- true
)You can even mix and match:
TextBlock((fun tb ->
setCommonTextBlockProps tb
tb.Text <- model.MyText),
IsHyphenationEnabled = true
)Or:
TextBlock(
setCommonTextBlockProps,
Text = model.MyText,
IsHyphenationEnabled = true
)The view function must either return a single Window (and the program started with Program.runWindow) or a list of Windows (and the program started with Program.runWindows).
As a general rule, every relevant WPF DependencyObject has a corresponding shadow DOM type (ultimately deriving from a based type currently called Node, like Fabulous’ ViewElement), and all relevant properties on each control have a corresponding property on the shadow DOM type. The shadow DOM type hierarchy mirrors the hierarchy of the WPF controls.
Attached props are defined as extension properties on the relevant types. For example, DockPanel.Dock is defined as an extension property DockPanel_Dock on UIElement.
In addition, the following special props exist:
Keyon all elements – used for reconciling collections intelligently, similar to ReactRefon all elements – used withViewRef<_>to get access to the raw WPF controls/viewsErroronFrameworkElement– used to set a validation error for the elementInitialViewfor setting the initial raw WPF view, useful for interoping with 3rd party resources (which may e.g. be obtained usinggetResource, see the sample app)
This is a fairly usable proof of concept. If the current limitations are not important to you, I see nothing wrong with creating apps with it. (There may of course be bugs, as always.)
Some important and helpful batteries are included, such as lazyWith for memoizing views, elmEq and refEq as sane defaults for lazyWith, getResource to interop with application resources, and a TextChangedEventArgs.Text extension property to easily retrieve the current text of a TextBox when changed. The generator should also work for 3rd party controls and attached props, though that is currently untested. However, there are at least a few limitations which may or may not be significant for you. They are described below.
I have no immediate plans to continue working on this, and I can’t promise I’ll get back to it in the future. Anyone is welcome to drive the project forward, whether by taking over this repo, creating a fork, creating your own project inspired by this, or submitting PRs. If you are interested in doing significant work, please take over the project instead of submitting PRs.
For me, this project is purely based on personal interest, and at the moment I’m more interested in creating apps with Fable.React and Electron due to the battle-tested nature of React and Electron and the synergy of React with an Elmish architecture. I am also thinking that perhaps static XAML views with Elmish.WPF synergizes better with WPF than dynamic views, since WPF is heavily based on bindings, templates, etc. While static views are far from as composable as dynamic views, Elmish.WPF puts all the power of WPF at your fingertips, while still allowing most of the goodness of an Elmish architecture.
In short, it seems that some WPF functionality that is desirable also in an Elmish architecture, such as virtualization and key bindings, depend on functionality that should generally not be available in an Elmish architecture, such as templates/bindings and ICommands. While solutions can be found, it’s certainly not trivial.
The most notable current limitations are:
- Templates and bindings are excluded from the generated code, which means that the following that depend on them are not available:
- Virtualization
DataGridListView
- Everything related to commands is excluded, which includes
KeyBindingandMouseBinding. This may make it harder to create keyboard shortcuts etc.
- Get virtualization to work. For inspiration, see fsprojects/Fabulous#455. Must probably change from
ItemsControl.ItemstoItemsControl.ItemsSource, useObservableCollectionand templates, and create some helper types. - Determine if some functionality is only available with commands, and if so, add them and/or create wrappers/helpers to allow idiomatic Elmish usage
- Determine whether
*Selectorprops are needed (e.g.ComboBox.GroupStyleSelector) - Determine what to do with types that implement
ISupportInitialize(and similar patterns, such as theBeginInitandEndInitmethods ofBitmapSource) - Determine what to do with properties that reference other views. Are they necessary, and if so, how should they be used in an Elmish architecture?
Label.TargetTooltip.PlacementTargetContextMenu.PlacementTargetContextMenu.CustomPopupPlacementCallbackStackPanel.ScrollOwnerVirtualizingStackPanel.ScrollOwnerPopup.PlacementTargetPopup.CustomPopupPlacementCallbackContextMenuService_PlacementTargetFocusManager_FocusedElementStoryboard_Target
- Animation – currently untested. Does it work as-is, or should usage be improved? How?
StoryboardStoryboard_TargetPropertyVisualStateand related stuff
- Test
FreezableCollection<'T>(e.g.ThumbButtonInfoCollection) since it’s the only generic shadow DOM type
ContainerVisual.Childrenis of typeVisualCollection, which doesn't implementIListor another modifiable collection interface, so it isn't picked up by the generator. Must be used as-is (asVisualCollection). Find a way to get the collection update helper to deal with this. (That is, ifContainerVisualand the derivingDrawingVisualis at all necessary to include.)- Currently, abstract real types become abstract generated shadow DOM types (by design). However, the “abstract” shadow DOM types should probably be instantiable in order to make it easier to use
InitialView. For example, if setting aBrushresource (Brushis abstract) usingInitialView, one must currently know whether the resource is aSolidColorBrushetc. and instantiate the correct shadow DOM type. It would be great to be able to simply instantiate a shadow DOMBrushand pass it the realBrush. To accomplish this, abstract real types can probably be modelled as non-[<Abstract>]shadow DOM types with only one constructor taking theInitialElement. - Some “official” WPF controls are in assemblies that are not yet included in the generator config. Include them and verify all generated types and props:
Ribbonand related stuff inSystem.Windows.Controls.Ribbon.dll
- Currently untested. Test with e.g. MaterialDesignInXamlToolkit, which has both custom controls and attached props (and is already used in the sample app for styling).
- Check out Fabulous’
fixfunction – it it useful here?
- Exclude all
Sibling*properties if they’re not needed:Block.SiblingBlocksInline.SiblingInlinesListItem.SiblingListItems
- Exclude all
*StringFormatproperties if they’re not needed:ContentControl.ContentStringFormatItemsControl.ItemStringFormat
- Should there be separate update helpers depending on whether we know the element to be updated to be a shadow DOM element or not? Would avoid some boxing and type checking, but unsure if it’s worth it.
- Don’t use a type provider – define the schema as types instead, document the properties, and use e.g. Newtonsoft.Json.
- Support regex matching on exclusions for convenience?
- Use
globallyIgnoredTypesonly for excluding shadow DOM types? Currently properties with matching types are also excluded. Separating these concerns is necessary if we need to exclude a shadow DOM type while still generating a shadow DOM property that has that type.
This section is mostly relevant for anyone wanting to take over.
Note that due to the WIP/POC nature of this project, any file/module/type/variable names may or may not be well thought through or accurately reflect the current state of the items they describe. Most names should be OK, but please rename to your liking.
The solution contains three projects:
Elmish.WPF.Dynamicis what would be published to NuGet. It contains all helpers and all generated WPF controls.Elmish.WPF.Dynamic.Generatoris a console app used to generate the code. It contains a fairly well-documented domain model. Currently, it only uses theWpfCore.jsonconfig file when run, but supporting command-line arguments is just a tiny change inMain.fs.Elmish.WPF.TestAppis an example app which has served testing purposes while developing and may or may not make much sense or look very pretty.
Some notes about the generated shadow DOM types:
- All shadow DOM types derive from
NodeinHelpers.fs Nodeitself mostly handles logic for validation error and attached props- The generated types are checked in so that it’s easy to inspect any changes
Please ask if you have more questions about the code.