Elmish.wpf: Recommended solution structure

Created on 9 Aug 2019  Â·  14Comments  Â·  Source: elmish/Elmish.WPF

I'd like for the readme and samples to contain a "proper" recommended project structure for non-trivial apps. I've given some thought to this, but I'd like some second opinions.

The forces I've identified are:

  • Use C# where the tooling is obviously better (e.g. for the XAML stuff due to the XAML designer)
  • Otherwise use F# as much as possible, including helpers used by XAML (e.g. IValueConverters, which might have to convert to/from F#-specific stuff)
  • Must be able to use design-time VMs. In other words, the C# XAML project must reference the project with the bindings (directly or transitively)
  • Separation of concerns and reuse of app code. The bindings should be in a separate project from the model/update code, so that the core logic does not reference any UI stuff and can, if desired, be re-used for other Elmish-based UIs

This leads me to the following three-project structure, each referencing the previous project.

  • Core project, F#, contains domain and model/update
  • Platform-specific (WPF) project, F#, contains bindings and other non-XAML platform-specific stuff (IValueConverters, design-time VMs, possibly controls, though F#'s lack of public static fields for dependency props means that controls will often have to be implemented in C#) as well as the entry point logic (a run: Window -> int function calling Program.mk... and Program.run...), though not the actual entry point
  • XAML project, C#, contains all XAML (including App.xaml), as well as the entry point. The entry point must be a normal console app entry point, not App.xaml, because App will be instantiated during Program.run... (or before, app-wide resources are needed for the window). For that reason, the entry point logic (Program.run...) can not be called in the App constructor.

A slight variation is to have the middle project only contain XAML helpers (not entry point logic), remove the entry point from the XAML project (but keep App.xaml), and have another F# project at the very top which contains the actual entry point. I don't really see any significant benefits to this, though.

opinions-wanted

All 14 comments

I'm unsure if this helps, but I have my (massive) Elmish.WPF project structured in the following way:

  • C# project containing views, including shared controls (MyProject.Views)
  • F# project containing UI/uncommon processing logic for WPF windows (MyProject)
  • F# project containing shared/common processing logic, including the logic for the shared controls (init, update, bindings, etc.) (MyProject.Core)

Given the unique nature of how Elmish.WPF interacts with separate submodels, I am unsure how well it would play with reuse across other elmish-based UIs. Though, that could also be my lack of experience with elmish outside of Elmish.WPF showing.

Thanks for your input!

I'm unsure if this helps, but I have my (massive) Elmish.WPF project structured in the following way:

Seems like it's more or less what I suggested, except that you have the bindings in the core project. Is that correct?

Given the unique nature of how Elmish.WPF interacts with separate submodels

AFAIK the current Binding.subModel, Binding.subModelOpt, and Binding.subModelSeq implementations are flexible enough that they do not place any view-specific constraints on your model/update. Please let me know if this is not correct!

I want as few projects as possible, so in principle I manage with only one C# and one F# project for the application. I want C#/XAML to reference F# models, so that I can easily design with F# data. I also want the C# project to be the application project, because it's so much easier with all the support VS has for the project settings and stuff, and XAML in the application project as opposed to in a library. There are issues I haven't solved with an F# application project using C#/XAML libraries. I used FsXaml for a few years, but bumped into issues that rule it out. To sum up, I've ended with well proven ways of doing things, while still using F# 99.99%.

Thanks! As I understand it, what you propose is basically what I proposed but where the two F# projects are merged into one. Is that correct?


General note: A potential weakness of the structure I proposed, is that you can't instantiate windows, as needed by the new Binding.subModelWin bindings, unless you do some kind of dependency injection (e.g. let the bindings function accept dependencies for constructing the necessary windows).

An alternative way to solve it using project structure, is to have (in order from lowest to highest) an F# core project, an F# WPF XAML-helper project (value converters, design-time view models, etc. - could be merged into the core project depending on the desired level of separation), the C# XAML project with the views, and an F# project with the bindings and entry point. The bindings can then directly refer to the necessary views.

Yes, that's correct. Except "propose" is perhaps a strong word, but it certainly looks like a good alternative to me. But then again I don't understand fully which considerations has to be taken into account.

I don't have all the answers either, which is why I made this issue in the first place. :) I appreciate your feedback!

Since I am new at this, I have decided to keep things simple for me and have the entry point be in F# (since that is what all of the samples do, and I understand how the samples work).

I have previous functional programming experience in C# (via Language.Ext), but I have only recently started my first F# project. Since F# is only functional first (instead of being purely functional like Elm), I want to try having separate pure and impure core projects to help remind me and my team focus on writing mostly pure code.

This gives me five projects:

  1. Prefix.Elmish.WPF.EntryPoint (F#, entry point, calls Program.runWindow)
  2. Prefix.WPF.Views (C#, contains just XAML files)
  3. Prefix.Elmish.WPF.ViewModeling (F#, contains App.bindings function and DesignTime.viewModel property)
  4. Prefix.Core.Impure (F#, contains impure code used to create commands and subscriptions including the App.toCmd function)
  5. Prefix.Core.Pure (F#, contains App.init and App.update functions as well as the CmdMsg type for the command message pattern, contains only pure code)

A corollary of this is that I can't exactly copy the examples from @MangelMaxime's blog post titled "My tips for working with Elmish" because some of his update functions are impure.

(Updated to include how the command message patterns fits in and put Prefix.Core.Pure at the bottom)

Thanks!

Tip: To keep things pure and switch around 4 and 5 (i.e., pure at the bottom, impure depends on pure), try the CmdMsg pattern. See the readme for details and links to further reading.

Yes, I read that. I definitely like the idea of the CmdMsg pattern. I intend to use it. Thanks for the reminder :)

I updated my original comment to show how I intend to organize the parts of the command message pattern

Great! But can't you now switch 4 and 5, so that you have the pure project at the bottom (no references), and the impure one above that (referencing the pure project)? It's generally a good practice in F# to have all your pure code first, and then the impure code after that (whether in the file order or in another project). That enhances the focus on writing pure code.

Since the App.update function only returns CmdMsg, and the conversion is done in the entry point project using a suitable Program.mk... function, you can place the actual CmdMsg -> Cmd<Msg> converter function anywhere, such as in the impure project (above the pure one).

But can't you now switch 4 and 5, so that you have the pure project at the bottom (no references), and the impure one above that (referencing the pure project)? It's generally a good practice in F# to have all your pure code first, and then the impure code after that (whether in the file order or in another project). That enhances the focus on writing pure code.

Oh, sure. Done.

Of course F# forces us to order our files from top-to-bottom by "no references" to "any references". I never considered organizing the projects in essentially the same way. Thanks for the suggestion :)

you can place the actual CmdMsg -> Cmd converter function anywhere, such as in the impure project (above the pure one).

Yep, that is my plan. I tried to express that I would put it in Prefix.Core.Impure I said "the App.toCmd function".

How do you organize projects in a solution? I think they are organized alphabetically (by default). Do you prefix the project name with a appropriate numbers? That seems to be the hack I see people suggesting online.

Personally I don't care about the visual order of the projects, I gave up that a long time ago ¯\_(ツ)_/¯

Was this page helpful?
0 / 5 - 0 ratings

Related issues

TysonMN picture TysonMN  Â·  10Comments

cmeeren picture cmeeren  Â·  6Comments

TysonMN picture TysonMN  Â·  7Comments

KingKnecht picture KingKnecht  Â·  6Comments

ArthurFSharp picture ArthurFSharp  Â·  12Comments