0

I cannot get Undo and Redo to behave correctly when using a dialog.

I have a simple model with a property indicating the state of the object(running, paused, stopped) which can be altered via a dialog. What happens is that I get actions that seems to do nothing in my undo queue or undo restores the object to an intermediate state.

The model object is registered with memento in the constructor. The dialog has three radio buttons each representing one of the three different states. Each radio button is bind to a command each. Each command performs a change of the property. I have tried two different approaches, either each command sets the property directly in the object or each command sets an instance variable for the view model when called and then I use the Saving event to modify the object.

If using the first approach each property change is put on the Undo queue if the user clicks on more than just one radiobutton before clicking Ok in the dialog. Tried to solve that by wrapping the whole dialog into a batch but that results in undoing the state change the object is restored to the state it had before the final one, i.e. if the property was set to stopped before the dialog opened and the user pressed the pause radiobutton, then start one and finally Ok, undo will set the property to paused instead of the expected stopped.

If using the second approach the user opens the dialog, change the state to paused, click Ok in the dialog the undo/redo behaves as expected but if the dialog is opened again and Cancel is chosen one more action is added to the Undo queue, i.e. the user has to click Undo twice to get back to the initial stopped-state.

So my question is how should this be correctly implemented to get the expected behaviour; that each dialog interaction can be undone and not every interaction in the dialog?

Here is the code for the ViewModel:

namespace UndoRedoTest.ViewModels
{
    using Catel.Data;
    using Catel.MVVM;
    public class StartStopViewModel : ViewModelBase
    {
        Machine.MachineState _state;
        public StartStopViewModel(Machine controlledMachine) 
        {
            ControlledMachine = controlledMachine;
            _state = controlledMachine.State;
            StartMachine = new Command(OnStartMachineExecute);
            PauseMachine = new Command(OnPauseMachineExecute);
            StopMachine = new Command(OnStopMachineExecute);
            Saving += StartStopViewModel_Saving;
        }

        void StartStopViewModel_Saving(object sender, SavingEventArgs e)
        {
            ControlledMachine.State = _state;
        }

        [Model]
        public Machine ControlledMachine
        {
            get { return GetValue<Machine>(ControlledMachineProperty); }
            private set { SetValue(ControlledMachineProperty, value); }
        }

        public static readonly PropertyData ControlledMachineProperty = RegisterProperty("ControlledMachine", typeof(Machine));

        public override string Title { get { return "Set Machine state"; } }

        public Command StartMachine { get; private set; }
        public Command PauseMachine { get; private set; }
        public Command StopMachine { get; private set; }

        private void OnStartMachineExecute()
        {
            _state = Machine.MachineState.RUNNING;
            //ControlledMachine.SecondState = Machine.MachineState.RUNNING;
        }

        private void OnPauseMachineExecute()
        {
            _state = Machine.MachineState.PAUSED;
            //ControlledMachine.SecondState = Machine.MachineState.PAUSED;
        }

        private void OnStopMachineExecute()
        {
            _state = Machine.MachineState.STOPPED;
            //ControlledMachine.SecondState = Machine.MachineState.STOPPED;
        }
    }
}

1 Answers1

1

First of all, don't subscribe to the Saving event but simply override the Save() method. Note that Catel handles the model state for you when you decorate a model with the ModelAttribute. Therefore you need to get the prestate and poststate of the dialog and then push the result set into a batch.

For example, I would create extension methods for the object class (or model class) like this:

public static Dictionary<string, object> GetProperties(this IModel model)
{
    // todo: return properties
}

Then you do this in the Initialize and in the Save method and you would have 2 sets of properties (pre state and post state). Now you have that, it's easy to calculate the differences:

public static Dictionary<string, object> GetChangedProperties(Dictionary<string, object> preState, Dictionary<string, object> postState)
{
    // todo: calculate difference
}

Now you have the difference, you can create a memento batch and it would restore the exact state as you expected.

ps. it would be great if you could put this into a blog post once done or create a PR with this feature

Geert van Horrik
  • 5,689
  • 1
  • 18
  • 32
  • I do not understand why I have to do it this way and why it does not work the way I have implemented it. Anyway I got it to behave as expected based on the information above. I created a copy of the properties when the dialog is created as an initial state. Then in the Save method I go through the Dictionary of properties looking for the one this dialog changes and see if it changed. If it changed I create an ActionUndo and add it to a Batch. Finally I add the batch to the Memento service. Sure, can write a post about it. But what is a PR? – Karl-Petter Åkesson Sep 11 '14 at 13:22
  • aah, you mean integrate it into ModelBase and ViewModelBase. Think I would need to understand Catel better for that, but can give it a try and if you like it, accept it. – Karl-Petter Åkesson Sep 11 '14 at 14:56