Undo from one level

Jul 14, 2012 at 5:15 PM
Edited Jul 14, 2012 at 5:16 PM

It is a nice Undo system, easy to use. But there is a hard time for me to undo a modification in a collection. 

Main window has a Players collection, I want the undo could undo item added or removed from the collection , and also modifications of an item.

Could you show me how to do it?  With current code it only can handle add or remove. Modifications of an item doesn't work.

Thanks

 

namespace MonitoredUndo_Test {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, ISupportsUndo {
        public ObservableCollection<Player> Players { get; set; }

        public MainWindow() {
            InitializeComponent();
            Players = new ObservableCollection<Player>();
            
            Players.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Players_CollectionChanged);
            
        }

        void Players_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
            
            DefaultChangeFactory.OnCollectionChanged(this, "Players", this.Players, e, "Collection of Players' Changed");
        }

        private void Button_Click(object sender, RoutedEventArgs e) {
            //Want to use this button to undo modification of an item or Added/Remove of a Collection.
            var undoRoot = UndoService.Current[this];  
            undoRoot.Undo();
        }

        private void Button_Click_1(object sender, RoutedEventArgs e) {
            var item = Players.FirstOrDefault(i => i.FirstName == "1");
            if (item != null) {
                item.LastName = "Smith";
            }
            
        }
        public object GetUndoRoot() {
            return this;
        }

        private void Window_Loaded(object sender, RoutedEventArgs e) {
            Players.Add(new Player() { FirstName = "1", LastName = "A" });
            Players.Add(new Player() { FirstName = "2", LastName = "B" });
            Players.Add(new Player() { FirstName = "3", LastName = "C" });
            Players.Add(new Player() { FirstName = "4", LastName = "D" });
        }

    }
}
Coordinator
Jul 17, 2012 at 8:54 PM

Hi @sharethl,

Thanks for trying out the framework. It looks like you setup the collection undo logic well. This is the correct idea for collections.

For individual class instances, you'll need to add some logic to the property setters so that the class can tell MUF about the change to that property. There is a method on the DefaultChangeFactory for use inside the property setters. You just pass in the instance, the name of the property, the previous value, and the new value. That will then register an undo-able action in the undo stack.

I'd suggest taking a look at the RootDocument and Child classes in the unit tests for an example of how to put these calls into the property setters.

Let me know if you have any further questions, or if the unit tests don't answer your questions.

- Nathan

Jul 18, 2012 at 12:37 AM
Edited Jul 18, 2012 at 12:38 AM

Hi nallenwagner

Thanks for your replying. I can apply undo to individual class already. Problem is how to access to individual's undo in from MainWindow. 

Let's use an example: 

MainWindow has a collection of Player

Player has a property "Name", implement muf. Here is pseudo code.

 

Class MainWindow{
	Test(){
	   Player P = new Player();
	   P.Name="John";
	   P.Name="Lin"
	   Players.Add(P);
	   var undoRoot = UndoService.Current[this];  
		undoRoot.Undo();
	}
}

 

When test() is been call, the undoRoot only can remove P from collection.

But I hope it could change P.Name to "John" first, then one more undo call could remove it from list.

So I am thinking this kind of code works or not, just for thoughts, actually no UndoObject in Root:

	Test1(){
	var undoRoot = UndoService.Current[this];  
	var ObjectToDelete= undoRoot.UndoObject;
	if (ObjectToDelete.undoable==true){
		var undoRoot=UndoService.Current[ObjectToDelete];
	}
	undoRoot.Undo();
	}
Coordinator
Jul 18, 2012 at 1:57 PM

Hi @sharethl,

I think you'll want to do a couple things:

1) Alter the player class to accept an undo token (or access to the "parent" object, if that is preferred)

public class Player : ISupportsUndo {
    
    private object UndoToken;
    
    public Player(object undoToken) {
        this.UndoToken = undoToken;
    }

    public object GetUndoRoot()
    {
        return UndoToken;
    }    

    ...
}

2) Alter your Player class so that in your setter for the Name property, you do this:

        private string _Name;
        public string Name
        {
            get { return _Name; }
            set
            {
                if (value == _Name)
                    return;

                // This line will log the property change with the undo framework.
                DefaultChangeFactory.OnChanging(this, "Name", _Name, value);

                _Name = value;
                OnPropertyChanged("Name");
            }
        }

 

3) Provide a common "token" to represent the undo stack.

namespace MonitoredUndo_Test {
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, ISupportsUndo {

        // Changes are grouped together into a separate "stack" 
        // that is accessible using a token. Using a single token will
        // put all changes on a single stack. Using multiple tokens
        // will put changes on separate undo stacks.
        private object undoToken = new object();

        public ObservableCollection<Player> Players { get; set; }

        public MainWindow() {
            InitializeComponent();
            Players = new ObservableCollection<Player>();
            
            Players.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(Players_CollectionChanged);
            
        }

        void Players_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
            
            DefaultChangeFactory.OnCollectionChanged(this, "Players", this.Players, e, "Collection of Players' Changed");
        }

        private void Button_Click(object sender, RoutedEventArgs e) {
            //Want to use this button to undo modification of an item or Added/Remove of a Collection.
            var undoRoot = UndoService.Current[this];  
            undoRoot.Undo();
        }

        private void Button_Click_1(object sender, RoutedEventArgs e) {
            var item = Players.FirstOrDefault(i => i.FirstName == "1");
            if (item != null) {
                item.LastName = "Smith";
            }
            
        }
        public object GetUndoRoot() {
            return undoToken;
        }

        private void Window_Loaded(object sender, RoutedEventArgs e) {
            Players.Add(new Player(undoToken) { FirstName = "1", LastName = "A" });
            Players.Add(new Player(undoToken) { FirstName = "2", LastName = "B" });
            Players.Add(new Player(undoToken) { FirstName = "3", LastName = "C" });
            Players.Add(new Player(undoToken) { FirstName = "4", LastName = "D" });
        }

    }
}

The general idea is that you want to monitor for the changes to the name property and then inject those into the undo stack. There can be one or more undo stacks, so if you want all changes in a single stack, you'll need a common "token" when getting the undo root. (ie UndoService.Current[token]). This token is what your classes need to return via the "ISupportsUndo.GetUndoRoot()" method.

Does that help you achieve your goal? If not, please let me know.

- Nathan

Jul 18, 2012 at 5:46 PM

The undo token method works!

Really appreciated for your help. By the way, is this method mentioned in Test Sample?

It may be helpful to put into document.

Coordinator
Jul 18, 2012 at 5:54 PM

Glad to help and glad it worked for you! Let me know if you have any more questions.

I'll take a look at the docs and see if I can make this part a bit clearer and simpler.

- Nathan

Sep 23, 2013 at 6:08 PM
Edited Sep 23, 2013 at 6:17 PM
Thanks for a very good approach and implementation of an important functionality. I am developing a MVVM Light application and am now able to Undo/Redo across multiple viewmodels based on the Token approach described above. What I am struggling with now is a way to undo all of my changes in one operation using the UndoBatch approach mentioned in the documentation. For "single" undo I have this in my MainViewMode, and it works flawlessly:
        private void UndoDelete()
        {
            var undoRoot = UndoService.Current[UndoToken];
            undoRoot.Undo();         
        }
When I debug I see that I have a nice collection of ChangeSets in my UndoStack on my UndoToken. So (among a lots of tries) I created this method:
        private void UndoAllDelete()
        {
            var undoRoot = UndoService.Current[UndoToken];
            // TODO: Create UndoBatch to Undo all changes in one operation
            using (new UndoBatch(undoRoot, "Batch Undo", false))
            {
                UndoToken = undoRoot.UndoStack;
            }
            undoRoot.Undo();
        }
But, alas, it does not work. Together with all my other attempts either ... :-P

I have a workaround - kind of ... :
    private void UndoAllDelete()
    {
        var undoRoot = UndoService.Current[UndoToken];
        // TODO: Create UndoBatch to Undo all changes in one operation
        var changes = undoRoot.UndoStack.Count();
        int i = 0;
        while (i < changes)
        {
            undoRoot.Undo();
            i++;
        }
    }
It is very uncool, I admit, and I would like some advice on how to get the UndoBatch approach to work. Any suggestions?

-jorgen