JordanMarr / ReactiveElmish.Avalonia

Static Avalonia views for Elmish programs
Other
94 stars 8 forks source link

Best way to parameterize init() #6

Closed daz10000 closed 1 year ago

daz10000 commented 1 year ago

I have been having a (mostly) wonderful time with Elmish.Avalonia. Thanks for that. As I'm building out more complex flows between different screens, I was seeing some strange behavior moving between a gallery view and a view looking at one sub document. I traced the problem to recreation of the model for the second screen (let's call it screen B). The user selects an item in screen A. I was using the pattern you helped me with to send a message to the main controller to switch views to view B. What is the best practice for initializing the subview with the right starting data? I found a method that doesn't work. What I tried first was to have viewmodel B see the switching message on the message bus, pick up the parameter and load the right doc. The problem with that is that Elmish seems to reinitialize the model for view B as the screen view switches, wiping out the model data changes. I have a kludgey workaround, but is there a clean way to pass data to that call to init? I can see internally in the Elmish code that init can take an arg in runWithDispatch but the call to reinitalize the model is triggered internally when the ContentVM changes, so I can't see a clean way to influence the recreation of the second model.

To put it in more concrete terms, if the counter page in the samples wanted to show the current count on the about page as the view switches, how would one do that?

JordanMarr commented 1 year ago

One way to do it:

Essentially, MainViewModel becomes the place to store any global shared app data since it is open for the lifetime of the app, while the other viewmodels are generally being initialized on demand.

Glad to hear that you are enjoying It!

JordanMarr commented 1 year ago

After trying this, it looks like the underlying library may require an adjustment to allow passing in an arg.

daz10000 commented 1 year ago

Yes, at first I was 🤦 here - just add an argument, but I went down that path among many, and the wrapper for the StartElmishLoop / IStart interface doesn't allow an argument to slip in, even though the underlying library has a slot for an arg parameter internally

image

somewhere between startElmishLoop and the init call in runWithDispatch, an argument is conjured up but I can't get to it as a library user

image

I haven't followed the plumbing closely enough, but this feature is really critical for correctly parameterizing startup of alternate models. Right now I have a mutable global variable that the global message bus stashes when the view change occurs, and then the init function is fishing it out on startup. Really ugly but the app works as exected. I was going crazy in my first implementation I had the submodel listen to the global message bus. It would see the view change, go and do a data fetch, correctly update the model (unfortunately the copy of the model that was about to die), and then when the global content switched over, a fresh model init() happens and wipes it all out (which I can't parameterize). Took a while and some careful fingerprinting of model instances to even work out what was happening.

If you have ideas on how to wire that hook in, l'm happy to help implement or discuss. Thanks for the quick reply as usual

JordanMarr commented 1 year ago

Here is an example where the CounterViewModel publishes a SetCount message that MainViewModel subscribes to. MainViewModel stores this in its model so that I can later pass it to AboutViewModel.vm as an argument.

AvaloniaProgram.mkProgram is hardcoded to always expect init to take unit; however, we can get around this easily in the AboutViewModel by wrapping init with an anonymous function that takes unit:

let vm currentCount = ElmishViewModel(AvaloniaProgram.mkProgram (fun () -> init currentCount) update bindings)
JordanMarr commented 1 year ago

When possible, I would try to only subscribe to and store shared data changes messages in the MainViewModel and then pass it to child VMs as needed. This is because MainViewModel is essentially a singleton, whereas the child VMs (at least in my example) are transient (re-initialized every time).

However, it should also be possible to initialize a child view once (during MainViewModel.init), and then it would maintain state, and could also subscribe directly to global messages that it cares about. It really just depends on what you are trying to build. Sometimes you want the VMs to be transient, and other times you want them to maintain state.

JordanMarr commented 1 year ago

Just for context, I should also point out that the idea of having completely independent "Elmish View Models" that communicate shared state via a message bus is an idea that I tacked onto the original Elmish.WPF project (which this was ported from).

In Elmish.WPF, you would have a more traditional global Elmish loop, and you would manually "wire up" the messages between parent and child. (Although they have some very nice helper functions that make this fairly easy).

There are obviously pros and cons to each approach.

Isolated IElmishViewModels Apprach

Pros:

Cons:

Global Elmish Loop Approach

Pros:

Cons:

daz10000 commented 1 year ago

Thanks for all the suggestions and especially the wisdom. TL;DR - the init function with the closure solves the problem. I'm not ready to go full Elmish :)

The child subscribing to the global message stream indeed breaks for exactly the reason you explain - they are transient (I didn't appreciate that initially). The wrapper for the init function looks like it will work nicely for the applications (just tried that and it works perfectly and looks clean). On the philosophy bit, I have had the same discussions and internal debates using Fable / Elmish for large heterogeneous applications (a lot of the systems I've worked on are loosely federated sub-applications with some high level navigation and smaller sets of interconnected pages. I have never managed to love the hierarchical Elmish model. It breaks my sense of implementation hiding having everything routing through one place - everything is entangled. I have to remind myself that I don't do functional programming because I love functional programming. I do it to get useful applications in the hands of users faster. Having fewer defects is part of doing things quickly, but also being able to design pieces quickly (in isolation) and bolt them together with minimal interactions. We were burned in the javascript world a few times by overzealous event routing turning simple user interactions (like scanning a barcode into a field) into low performance, buggy user experiences by too much message passing, when simply encapsulating a working and perhaps slightly stateful UI element would have been faster to build and better for the user.

It might be fun / instructional to see the About/Counter app side by side in both styles just as an example / documentation. I think for now, the global message bus seems more practical for building what I need, but I'll let you know. I'm still very impressed with how easy it is to throw things together so far, so I'm liking the pattern you've built.