fabulous-dev / Fabulous

Declarative UI framework for cross-platform mobile & desktop apps, using MVU and F# functional programming
https://fabulous.dev
Apache License 2.0
1.16k stars 122 forks source link

Live Update Model Serialization #397

Closed JordanMarr closed 2 years ago

JordanMarr commented 5 years ago

I am trying to create a revit-fabulous template - because I think that Fabulous + Live Update has the potential to be a game changer for Revit add-in developers (mostly due to the fact that "[stop; change code; reload Revit; wait; reload model; wait; verify change; repeat]" is a terribly inefficient development loop).

However, I have come across a few stumbling blocks, the first of which I will address here:

My current "ViewManagerPage" sample exists to showcase the technique for injecting Revit functionality into the init function from the composition root, which is the ViewManagerCommand.

The first big roadblock is that LiveUpdate recompiles the WPF project (I have merged the base and WPF projects in this template), and then it calls programLiveUpdate, which replaces my injected Revit functions that were stored in the model with stubbed versions.

My attempt to get around this was to serialize my model using FsPickler to preserve my injected Revit function references, and then deserialize after LiveUpdate recompiles; but the problem is that LiveUpdate / PortaCode appears to be recreating a structure to represent my model that is a new generated type with a different shape, which prevents the original model from deserializing.

Might there be a way for PortaCode to leave my model type intact (or at least the functions)?

I suspect I may be at a dead end, but I hope not because I think it would pretty a compelling feature for Revit add-in developers to be able to LiveUpdate without losing the injected references to the Revit model.

There are a few other smaller issues that have to be resolve as well; namely the fact that I want to be able to load different "programs" in different windows which seems to be problematic; and also that the ability to debug goes away after a LiveUpdate recompile; but I think this is the main issue that will determine if I can continue.

JordanMarr commented 5 years ago

Using the Counter example, this shows the way LiveUpdate changes the model serialization:

image

And using FsPickler: image

dsyme commented 5 years ago

@JordanMarr Ah yes, of course. I'm not sure there's going to be an easy way around that.

The only approach I know of that will avoid this is to implement bespoke state serialization (something that likely comes eventually in the development of any app, but this would force it earlier)

Alternatively we can look at adjusting the LiveUpdate types like RecordValue and UnionValue to be 100% NewtonSoft JSON compatible.

JordanMarr commented 5 years ago

Ultimately, I have to serialize the model using FsPickler to ensure that the Revit functions remain intact. FsPickler.Json uses Newtonsoft.Json, so maybe making RecordValue and UnionValue 100% Newtonsoft.Json compatible would be enough to facilitate this, but I'm not sure.

Is the idea that RecordValue serialization would be customized to output {"Count":0,"Step":0,"TimerOn":false} instead of {"Case":"RecordValue","Fields":[[0,1,false]]}?

In order to do that, then RecordValue would also need to hold the record label names in addition to the values, so maybe RecordValue of (string * object)[]. Then it would need a custom Newtonsoft JsonConverter to customize the serialization... is that right?

JordanMarr commented 5 years ago

I think I have the serialization bit mostly worked out: ModelStorage.fs

My strategy on saveModel is to use reflection to convert the model to a Interpreter.RecordValue (if the model is still a record). If the model is already a RecordValue, no conversion necessary.

On readModel, it can always deserialize a RecordValue, but it may have to use reflection to convert it back to a record.

Finally, persistModelDuringLiveUpdate enables this model persistence by loading the model after init, and saving the model after update (only if #DEBUG).

It seems to be basically working so far: everything in the model is being saved and then reloaded properly once LiveUpdate takes over! I may need to do more work to the reflection code to handle more complex models.

JordanMarr commented 5 years ago

One issue that I am having, which is likely due to the fact that I am trying to use this library in an unintended fashion by having more than one "program" / "page / "window" (instead of one SPA style program), is that let programLiveUpdate = ... is being evaluated for all my "pages". This causes strange things to happen. For example, LiveUpdate will try to swap out my CounterPage with the ViewManagerPage, I think because ViewManagerPage is listed first.

I attempted to change programLiveUpdate to be a function let programLiveUpdate() = ..., but LiveUpdate fails with "LiveUpdate couldn't quack!" because LiveUpdate expects a member: tryFindMemberByName "programLiveUpdate" ...

I think it would be nice if it allowed programLiveUpdate to be a function, which would be a step in the direction of allowing multiple programs/windows. Although I am open to the reality that maybe I am misusing the library by not making everything into one giant SPA. (The nature of Revit add-in development is that it is common to have separate dialog windows that are specific to a custom Revit command).

TimLariviere commented 2 years ago

Closing since we don't have LiveUpdate in v2 anymore