Closed BentTranberg closed 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?
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.
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.
@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.
Thanks :) Closing this as not a problem until proved otherwise. Feel free to re-open or post a new issue if necessary.
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>
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:
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).
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.
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.
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
.
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.
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.
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.
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.