8
\$\begingroup\$

Several times we need to be able to revert the data the user is inserting to a previous state (cancel data update). In order to keep DRY and for the sake of separation of concerns I thought about a way of just allowing that.

public class RevertableObject<T> : ViewModelBase //implements INotifyPropertyChanged
    where T:class, ICloneable
{
    public RevertableObject(T initialValue)
    {
        Value = initialValue;
        Value = initialValue.Clone() as T;
    } 

    private T _oldValue;
    private T _currentValue;
    public T Value
    {
        get { return _currentValue; }
        private set
        {
            _oldValue = _currentValue;
            _currentValue = value;
            OnPropertyChanged();
        }
    }

    public void Revert()
    {
        Value = _oldValue;
    }
}

Some drawbacks of this solution are, but are not limited to:

  • Dependency on the interface ICloneable which does not have a way of knowing the cloning algorithm being used (member clone vs deep clone)
  • It only allows one object to be reverted once.

Any insights of better alternatives? Is this code acceptable?

\$\endgroup\$
  • \$\begingroup\$ An alternative that I have used is to just clone the entire viewmodel object graph and save it as a checkpoint. Then the revert operation sets the data context to one of those checkpoints. It worked pretty well. \$\endgroup\$ – default.kramer Jan 29 '16 at 23:13
  • \$\begingroup\$ @default.kramer Meaning that your view model had a reference to the control/Window? :\ \$\endgroup\$ – Bruno Costa Jan 29 '16 at 23:21
  • 1
    \$\begingroup\$ No, it didn't set the data context directly. There is a main viewmodel (has logic) which holds an object graph (no logic) for data binding. The main viewmodel can then create and restore snapshots of that object graph. But that is just one way to do it; I can imagine an attached behavior working also. \$\endgroup\$ – default.kramer Jan 30 '16 at 0:29
4
\$\begingroup\$

You could empower IEditableObject. Here is a good article. Approach described will allow your model to do not mix concerns - they use memento design pattern.

UPDATE: I do not feel right by copy-pasting that article, so I would still recommend to have a look at it. Quick test though might look like the following:

1) Download and reference BindingOriented.Adapters project in your WPF application.

2) Create model:

public class Contact
{
    public string Name { get; set; }
}

3) Create view model (I used MvvmLight for RelayCommand):

public class MainViewModel 
{        
    public MainViewModel()
    {
        Contact = new EditableAdapter<Contact>(new Contact() { Name="Dmitry" });
        OKCommand = new RelayCommand(() => Contact.EndEdit());
        CancelCommand = new RelayCommand(() => Contact.CancelEdit());
    }

    public EditableAdapter<Contact> Contact { get; }
    public ICommand OKCommand { get; }
    public ICommand CancelCommand { get; }
}

4) Update MainWindow to include:

<StackPanel>
    <StackPanel.DataContext>
        <vm:MainViewModel/>
    </StackPanel.DataContext>
    <TextBox Text="{Binding Contact.Name}"/>
    <Button Content="OK" Command="{Binding OKCommand}"/>
    <Button Content="Cancel" Command="{Binding CancelCommand}"/>
</StackPanel>

It works :)

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ I saw something about the IEditableObject sometime ago, don't really know why I never used it. Thanks for bringing it back. if you include a sscce you get +2 :p. If it is much a bother you can just include how my RevertableObject would be implemented (while still using the cloning approach). If not I'll need to post it on a different answer <.< \$\endgroup\$ – Bruno Costa Jan 30 '16 at 0:04
  • \$\begingroup\$ Yep, no problem. I will put some code altogether this evening :) \$\endgroup\$ – Dmitry Nogin Jan 30 '16 at 0:08
3
\$\begingroup\$

Here is a suggestion:

Pass in a Func<T, T> that does the cloning.

Then it would look something like:

public class RevertableObject<T>
    where T : class
{
    private T _oldValue;
    private T _currentValue;

    public RevertableObject(T initialValue, Func<T, T> clone)
    {
        _oldValue = clone(initialValue);
        _currentValue = initialValue;
        // minor remark here, prefer to set fields in the ctor
        // setting the property calls OnPropertyChanged which is often virtual.
        // not an issue in this case but you want to keep code analysis warnings at zero
    }
    ...
}

For cloning a common pattern is to use serialization, sample:

public class Cloner
{
    public SomeSetting Clone(SomeSetting item)
    {
        // remark: as you write WPF you probably have PropertyChanged
        // http://stackoverflow.com/a/30483796/1069200
        return CloneBinary(item);
    }

    private static T CloneBinary<T>(T original)
    {
        // using binary here but you can of course use xml or json or anything
        var bf = new BinaryFormatter();
        using (var stream = new MemoryStream())
        {
            bf.Serialize(stream, original);
            stream.Position = 0;
            return (T)bf.Deserialize(stream);
        }
    }
}

This has a couple of benefits:

  • You can have the cloning code in a separate class doing only one thing.
  • Allows for composing with any cloning logic.
  • No dependency on IClonable which is as you say a pretty useless interface. The cloning code tends to be verbose and error prone.
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.