Elmish.wpf: Q: Are there any Undo/Redo suggestions?

Created on 18 Jan 2020  路  6Comments  路  Source: elmish/Elmish.WPF

How could I implement a undo/redo functionality?

Approaches that I was thinking about:

  • Store the state of the model every time a messages has been sent.

    • Problem: Memory consumption

  • Store each message sent

    • Undo = replay messages from init state to the desired undo state

    • Problems:

    • Could be slow

    • If some non pure functional code is involved e.g. Guid.NewGuid(), the result will not be the same

    • Store an inverting function for every message that has been processed

    • Problems:



      • How to store them if there are many different types of inverter functions?


      • What if the invert function can't be found? Does a inverter function always exist?



Are there any other ideas? Or even examples? How does the time-warp debugger of Elmish work?

Most helpful comment

I created a demo here https://github.com/bender2k14/Elmish.WPF/commits/demo/undo_redo

It adds undo-redo functionality to the simple counter sample. It stores the model every time a message is sent via a function called push. However, I also added a function called map that changes the current model without also changing the undo-redo state.

I am going to use this code in my application at work. My plan is for the parent context to use either map or push to perfectly control the undo-redo state.

I will comment again to provide an update on that use case.

All 6 comments

While Elm/Elmish architectures make undo/redo simpler, there is no silver bullet. An undo strategy is completely up to the implementer, and will vary based on application requirements. Relevant questions are: What in particular should be undoable? Do you want to undo several things separately? And many more.

Undo based on whole state: Probably not a good idea. You don't want to undo UI stuff such as hiding/showing dialogs etc.

Regarding GUIDs: You could keep update completely pure (including deterministic) by making sure that a GUID is included in the message. Elm forces this, but F# doesn't since it isn't a strictly pure language. (Which has both benefits and drawbacks.)

Closing since the choice of a redo strategy is not related to Elmish.WPF and thus not something to be "solved" here, but feel free to continue the discussion. In general though, you might be better off asking this on a general Elmish or Elm discussion forum (and investigating how others have solved this in those ecosystems).

Making a prototype with this feature sounds fun. I might try this sometime. I will share here if I do.

I created a demo here https://github.com/bender2k14/Elmish.WPF/commits/demo/undo_redo

It adds undo-redo functionality to the simple counter sample. It stores the model every time a message is sent via a function called push. However, I also added a function called map that changes the current model without also changing the undo-redo state.

I am going to use this code in my application at work. My plan is for the parent context to use either map or push to perfectly control the undo-redo state.

I will comment again to provide an update on that use case.

I forced pushed many times to that branch. I am satisfied with that code now.

All changes to the current state are done via map.

The functionality that used to be called push is now slightly simpler and called mark. You call mark when you want to mark the current state and push the previously marked state onto the undo stack. However, this only happens if the current state and the marked state are different as determined by a given equals function. Otherwise, nothing happens.

The data in the undo and redo stacks is a different type than the type of the current and marked state. The mapping between these types is done by the given get and set functions. The simplest case is having these types be the same, which means the undo and redo stacks are storing the entire state. That is what my example branch does. I thought of two use cases for having these types be different.

  1. The first is that the undo and redo stack could use the different type to save less information. This is just an optimization though; the second reason is more important.
  2. The second reason is to be able to undo some changes to the primary model but not all.

I am using this code in a drawing editor that I am creating at my work with different types for the current and marked state. I want changes to the drawing content to be undoable but not changes to the viewport (i.e. when the view is translated, scaled, or rotated). I achieve this by only storing the drawing content in the undo and redo stacks instead of also including the transformation to the view. I then call mark when a user interaction is complete, which is either a mouse up event, a multi-touch completed event, or certain buttons are pressed (like "Remove" and "Reset").

My example goes with option 1 that I gave in https://github.com/elmish/Elmish.WPF/pull/212#issuecomment-625213318. That is, my example uses two SubModel bindings and the bindings for each submodel are in different subtrees in the XAML.

The two undo-redo bindings are trivial. They don't need to be extracted for reuse, but it is good that all of the other F# undo-redo code is completely independent of any use case. I also like that none of the F# counter code knows about the undo-redo code.

But what if you want to have better/tighter integration? For example, instead of the (left-to-right) button order being "Reset", "Undo", and "Redo", what if you wanted it to be "Undo", "Redo", and "Reset"? Then corresponding XAML bindings would not be in separate substrees.

My program at work is like that. Instead of pushing complexity into the XAML, I pushed complexity into the F# bindings by specifying them all together. For example, here are five of them that I also shared in https://github.com/elmish/Elmish.WPF/pull/212#issuecomment-624877706.

fun () ->
  [ "MouseLeftButtonDown" |> Binding.cmdParam(RawDown >> RawInputMsg |> onMouseEvent)
    "MouseLeftButtonUp" |> Binding.cmdParam(RawUp >> RawInputMsg |> onMouseEvent)
    "MouseMove" |> Binding.cmdParam(RawMove >> RawInputMsg |> onMouseEvent)
    "Undo" |> Binding.cmdIf(Undo |> UndoRedoMsg, (fun m -> m.Core) >> canUndo)
    "Redo" |> Binding.cmdIf(Redo |> UndoRedoMsg, (fun m -> m.Core) >> canRedo)
  ]

I am still interested in Binding<'model, 'msg> being a (covariant) functor in 'msg (so that it is possible to define mapMsg), but I don't intend to put too much more time into this unless someone requests further assistance.

I forced pushed many times to that branch. I am satisfied with that code now.

Except I just realized that I had not committed all my code. I just forced pushed again. It is all there now.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

BentTranberg picture BentTranberg  路  4Comments

dsyme picture dsyme  路  10Comments

TysonMN picture TysonMN  路  13Comments

TysonMN picture TysonMN  路  9Comments

awaynemd picture awaynemd  路  5Comments