I’m trying to create an undo stack, using the undo [control] from corelibbasic.vl
I’m working with a bunch of UI widgets that are classes under the hood.
I’ve noticed that undo doesn’t work properly with a spread of classes, it knows when they are created/removed (changing the spreadcount) but it doesn’t track changes in individual objects. My understanding is this is because the class is just a pointer to a memory location so the undo is only storing the pointers to the memory location. The pointers don’t actually change so undo doesn’t track any changes in the objects themselves.
Undo does however work on a spread of records.
For the structure of my application its too late to turn my UI widgets into records permanently.
Is there a function that would transform a class object into a record version at runtime (and back again) so I can use it in the undo stack? (is such a function maybe also used for temporarily passing an object to an async process?)
I could use serialize but I would think that’s a lot of extra processing and memory space to store it all as xelements?
Or I could manually create a ‘record version’ of the widget class but then every time I add properties to the class I would also have to manually add them in the record plus to any utility converter operations?
What you’re asking for is a deep copy of your widgets. And no such a “in-memory” function which works on any object does not exist yet. The closest we have for that is what you already mentioned: serializing and deserializing.
You could however patch such a function for your specific widgets I guess. That deep copy widget function you would then call before putting the whole thing into the Undo stack. One could probably even get smarter and make the copy in a way that objects (or widgets) get re-used from the previous snapshot so you end up with similiar semantics as what records are doing.
Having such copy functions in a general way sounds interesting though and maybe worth investigating a little further.
But before generalizing I’d encourge you to do it manually for your widgets and report back your findings.
Your UI widgets are mutating (they are maybe animated and even change slightly from frame to frame). But what you actually want to have is a small model that describes what data the widget should display. And this model could be a record.
So it sounds that it’s not so much about turning everything into records. It sounds like you should add the notion of a model that captures the state of your app. A model is something you would undo, store, load. The UI would sync to that. Tools for syncing:
You could use Change to detect changes in the model (a change with records is only possible by handing a new snapshot). So Change will always work on no matter how complicated your model
There is an Identity property in each instance of a patched datatype. The Identity is kept through all the snapshots of a model. Let’s say you only set a certain property of a record: you get a new snapshot where only this property got changed. All the rest is the same (including the identity).
When syncing a collection: With that in mind you can make sure that Widgets stick to their items even when a slice in the middle of a spread got deleted.
When using an immutable model: Another trick is to hold a main Reference < ModelRoot >. This reference you can pass around, so that you can post a new model snapshot from deep within some controller patches.
I will definitely consider model<>runtime structure for next time for this purpose. I will be looking closely at dottores patterns.
For this application in the end I used serialize with manual hooks into the UI for pushing undo.
Its not very performant but it works fine for my application and was relatively quick to patch considering an undo/redo was never originally planned as a feature of this software.
I think I faced the same problem with an app I am working on, and also thought it was too late to change to an immutable workflow. But I took the effort and converted the spread of objects which I needed the benefits of immutability - and implemented operations in the containing class to update objects in that spread.
Like that you can generally keep the structure you have, connecting a lot of modifying nodes downstream. But you write back to the list of objects just through calling operations in the mutable ‘container’ class, instead of working on mutable objects directly.
It works basically the same like the reference pointer that dottore used in his demo.