Elmish.WPF icon indicating copy to clipboard operation
Elmish.WPF copied to clipboard

Use the samples' project structure for the Getting Started instructions?

Open cmeeren opened this issue 4 years ago • 2 comments

The current "Getting started" section in the readme presents itself as a summary of the SingleCounter sample, but the instructions demonstrate a different project structure. Specifically, the F# project is the startup project, whereas in all our samples, the C# project has been the startup project.

I think we should change the "Getting started" section to reflect the current sample project structure, which IIRC is in many ways better than the opposite.

@TysonMN what do you think?

cmeeren avatar Nov 29 '20 13:11 cmeeren

@cmeeren For simple projects, the Views in the C# project could be used as the startup project. However, in my case, where I have an adorner in C# being called by the F# project, using the C# project as startup would result in a circular reference. So it seems that I, at least, am forced to use the F# project as startup -- so I appreciate the instructions as is. (Unless you can tell me another way of doing it).

awaynemd avatar Dec 10 '20 19:12 awaynemd

...in my case, where I have an adorner in C# being called by the F# project, using the C# project as startup would result in a circular reference. So it seems that I, at least, am forced to use the F# project as startup -- so I appreciate the instructions as is. (Unless you can tell me another way of doing it).

You are not forced to have an F# project as the startup project. You can definitely add a layer of indirection. I have done this in my application at work.

Here is a simplified version of...

...my ProgramFSharp.fs file...

type IWpfData =
  abstract member InitializeWpf : Unit -> Unit

  abstract member MainWindow : System.Windows.Window
  abstract member SkElement : SkiaSharp.Views.WPF.SKElement

  abstract member EnqueueSnackbarMessage : string -> unit


let main (wpfData: IWpfData) =
  let config = ...
  let initialCmds = ...

  wpfData.InitializeWpf ()

  let model = App.init config
  let init = fun () -> model, initialCmds
  let update = (App.update, App.generate) |> Func2.product2
  let bindings = ViewModel.App.bindings model wpfData.SkElement
  let bindCmd = Impure.App.bindCmd wpfData.SkElement.InvalidateVisual wpfData.EnqueueSnackbarMessage
  WpfProgram.mkProgramWithCmdMsg init update bindings bindCmd
  |> WpfProgram.runWindow
    wpfData.MainWindow

...and my ProgramCSharp.cs file

public class WpfData : ProgramFSharp.IWpfData {
  private MainWindow _mainWindow;

  public void InitializeWpf() {
    var mainWindow = new MainWindow();
    _mainWindow = mainWindow;
    InitializeApplication(mainWindow);
  }

  private static void InitializeApplication(MainWindow mainWindow) =>
    _ = new Application() {
      MainWindow = mainWindow,
      Resources = new DefaultResourceDicitionaryContainer().Resources
    };

  public Window MainWindow => _mainWindow;
  public SKElement SkElement => DrawScreenBody.SkElementStatic;

  public async void EnqueueSnackbarMessage(string message) {
    try {
      await _mainWindow.SnackbarLoadedCompletion;
      await _mainWindow.SnackbarVisibleCompletion;
      await _mainWindow.WindowStateNotMinimizedCompletion;

      _mainWindow.Dispatcher.Invoke(() => _mainWindow.Snackbar.MessageQueue.Enqueue(message));
    } catch (Exception e) {
      Log.Error(e, "An exception was thrown while enqueuing a massage to the snackbar");
    }
  }
}

public static class ProgramCSharp {
  [STAThread] // Assigns to UI components a unique thread
  public static int Main() => ProgramFSharp.main(new WpfData());
}

To make for a simpler example, I removed configuration, logging, initial command creation, and a comment from the F# code and lengthy comments (explaining "why") in the C# code.

TysonMN avatar Dec 10 '20 20:12 TysonMN