elmish / Elmish.WPF

Static WPF views for elmish programs
Other
428 stars 71 forks source link

How to handle views that are only used part of the time #168

Closed BentTranberg closed 4 years ago

BentTranberg commented 4 years ago

I know I can use windows to only keep part of my user interface alive at any one time, but how can I achieve the same when I want all of my user interface within a single window?

In the SubModelOpt sample, Form1 and Form2 are declared in a StackPanel, and their Visibility is controlled. Doesn't that mean that even though they're invisible, they still occupy memory?

What I'm worried about, is that I have this huge hierarchy of controls that waste memory and resources, and potentially can cause problems, because I haven't yet understood how to deal with this in a good way in general.

TysonMN commented 4 years ago

I was just adding such a hierarchy for the first time to my program at work!

I am thinking about it at two different levels. At the view level, I was planning on binding the visibility of huge trees of XAML to either Visible or Collapsed base on some top-level state that knows which "screen" is currently visible. At the model level, I was considering having functions called enter/exit (or start/stop or show/hide) or just have message types by those names. Then when the top-level has to "exit" one "screen" and "enter" the "another" screen, it would first execute the "exit" logic of the first and then the "enter" logic of the second.

This approach allows full customization for what each screen looks like when it is entered a second time (i.e. does it look identical to when it was entered the first time or does it look like identical to when it was exited the first time?). I think the object tree/graph of view/XAML instances would be in memory the whole time. The model tree/graph could be in memory the whole time, or parts of it could be discarded in the exit logic.

In my case, I have a certain screen that subscribes to changes in some external data source. When navigating away from that screen, I will unsubscribe from those change notifications; when navigating to that screen, I will subscribe to them again. These subscriptions are kept in the model as a field of type IDisposable.

What I'm worried about, is that I have this huge hierarchy of controls that waste memory and resources, and potentially can cause problems..

Can you elaborate? Are you concerned about just the memory used by the view/XAML instances or memory used by the models or memory/CPU used by background commands or something else?

BentTranberg commented 4 years ago

The problem that I feel is there, is that the entire object graph described by the XAML is alive and potentially doing things that doesn't need doing at the moment. Even if it doesn't blow up the machine, what problems can it cause? Just having all this in memory is a waste, and adds to potential problems with GC and with profiling memory use. Without even having looked yet, I guess it's a nuisance to have the entire tree present in the debugger. Does it slow down the response of the app? Every fifth year or so, something in XAML cause the app to crash, and it would be easier for me and my customers in so many ways if that didn't happen on startup, but waited until we hit the bad spot while navigating around.

When I used FsXaml and simple code behind some years ago, I was easily able to use a control to host any other user control, and I could insert and remove user controls at runtime.

I'm thinking that if I could easily do something similar using Elmish.WPF - be able to add and remove any user control with model into/out of a host control/model of some kind, then I would use this strategically in some places. Maybe even use it for a plug-in system involving DLLs, which I actually want too.

When designing a modern user interface with XAML, I want to avoid the use of more than one window, although I understand using windows could easily solve this problem. Instead I want e.g. toolbar buttons that work like radio buttons to control which one of many different user controls should be visible in the main pane of the window. Also in many places further down in the hierarchy, I have the same situation again, with something looking like a tab control or a toolbar or a wizard deciding which one of many user controls to show. So these controls represents what could have been quite independent windows that wouldn't need to communicate, had I not insisted on having just one window. Inside these controls I will happily stick to the more common way of doing things in Elmish.WPF.

It's obviously a pattern, and I really use it a lot, because it actually makes sense with the application I am building. The system is used to control hundreds of industrial plants with hundreds of various subsystems having hundreds of settings and tables and outputs and whatnots, so I need to expand this GUI in all directions.

cmeeren commented 4 years ago

What I'm worried about, is that I have this huge hierarchy of controls that waste memory and resources, and potentially can cause problems, because I haven't yet understood how to deal with this in a good way in general.

I'd be very, very surprised if WPF keeps view hierarchies around when a binding high up in the hierarchy returns null, which is the case with subModelOpt as long as you don't use sticky.

This would be a problem in normal MVVM, too, which is why I don't think it's a problem at all.

In short, don't feel/guess - actually measure, or find official WPF docs describing how this works. :) WPF is mature enough that I assume this is not a problem until proved otherwise.

BentTranberg commented 4 years ago

@cmeeren, thanks a lot. I'll proceed in that direction. Actually I'll be rebuilding the entire application to fix this and a lot of other stuff based on what I have learnt through your recent tutorial, and to take better advantage of all the nice work you and @bender2k14 have done. Thanks to both of you.

cmeeren commented 4 years ago

Thanks :) Closing this as not a problem until proved otherwise. Feel free to re-open or post a new issue if necessary.

BentTranberg commented 4 years ago

In my case, I have a certain screen that subscribes to changes in some external data source.

@bender2k14, I think I've managed to figure out how to do this.

When using subModelOpt, I don't know how to deal with the subscription hierarchy (using Program.withSubscription) that I had earlier, so I thought it worth investigating this alternative way of subscribing.

I've wanted to do this for some years actually, because it makes the model less dependent on a parent, and allows subscribing at any point. I could really need that, since I'd like each view to have several Akka actors that don't nessessarily have a lifetime equal that of the view.

As far as I know there are no demos of this on the net. The most helpful I've found is https://medium.com/@MangelMaxime/my-tips-for-working-with-elmish-ab8d193d52fd, and from there I guessed how to subscribe to an event source.

Since there is so little help to be found on the net for this particular functionality, I think it would be nice to have a demo and/or documentation of this in Elmish.WPF, maybe together with subModelOpt.

This is the sample I have at the moment.

    open System
    open System.Timers
    open Elmish
    open Elmish.WPF

    type Msg =
        | StartTicks
        | StopTicks
        | Tick

    let getTicker () =
        let t = new Timer()
        t.Interval <- 3_000.
        t.Enabled <- true
        t

    let getTicks (ticker: Timer) dispatch =
        ticker.Elapsed.Add (fun e -> dispatch Tick)

    type Model =
        {
            Ticker: Timer
            ClockText: string
        }

    let init () : (Model * Cmd<Msg>) =
        {
            Ticker = null
            ClockText = "no time"
        }, Cmd.none

    let update (msg: Msg) (m: Model) : (Model * Cmd<Msg>) =
        match msg with
        | StartTicks ->
            let ticker = getTicker ()
            { m with Ticker = ticker }, Cmd.ofSub (getTicks ticker)
        | StopTicks ->
            m.Ticker.Dispose ()
            { m with Ticker = null }, Cmd.none
        | Tick -> { m with ClockText = string DateTime.Now }, Cmd.none

    let bindings () : Binding<Model, Msg> list =
        [
            "StartTicks" |> Binding.cmd StartTicks
            "StopTicks" |> Binding.cmd StopTicks
            "ClockText" |> Binding.oneWay (fun m -> m.ClockText)
        ]
    <StackPanel>
        <Button Content="Start" Command="{Binding StartTicks}"/>
        <Button Content="Stopp" Command="{Binding StopTicks}"/>
        <Label Content="{Binding ClockText}"/>
    </StackPanel>
cmeeren commented 4 years ago

Since there is so little help to be found on the net for this particular functionality, I think it would be nice to have a demo and/or documentation of this in Elmish.WPF, maybe together with subModelOpt.

Adding a sample would depend on whether this is the best way to accomplish what you're trying to do. I'm confused what that is in the first place; your latest comment doesn't seem to be related to the original issue.

In any case, the subModel sample sets up a timer subscription:

https://github.com/elmish/Elmish.WPF/blob/bef1a91d99427e3d8147b35717fcba69a1a73875/src/Samples/SubModel/Program.fs#L130-L140

Also note that instead of update directly using DateTimeOffset.Now, this is done outside in the "impure" world. The message contains the current time that the update function should use, which is arguably better (easier to test, at least).

BentTranberg commented 4 years ago

I am not sure I agree it's not on subject. I am trying to figure out how to deal with issues that I have as a result of switching to subModelOpt - in other words views that are only used part of the time.

If I were to use Program.withSubscription and a hierarchy of subscription calls, how would I do that when the model is optional, and None at the start?

the subModel sample sets up a timer subscription

It does so using Program.withSubscription. I never before found out how to do it using Cmd.ofSub outside the hierarchy of subscription calls of which Program.withSubscription is the root. I only knew it could be done because Eugene Tolmachev told me some years ago, but only now did I have to make time to research it.

Btw, having a great time rebuilding my app. Lots of work, but the result is so much better this time round, so thanks again.

cmeeren commented 4 years ago

Could you (briefly) describe the actual real-world use-case/functionality that requires you to set up subscriptions in sub-models? It would make it easier to think about the issue. Some problems can be avoided by re-designing, but that's hard to do with trivial cases, which are often (trivially) re-designable in ways not representative of the real-world problem.

BentTranberg commented 4 years ago

I use Akka.NET actors in the "views" (the views are normally level 3 proper submodels bound with subModelOpt). The views will create Akka actors in order to communicate with actors in a server.

In the old app I have used just one actor per view, instantiated through the Program.withSubscription hierarchy and with lifetime equal to the view.

However, with this new way of subscribing, I can create actors at any point in time. I suspect this will be much better because I can create and use several specialized (one responsibility) reusable actors that are more easily used within the Elmish models.

I don't think I will have a need for Program.withSubscription to call the subscribe of submodels again, but it would still be nice to know if there's a simple way to do it with a model bound with subModelOpt.

cmeeren commented 4 years ago

The views will create Akka actors in order to communicate with actors in a server.

As I understand TEA (The Elm Architecture), this really sounds like it should be handled globally. Basically, all impurity/side-effects are intended to be done through returning Cmd from update. (And indeed must be in Elm; since it's a pure language, that wouldn't even compile.)

My first thought is that your sub-views can send messages that through a Cmd causes an actor to be created somewhere else (if you want to do this when the sub-view is created, this can also be done by returning a relevant Cmd in the update that creates the sub-model). Furthermore, you use messages to communicate that something should be sent through the actor. And the actor of course dispatches its own messages (as a subscription) into the Elmish loop.

If you need one actor per sub-view, I'm sure you can come up with a fairly clean way to use the sub-model IDs to control creation/disposal of the actors, as well as communication with them (which message goes to which actor). The actors themselves can live outside the model, e.g. in a mutable list.

This was very briefly explained. Let me know if I didn't get across a clear point, or if you have other problems with my suggestion.

In short: Keep bindings focused on bindings only (and the XAML views/code-behinds focused on UI only), and use Elmish messages and commands to do everything impure such as communication with the outside world.

BentTranberg commented 4 years ago

Thank you very much, that's helpful, and again points me in the right direction. Now that you explain, I believe I also see the reasoning behind pushing the actors themselves completely out of the model hierarchy. For web development, I use Bolero. There I have stuff like the following, modeled after Bolero samples. I suspect it reflects parts of what you're telling me.

let update (remote: RemoteServices) message model : Model * Cmd<Message> =
    ...
    | RequestAddUser (username, password, isAdmin) ->
        model, Cmd.ofAsync remote.Users.addUser (username, password, isAdmin) RespondAddUser UsersError
    | RespondAddUser ok -> if ok then model, Cmd.ofMsg RequestRefresh else model, Cmd.none
    | UsersError exn -> model, Cmd.none // Handled by parent.
cmeeren commented 4 years ago

Yes, that seems to show what I mean: Use Cmd to invoke anything that is impure. Whether that is HTTP calls or actors or anything else doesn't matter. :)

I have added a small tutorial section on this.