Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added an internal ObservableStack collection implementing INotifyCollectionChanged for UndoRoot's Undo and Redo stacks #12

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions samples/WpfUndoSample/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,9 @@ public MainWindow()

private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
// The undo / redo stack collections are not "Observable", so we
// need to manually refresh the UI when they change.
var root = UndoService.Current[this];
root.UndoStackChanged += new EventHandler(OnUndoStackChanged);
root.RedoStackChanged += new EventHandler(OnRedoStackChanged);
FirstNameTextbox.Focus();
}

// Refresh the UI when the undo stack changes.
void OnUndoStackChanged(object sender, EventArgs e)
{
RefreshUndoStackList();
}

// Refresh the UI when the redo stack changes.
void OnRedoStackChanged(object sender, EventArgs e)
{
RefreshUndoStackList();
}


// The following 4 event handlers support the "CommandBindings" in the window.
// These hook to the Undo and Redo commands.
Expand Down Expand Up @@ -246,18 +229,6 @@ public IEnumerable<ChangeSet> RedoStack



// Refresh the UI when the undo / redo stacks change.
private void RefreshUndoStackList()
{
// Calling refresh on the CollectionView will tell the UI to rebind the list.
// If the list were an ObservableCollection, or implemented INotifyCollectionChanged, this would not be needed.
var cv = CollectionViewSource.GetDefaultView(UndoStack);
cv.Refresh();

cv = CollectionViewSource.GetDefaultView(RedoStack);
cv.Refresh();
}




Expand Down
28 changes: 0 additions & 28 deletions samples/WpfUndoSampleMVVM.Core/MainWindowViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,9 @@ private void OnSliderMouseDown(MouseButtonEventArgs e)

private void OnWindowLoaded()
{
// The undo / redo stack collections are not "Observable", so we
// need to manually refresh the UI when they change.
var root = UndoService.Current[this];
root.UndoStackChanged += new EventHandler(OnUndoStackChanged);
root.RedoStackChanged += new EventHandler(OnRedoStackChanged);
//FirstNameTextbox.Focus();
}

// Refresh the UI when the undo stack changes.
void OnUndoStackChanged(object sender, EventArgs e)
{
RefreshUndoStackList();
}

// Refresh the UI when the redo stack changes.
void OnRedoStackChanged(object sender, EventArgs e)
{
RefreshUndoStackList();
}



Expand Down Expand Up @@ -218,18 +202,6 @@ public IEnumerable<ChangeSet> RedoStack



// Refresh the UI when the undo / redo stacks change.
private void RefreshUndoStackList()
{
// Calling refresh on the CollectionView will tell the UI to rebind the list.
// If the list were an ObservableCollection, or implemented INotifyCollectionChanged, this would not be needed.
var cv = CollectionViewSource.GetDefaultView(UndoStack);
cv.Refresh();

cv = CollectionViewSource.GetDefaultView(RedoStack);
cv.Refresh();
}



private void InitialiseCommandBindings()
Expand Down
83 changes: 83 additions & 0 deletions src/MonitoredUndo/ObservableStack.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
Based on sample code posted on Stack Overflow by 'Ernie S' (https://stackoverflow.com/users/1324284/ernie-s)
at https://stackoverflow.com/questions/3127136/observable-stack-and-queue/56177896#56177896
*/
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;

namespace MonitoredUndo
{
/// <summary>
/// Observable version of the generic Stack collection
/// </summary>
internal class ObservableStack<T> : Stack<T>, INotifyCollectionChanged, INotifyPropertyChanged
{
#region Constructors

public ObservableStack() : base()
{
}

public ObservableStack(IEnumerable<T> collection) : base(collection)
{
}

public ObservableStack(int capacity) : base(capacity)
{
}

#endregion

#region Overrides

public new virtual T Pop()
{
T item = base.Pop();
OnCollectionChanged(NotifyCollectionChangedAction.Remove, item);

return item;
}

public new virtual void Push(T item)
{
base.Push(item);
OnCollectionChanged(NotifyCollectionChangedAction.Add, item);
}

public new virtual void Clear()
{
base.Clear();
OnCollectionChanged(NotifyCollectionChangedAction.Reset, default);
}

#endregion

#region CollectionChanged

public virtual event NotifyCollectionChangedEventHandler CollectionChanged;

protected virtual void OnCollectionChanged(NotifyCollectionChangedAction action, T item)
{
CollectionChanged?.Invoke(this, new NotifyCollectionChangedEventArgs(action,
item,
item == null ? -1 : 0)
);

OnPropertyChanged(nameof(Count));
}

#endregion

#region PropertyChanged

public virtual event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string proertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(proertyName));
}

#endregion
}
}
8 changes: 4 additions & 4 deletions src/MonitoredUndo/UndoRoot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ public class UndoRoot
private WeakReference _Root;

// The list of undo / redo actions.
private Stack<ChangeSet> _UndoStack;
private Stack<ChangeSet> _RedoStack;
private ObservableStack<ChangeSet> _UndoStack;
private ObservableStack<ChangeSet> _RedoStack;

// Tracks whether a batch (or batches) has been started.
private int _IsInBatchCounter = 0;
Expand Down Expand Up @@ -53,8 +53,8 @@ public class UndoRoot
public UndoRoot(object root)
{
_Root = new WeakReference(root);
_UndoStack = new Stack<ChangeSet>();
_RedoStack = new Stack<ChangeSet>();
_UndoStack = new ObservableStack<ChangeSet>();
_RedoStack = new ObservableStack<ChangeSet>();
}


Expand Down
127 changes: 127 additions & 0 deletions tests/MonitoredUndo.Tests/UndoTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using Microsoft.VisualStudio.TestTools.UnitTesting;
Expand Down Expand Up @@ -306,6 +307,132 @@ public void UndoRoot_Raises_RedoStackChanged_Event_When_ChangeSet_Added()
}
}

[TestMethod]
public void UndoRoot_UndoStack_Implements_INotifyCollectionChanged()
{
string orig = Document1.A.Name;
var undoRoot = UndoService.Current[Document1];
Assert.IsInstanceOfType(undoRoot.UndoStack, typeof(INotifyCollectionChanged));

bool wasCalledForAdd = false;
bool wasCalledForRemove = false;
bool wasCalledForReset = false;

var callback = new NotifyCollectionChangedEventHandler((s, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
wasCalledForAdd = true;
break;
case NotifyCollectionChangedAction.Remove:
wasCalledForRemove = true;
break;
case NotifyCollectionChangedAction.Reset:
wasCalledForReset = true;
break;
}
});

((INotifyCollectionChanged)undoRoot.UndoStack).CollectionChanged += callback;

try
{
Document1.A.Name = "Updated1";
Assert.IsTrue(wasCalledForAdd);
Assert.IsFalse(wasCalledForRemove);
Assert.IsFalse(wasCalledForReset);

wasCalledForAdd = false;

undoRoot.Undo();
Assert.IsTrue(wasCalledForRemove);
Assert.IsFalse(wasCalledForAdd);
Assert.IsFalse(wasCalledForReset);

wasCalledForRemove = false;

undoRoot.Redo();
Assert.IsTrue(wasCalledForAdd);
Assert.IsFalse(wasCalledForRemove);
Assert.IsFalse(wasCalledForReset);

wasCalledForAdd = false;

undoRoot.Clear();
Assert.IsTrue(wasCalledForReset);
Assert.IsFalse(wasCalledForAdd);
Assert.IsFalse(wasCalledForRemove);
}
finally
{
((INotifyCollectionChanged)undoRoot.UndoStack).CollectionChanged -= callback;
}
}

[TestMethod]
public void UndoRoot_RedoStack_Implements_INotifyCollectionChanged()
{
string orig = Document1.A.Name;
var undoRoot = UndoService.Current[Document1];
Assert.IsInstanceOfType(undoRoot.RedoStack, typeof(INotifyCollectionChanged));

bool wasCalledForAdd = false;
bool wasCalledForRemove = false;
bool wasCalledForReset = false;

var callback = new NotifyCollectionChangedEventHandler((s, e) =>
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
wasCalledForAdd = true;
break;
case NotifyCollectionChangedAction.Remove:
wasCalledForRemove = true;
break;
case NotifyCollectionChangedAction.Reset:
wasCalledForReset = true;
break;
}
});

((INotifyCollectionChanged)undoRoot.RedoStack).CollectionChanged += callback;

try
{
Document1.A.Name = "Updated1";
Assert.IsTrue(wasCalledForReset, "Redo stack CollectionChanged event should have been called with NotifyCollectionChangedAction.Reset args because of the new change pruning the redo stack.");
Assert.IsFalse(wasCalledForAdd);
Assert.IsFalse(wasCalledForRemove);

wasCalledForReset = false;

undoRoot.Undo();
Assert.IsTrue(wasCalledForAdd);
Assert.IsFalse(wasCalledForRemove);
Assert.IsFalse(wasCalledForReset);

wasCalledForAdd = false;

undoRoot.Redo();
Assert.IsTrue(wasCalledForRemove);
Assert.IsFalse(wasCalledForAdd);
Assert.IsFalse(wasCalledForReset);

wasCalledForRemove = false;

undoRoot.Clear();
Assert.IsTrue(wasCalledForReset);
Assert.IsFalse(wasCalledForAdd);
Assert.IsFalse(wasCalledForRemove);
}
finally
{
((INotifyCollectionChanged)undoRoot.RedoStack).CollectionChanged -= callback;
}
}

[TestMethod]
public void UndoRoot_Can_Undo_the_Last_ChangeSet()
{
Expand Down