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

How to open the WPF window a second time

Open ScottHutchinson opened this issue 5 years ago • 53 comments

In my application, another project calls the LoadWindow function shown below. But after the user closes the MsgTypeFiltersWindow window and attempts to open it again by calling the LoadWindow function again, I get 'System.InvalidOperationException' in PresentationFramework.dll The Application object is being shut down..

How can I write the LoadWindow function so it can be called over and over again?

Thanks

module PublicAPI =
    open NG_DART_WPF

    let LoadWindow (msgTypeID: int) (msgTypeName: string) (parentStructName: string) =
      Program.mkSimpleWpf App.init App.update App.rootBindings
      |> Program.withConsoleTrace
      |> Program.runWindowWithConfig
        { ElmConfig.Default with LogConsole = true; Measure = true }
        (MsgTypeFiltersWindow())

ScottHutchinson avatar May 02 '20 19:05 ScottHutchinson

Can you share a link to a branch that exhibits this problem?

TysonMN avatar May 02 '20 19:05 TysonMN

Haven't looked closely at this, but Program.run... is only ever intended to be run once in an app. Use the subModelWin bindings to control multiple windows.

cmeeren avatar May 02 '20 21:05 cmeeren

https://github.com/ScottHutchinson/MyExistingMFCApp

Somehow this project does not reproduce the same exception as my production application, which I cannot share with you. But it still crashes when attempting to show the window a second time.

Run the MyExistingMFCApp startup project, which will automatically open the MsgTypeFiltersWindow window. Close that window and then choose File...New to open that window again: Exception thrown at 0x769D4192 (KernelBase.dll) in MyExistingMFCApp.exe: 0xE0434352 (parameters: 0x80131509, 0x00000000, 0x00000000, 0x00000000, 0x719D0000). Unhandled exception at 0x769D4192 (KernelBase.dll) in MyExistingMFCApp.exe: 0xE0434352 (parameters: 0x80131509, 0x00000000, 0x00000000, 0x00000000, 0x719D0000).

ScottHutchinson avatar May 02 '20 22:05 ScottHutchinson

subModelWin

Understand, I have only one window to display, but the user can open and close it multiple times with different arguments each time that will be passed to the init function. And there will only ever be one instance of the window open at any time. Essentially, it is a model dialog window.

Maybe subModelWin is the only way to accomplish this, but it seems a bit complicated.

ScottHutchinson avatar May 02 '20 22:05 ScottHutchinson

Also, I'm not thrilled with having the window state in the model like this: https://github.com/elmish/Elmish.WPF/blob/d6aa789806669a8a3735b9205351b3b5a7cb2255/src/Samples/NewWindow/Program.fs#L22

ScottHutchinson avatar May 02 '20 22:05 ScottHutchinson

It seems like maybe we need another function like Program.runWindowWithConfig that just closes the window instead of the application. Or a parameter that changes its behavior like that.

ScottHutchinson avatar May 02 '20 22:05 ScottHutchinson

https://github.com/ScottHutchinson/MyExistingMFCApp ... Run the MyExistingMFCApp startup project...

This is a C++ project, right? Is C++ necessary to reproduce the issue you are facing?

TysonMN avatar May 02 '20 23:05 TysonMN

I think C++ has nothing to do with it. But that is the context in which I want to show a WPF window as a dialog. And I'm still trying to find the best way to do that. I need to be able to call the ShowDialog again after the user closes the dialog window. Maybe I need to use the SubModelWin combined with the SubModelSeq, and I don't know if that has been done before, or if it's even possible. I don't think the NewWindow sample fits my use case very well.

ScottHutchinson avatar May 02 '20 23:05 ScottHutchinson

Quoting from https://github.com/elmish/Elmish.WPF/issues/211#issue-611266584

This might be related to Issue #210.

Indeed. Issue #211 seems easier to me right now. I suggest we resolve that issue first and then reconsider this one.

TysonMN avatar May 03 '20 00:05 TysonMN

I'm finding it difficult to adapt the NewWindow sample to my use case, because each time the new window (modal dialog) is displayed, I need it to call the init function with parameters to initialize the model for a tree view and other controls in that dialog window. But in that sample, the init function is called only for the main window. The SubModelSeq sample works well for my use case, but only if the user never shows the window a second time, which is definitely not good enough.

ScottHutchinson avatar May 05 '20 18:05 ScottHutchinson

I wrote this function, but it doesn't solve the problem mentioned above. The App.window is just a new Window, which will always remain hidden.

    let InitializeWpfApplication () =
        if isNull Application.Current then
            Application () |> ignore
            Application.Current.MainWindow <- App.window

        let init = App.init 0 "" "" measureElapsedTime
        Program.mkSimpleWpf init App.update App.rootBindings
        |> Program.startElmishLoop ElmConfig.Default App.window
        (* Run without showing the main window, which is not needed
           since the user will open a new dialog window by clicking
           in an existing C++ MFC window 
           (in other apps, it could be an existing C# WPF window).
        *)
        Application.Current.Run App.window

ScottHutchinson avatar May 05 '20 18:05 ScottHutchinson

Hmmm...Maybe the simplest solution would be to just call Application.Current.Shutdown() after the user closes the dialog. Then it would just start fresh again the next time the Elmish.WPF.Program is started. If that's possible, then it should work for my use case, because I have only the one WPF window. Actually, it seems like it would work in the more general case where only one WPF window is needed at a time.

EDIT: No, that didn't help. Maybe instead I could start the Elmish.WPF Application in its own AppDomain like this example: [Running multiple WPF applications in the same process using AppDomains] (https://eprystupa.wordpress.com/2008/07/31/running-multiple-wpf-applications-in-the-same-process-using-appdomains/). Not sure about .NET Core though.

ScottHutchinson avatar May 05 '20 18:05 ScottHutchinson

I might need to pass in an Application object to Elmish.WPF, so I can control its lifespan and/or AppDomain.

ScottHutchinson avatar May 05 '20 19:05 ScottHutchinson

I don't really understand; AFAIK Application is singleton, and instantiated only once for the whole AppDomain, when your application starts. Is this different in C++?

In any case, I think there are good Elmish-centric ways to achieve the behaviour you need, but I'm afraid I don't have the capacity to look into it now. In short, if you use Elmish.WPF for the whole app, then it shouldn't be a problem keeping a list of Elmish.WPF window states (or your preferred domain proxy) in the main model, and initializing these models however you want in the update function when it receives a message indicating that a new window should be opened.

cmeeren avatar May 05 '20 19:05 cmeeren

Is there a way to trigger an update by dispatching a message in code? Instead of binding to a WPF button, I just want the code to do it directly. Thanks

ScottHutchinson avatar May 06 '20 14:05 ScottHutchinson

You can use commands/subscriptions for this. See this section in the tutorial.

cmeeren avatar May 06 '20 14:05 cmeeren

One way to access the dispatcher is via a subscription. Here is one place in the samples where this is done. https://github.com/elmish/Elmish.WPF/blob/4452a1d2e69df377b1e9e4e72f0d6293f56c6676/src/Samples/SubModel/Program.fs#L146

TysonMN avatar May 06 '20 14:05 TysonMN

Thanks. I just found that. Also, this might be more direct: Cmd.OfFunc.result.

ScottHutchinson avatar May 06 '20 14:05 ScottHutchinson

I still can't figure out how to trigger an update using Cmd.OfFunc.result or Cmd.OfMsg. I ran the line below, but it did not trigger the ShowDialog case in my update function. What am I missing?

Cmd.OfFunc.result (App.ShowDialog (msgTypeID, msgTypeName, parentStructName)) |> ignore

ScottHutchinson avatar May 06 '20 15:05 ScottHutchinson

Trying to use subModelWin like below, but I'm stuck on how to get the msgTypeID, msgTypeName, parentStructName arguments to initialize the model. So now I'm going to try the obsolete showWindow function (or a variation of that) instead, but will probably still get stuck on how to get Cmd.OfFunc.result to work. EDIT: Maybe the best way is to bind the ShowDialog message to the dialog window's Activated event (and make the msgTypeID, msgTypeName, parentStructName arguments public members of the MsgTypeFiltersWindow).

    let bindings () : Binding<Model, Msg> list = [
        "Dialog" |> Binding.subModelWin(
            (fun m -> m.WinState), fst, id,
            rootBindings,
            (fun m dispatch -> 
                dispatch (ShowDialog (msgTypeID, msgTypeName, parentStructName))
                MsgTypeFiltersWindow(Owner = Application.Current.MainWindow)
            ),
            onCloseRequested = CloseDialog,
            isModal = true
        )
    ]

ScottHutchinson avatar May 06 '20 16:05 ScottHutchinson

EDIT: I think this is working. EDIT2: Sort of. Unfortunately, the Program.runWindowWithConfig call is blocking, so nothing happens until the user closes the main window. That could be a show stopper. And even if I get past that issue, I still need to figure out how to trigger the Binding.subModelWin binding without binding it to a button command.

    type DialogOpeningEventArgs = {
        MsgTypeID: int
        MsgTypeName: string
        ParentStructName: string
    }

    let dialogOpening = new Event<DialogOpeningEventArgs>()
    let raiseDialogOpening (args : DialogOpeningEventArgs) = dialogOpening.Trigger(args)
    let DialogOpening = dialogOpening.Publish
    let dialogOpeningSubscriber dispatch =
        DialogOpening.Add (fun args ->
            dispatch (ShowDialog (args.MsgTypeID, args.MsgTypeName, args.ParentStructName))
        )
...
            |> Program.withSubscription (fun _ -> Cmd.ofSub App.dialogOpeningSubscriber)
            |> Program.runWindowWithConfig...
...
        App.raiseDialogOpening { MsgTypeID = msgTypeID; MsgTypeName = msgTypeName; ParentStructName = parentStructName}

ScottHutchinson avatar May 06 '20 18:05 ScottHutchinson

@ScottHutchinson, it seems you have some misconceptions regarding how the Elm architecture works.

I still can't figure out how to trigger an update using Cmd.OfFunc.result or Cmd.OfMsg. I ran the line below, but it did not trigger the ShowDialog case in my update function. What am I missing?

Cmd.OfFunc.result (App.ShowDialog (msgTypeID, msgTypeName, parentStructName)) |> ignore

This just creates a command; it does not execute it (which is done by the Elmish update loop). In order to dispatch messages in code, you need to set up a subscription. I think there is a sample that does this. You can use Program.withSubscription, or have the init function return both the model and a Cmd.

If you haven't already, I highly recommend you read the first parts of the Elmish.WPF tutorial, which explains the basics of the Elm architecture concepts. 🙂

And again: Program.runWindowWithConfig is only intended to be run once for an entire app, at the entry point of the app. It should not be used when opening dialogs or new windows. If using Elmish.WPF, that should be handled by the subModelWin binding.

cmeeren avatar May 06 '20 19:05 cmeeren

I have read all of your excellent tutorial, yet I still struggle with this use case. And I am trying everything you are saying to do, but still failing. If you read my previous post again, you'll see that am trying to do as you say.

ScottHutchinson avatar May 06 '20 20:05 ScottHutchinson

Not to worry. If you can explain in very simple terms the high-level functionality you are trying to accomplish, I may be able to create a sample that demonstrates how to do it. (The sample will use Elmish.WPF for the whole app and be in F# – if that should not work for your use-case, then I'm not sure Elmish.WPF is right for your use-case.)

cmeeren avatar May 06 '20 20:05 cmeeren

I really want Elmish.WPF to work for this. It just seems like too simple a problem to give up on it.

I think there is not a single sample of init returning a command (using Program.mkProgramWpf), so it's difficult for me to understand how that would work and whether it would help in my case.

ScottHutchinson avatar May 06 '20 20:05 ScottHutchinson

Instead of init returning a command, you can use Program.withSubscription in the chain before Program.run.... I think the end result is exactly the same.

In any case, as I said above:

If you can explain in very simple terms the high-level functionality you are trying to accomplish, I may be able to create a sample that demonstrates how to do it.

cmeeren avatar May 06 '20 20:05 cmeeren

I want to show a modal dialog, with data initialized based on arguments. I want the user to be able to show that dialog over and over again, each time with different arguments. But I don't want the user to have to click a button on the main window to show the dialog. I want to show it from code. I don't really want the main window to ever be visible. The main window can just be an empty window.

EDIT: Hang on. Maybe I should just be using the main window as the dialog (which is what I started out doing). I'll try that again with what I've learned today, maybe I can get it to work.

ScottHutchinson avatar May 06 '20 20:05 ScottHutchinson

Thanks. I assume only one such dialog can be open simultaneously? What is it that causes it to be shown?

cmeeren avatar May 06 '20 20:05 cmeeren

Yes, only one dialog at time, since it is modal. A function call causes it to be shown. If possible, I can just use the main window as the dialog, but I need to be able to hide it or close it between uses.

ScottHutchinson avatar May 06 '20 20:05 ScottHutchinson

What causes this triggering function to be called? (Just trying to understand the use-case better.)

cmeeren avatar May 06 '20 20:05 cmeeren