Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to open the WPF window a second time #210

Open
ScottHutchinson opened this issue May 2, 2020 · 53 comments
Open

How to open the WPF window a second time #210

ScottHutchinson opened this issue May 2, 2020 · 53 comments

Comments

@ScottHutchinson
Copy link
Contributor

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())
@TysonMN
Copy link
Member

TysonMN commented May 2, 2020

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

@cmeeren
Copy link
Member

cmeeren commented May 2, 2020

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.

@ScottHutchinson
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

Also, I'm not thrilled with having the window state in the model like this:

{ Win1State: WindowState<string>

@ScottHutchinson
Copy link
Contributor Author

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.

@TysonMN
Copy link
Member

TysonMN commented May 2, 2020

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?

@ScottHutchinson
Copy link
Contributor Author

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.

@TysonMN
Copy link
Member

TysonMN commented May 3, 2020

Quoting from #211 (comment)

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.

@ScottHutchinson
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Contributor Author

ScottHutchinson commented May 5, 2020

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
Copy link
Contributor Author

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

@cmeeren
Copy link
Member

cmeeren commented May 5, 2020

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.

@ScottHutchinson
Copy link
Contributor Author

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

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

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

@TysonMN
Copy link
Member

TysonMN commented May 6, 2020

One way to access the dispatcher is via a subscription. Here is one place in the samples where this is done.

|> Program.withSubscription (fun _ -> Cmd.ofSub timerTick)

@ScottHutchinson
Copy link
Contributor Author

ScottHutchinson commented May 6, 2020

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

@ScottHutchinson
Copy link
Contributor Author

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
Copy link
Contributor Author

ScottHutchinson commented May 6, 2020

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
Copy link
Contributor Author

ScottHutchinson commented May 6, 2020

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}

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

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

@ScottHutchinson
Copy link
Contributor Author

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.

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

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

@ScottHutchinson
Copy link
Contributor Author

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.

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

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.

@ScottHutchinson
Copy link
Contributor Author

ScottHutchinson commented May 6, 2020

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.

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

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

@ScottHutchinson
Copy link
Contributor Author

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.

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

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

@TysonMN
Copy link
Member

TysonMN commented May 6, 2020

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.

They are.

@ScottHutchinson
Copy link
Contributor Author

The function is called when the user clicks on an item in a C++ MFC window. It is quite a complicated window with several tabs and a menu that I do not want to re-implement in WPF just so I can show one new dialog.

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

@bender2k14 For reference, do you have a source for that? I remember an issue a while back, probably in the main Elmish repo, where this was discussed and where there may have been ever so slightly different (but that may have been a bug that is now fixed). At least it was not trivially true, as I remember it.

@ScottHutchinson
Copy link
Contributor Author

elmish/elmish#183 (comment)

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

@ScottHutchinson Thanks! So this is where I fall short. I have never used C++ nor MFC, and I have no idea how C++/MFC interfaces/interops with .NET/WPF, where the WPF Application lifecycle is controlled, etc. In short, Elmish.WPF is only intended to run a single WPF app. Is there anything relevant you can tell me about how this works?

@ScottHutchinson
Copy link
Contributor Author

I think maybe you should ignore the C++ aspect. Just focus on the idea of an F# function with parameters that shows an Elmish.WPF dialog window with data initialized based on those parameters.

@ScottHutchinson
Copy link
Contributor Author

The function currently looks like below, but it only opens an empty main window and then triggers a ShowDialog case in the update function after the main window closes. Calling the function a second time triggers that case again. I'm still working out how to proceed next.

module PublicAPI =
    open NG_DART_WPF

    let mutable isInitialized = false

    let LoadWindow (msgTypeID: int) (msgTypeName: string) (parentStructName: string) =
        if not isInitialized then
            isInitialized <- true
            let init = App.init msgTypeID msgTypeName parentStructName
            Program.mkSimpleWpf init App.update App.bindings
            |> Program.withSubscription (fun _ -> 
                Cmd.ofSub App.dialogOpeningSubscriber
            )
            |> Program.runWindowWithConfig
                ElmConfig.Default
                (Window())
            |> ignore<int>

        App.raiseDialogOpening { MsgTypeID = msgTypeID; MsgTypeName = msgTypeName; ParentStructName = parentStructName}

@cmeeren
Copy link
Member

cmeeren commented May 6, 2020

It's getting late here but I'll try to have a look at it tomorrow, if no-one else does so in the meantime.

In short, the main "error" above is calling Program... outside of the application's entry point. As previously mentioned, this should only be called in the WPF app's entry point (just like the plain WPF Application.RunWindow). So you first need to start a normal, long-running WPF (and Elmish.WPF) app (like the samples show), with a hidden main window if you want, and then exclusively use the Elmish model/update/message stuff to hide/show windows and store/update the necessary state for those windows. When you start the app, as you do above, you can use Program.withSubscription to set up "external triggers" as it were, but again, this is only done once, on app startup.

@ScottHutchinson
Copy link
Contributor Author

I think maybe this is the WPF application's entry point in my case. I could call it in a separate function, but I'm not sure the result will be any different. The isInitialized value ensures that it is called only once.

@ScottHutchinson
Copy link
Contributor Author

I tried adding an implicit entry point as shown below, but it blocked the initialization of the MFC application until I closed the WPF window, so I'm thinking there are only two ways I would be able to use Elmish.WPF: (1) Launch a new WPF application in a new process or AppDomain; or (2) if Elmish.WPF could be modified to support running in the context of window.ShowDialog(), which can be called over and over again without ever calling the blocking Application.Run function.

module Main

open System
open System.Windows

[<STAThread()>]
do 
    let win = Window (WindowState = WindowState.Minimized)
    //win.DataContext <- vm :> obj
    let app = new Application() in
    app.Run(win) |> ignore

let init = (do ()); true // from https://stackoverflow.com/a/18619285/5652483

@cmeeren
Copy link
Member

cmeeren commented May 7, 2020

I see, thanks for trying that out.

Elmish.WPF is, from the ground up, intended to "be the whole app", as it were. If anyone wants to have a look at which changes would be needed to support running Elmish.WPF separately for separate windows, feel free, but that would also require someone to be willing to help maintain any increased complexity in Elmish.WPF going forward, because I'm not that keen on implementing and supporting it. (Since this is a hobby project, I need to prioritize my resources.)

@TysonMN
Copy link
Member

TysonMN commented May 7, 2020

@bender2k14 For reference, do you have a source for that? I remember an issue a while back, probably in the main Elmish repo, where this was discussed...

elmish/elmish#183 (comment)

Yep.

@TysonMN
Copy link
Member

TysonMN commented May 7, 2020

If anyone wants to have a look at which changes would be needed to support running Elmish.WPF separately for separate windows, feel free, but that would also require someone to be willing to help maintain any increased complexity in Elmish.WPF going forward, because I'm not that keen on implementing and supporting it.

I am willing to consider this. I just haven't had enough time yet to try things. My plan is to focus on #211 first. I might end up implementing several different approaches to help us focus on the implementation details.

@ScottHutchinson
Copy link
Contributor Author

I'm going to try to implement a new Program.showDialogWithConfig function. If I can make it work, then I'll submit a pull request. I'm hoping the changes required will be minimal, but we'll see. I'm willing to help maintain it. Of course I welcome any help from @bender2k14. I don't find any guidelines for contributors, so let me know if you'd like me to fork or branch, etc in a particular way.

@cmeeren
Copy link
Member

cmeeren commented May 7, 2020

Contributor guidelines are here: https://github.com/elmish/Elmish.WPF/blob/master/.github/CONTRIBUTING.md (they should show up when you create an issue/PR)

@ScottHutchinson
Copy link
Contributor Author

Thanks. I should have searched.

@ScottHutchinson
Copy link
Contributor Author

This is looking like it might require nothing more than adding the function below. I'll continue implementing the features of my dialog using that function, but so far it is working perfectly. If it works, then I'll write a sample before submitting a pull request.

/// Starts the Elmish and WPF dispatch loops with the specified configuration.
/// Will show the specified window as a dialog, returning the dialog result.
/// This is a blocking function.
let showDialogWithConfig config (window: Window) program =
  startElmishLoop config window program
  window.ShowDialog ()

@TysonMN
Copy link
Member

TysonMN commented May 8, 2020

Is window.ShowDialog () actually blocking?

@ScottHutchinson
Copy link
Contributor Author

I would say so.
From https://docs.microsoft.com/en-us/dotnet/api/system.windows.window.showdialog?view=netframework-4.6.1
"Opens a window and returns only when the newly opened window is closed."
...
"ShowDialog shows the window, disables all other windows in the application, and returns only when the window is closed. This type of window is known as a modal window."

@cmeeren
Copy link
Member

cmeeren commented May 8, 2020

I'm happy the fix seems so simple. 🙂

If it works, then I'll write a sample before submitting a pull request.

Thanks! As I understand it, this sample should not use Elmish for the main application, only for the dialog. That way we demonstrate how to use Elmish.WPF only for dialogs in an existing non-Elmish app. Do you agree?

Also, we might consider not adding a showDialogWithConfig but a showCustomWithConfig where the user simply supplies a unit -> unit that is run (which can be window.ShowDialog). That way the user has full control.

Also, it would be great if someone can investigate what happens with the Elmish dispatch loop when the window is closed. Is it garbage collected, or does it leak?

@ScottHutchinson
Copy link
Contributor Author

Yeah, I agree. For completeness, we might want one sample that does not use Elmish for the main application, and one that does use it.

Also, since the Program.startElmishLoop is public, I can also do it like below without modifying Elmish.WPF at all. We could still add sample(s) showing how to do this. It seems obvious and trivial now, but it took me a couple days of frustration before I figured it out.

        let showDialogWithConfig config (window: Window) program =
            Program.startElmishLoop config window program
            window.ShowDialog ()

        Program.mkSimpleWpf init App.update App.rootBindings
        |> showDialogWithConfig
            ElmConfig.Default
            (MsgTypeFiltersWindow())
        |> ignore

@TysonMN
Copy link
Member

TysonMN commented May 8, 2020

Since Program.startElmishLoop is already public, I think it is not worth adding another public function to also show a Window as a dailog.

@cmeeren
Copy link
Member

cmeeren commented May 8, 2020

Brilliant! Let's just focus on a sample, then.

Personally I think it's sufficient to show a non-Elmish app using Elmish.WPF for sub-windows. Using separate Elmish dispatch loops for an app that already runs Elmish.WPF at the root seems unnecessary and, while possible, not something that anyone would really want to do. Feel free to enlighten me with counter-examples, though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants