I’m trying to fix a garbage collection problem of a MVVM application which uses the following model of Undo stack.
The example is very minimalistic and real world code is much different, uses a factory class of undo lists per ViewModel instead of a single undolist but is representative:
using System;
using System.Collections.Generic;
using System.Text;
using System.Diagnostics;
using System.Reflection;
using System.ComponentModel;
using System.Linq;
namespace ConsoleApplication9
{
public class UndoList
{
public bool IsUndoing { get; set; }
private Stack<Action> _undo = new Stack<Action>();
public Stack<Action> Undo
{
get { return _undo; }
set { _undo = value; }
}
private static UndoList _instance;
// singleton of the undo stack
public static UndoList Instance
{
get
{
if (_instance == null)
{
_instance = new UndoList();
}
return _instance;
}
}
}
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
// execute the last undo operation
public void Undo()
{
UndoList.Instance.IsUndoing = true;
var action = UndoList.Instance.Undo.Pop();
action();
UndoList.Instance.IsUndoing = false;
}
// push an action into the undo stack
public void AddUndo(Action action)
{
if (UndoList.Instance.IsUndoing) return;
UndoList.Instance.Undo.Push(action);
}
// create push an action into the undo stack that resets a property value
public void AddUndo(string propertyName, object oldValue)
{
if (UndoList.Instance.IsUndoing) return;
var property = this.GetType().GetProperties().First(p => p.Name == propertyName);
Action action = () =>
{
property.SetValue(this, oldValue, null);
};
UndoList.Instance.Undo.Push(action);
}
}
public class TestModel : ViewModel
{
private bool _testProperty;
public bool TestProperty
{
get
{
return _testProperty;
}
set
{
base.AddUndo("TestProperty", _testProperty);
_testProperty = value;
}
}
// mock property indicating if a business action has been done for test
private bool _hasBusinessActionBeenDone;
public bool HasBusinessActionBeenDone
{
get
{
return _hasBusinessActionBeenDone;
}
set
{
_hasBusinessActionBeenDone = value;
}
}
public void DoBusinessAction()
{
AddUndo(() => { inverseBusinessAction(); });
businessAction();
}
private void businessAction()
{
// using fake property for brevity of example
this.HasBusinessActionBeenDone = true;
}
private void inverseBusinessAction()
{
// using fake property for brevity of example
this.HasBusinessActionBeenDone = false;
}
}
class Program
{
static void Test()
{
var vm = new TestModel();
// test undo of property
vm.TestProperty = true;
vm.Undo();
Debug.Assert(vm.TestProperty == false);
// test undo of business action
vm.DoBusinessAction();
vm.Undo();
Debug.Assert(vm.HasBusinessActionBeenDone == false);
// do it once more without Undo, so the undo stack has something
vm.DoBusinessAction();
}
static void Main(string[] args)
{
Program.Test();
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced);
// at this point UndoList.Instance.Undo
// contains an Action which references the TestModel
// which will never be collected...
// in real world code knowing when to clear this is a problem
// because it is a singleton factory class for undolists per viewmodel type
// ideally would be to clear the list when there are no more references
// to the viewmodel type in question, but the Actions in the list prevent that
}
}
}
You see that when any viewModel goes out of scope the actions in the UndoList keep references to them. The real code groups various viewmodels into grouped undolists (viewModels that contain child viewmodels share the same undo stack), so it is difficult to know when and where to put the clearing.
I was wondering if there is some method to make those actions expire if they are the only one keeping references to the variables inside them?
Suggestions welcome!
I’ve got a solution for you. I don’t like the use of the
UndoListas a singleton, but I’ve kept it to provide you with a direct answer to your question. In practice I wouldn’t use a singleton.Now, you will find it very difficult to avoid capturing references to your view models in your actions. It would make your code very ugly if you tried. The best approach is to make your view models implement
IDisposableand make sure that you dispose of them when they go out of scope. Remember that the garbage collector never callsDisposeso you must.So the first thing to define is a helper class that executes an action when it is disposed.
Now I can write things like this to remove elements from lists:
Later, when I call
disposable.Dispose()the item is removed from the list.Now here’s your code re-implemented.
I’ve changed
UndoListto be a static class, not a singleton. You can change it back if need be.You’ll notice that I’ve replaced the stack with a list. I did that because I need to remove items from inside the list.
Also, you can see that
AddUndonow returns anIDisposable. Calling code needs to keep the return disposable and callDisposewhen it wants to remove the action from the list.I’ve also internalized the
Undoaction. It didn’t make sense to have it in the view model. CallingUndoeffectively pops the top item off of the list and executes the action and returnstrue. However, if the list is empty it returnsfalse. You can use this for testing purposes.The
ViewModelclass now looks like this:It implements
IDisposableand internally, keeps track of a list of disposables and an anonymous disposable that will dispose of the items in the list when the view model itself is disposed of. Whew! A mouthful, but I hope that makes sense.The
AddUndomethod body is this:Internally it calls
UndoList.AddUndopassing in an action that will remove the returnedIDisposablefrom the view model’s list of undo actions whenUndoList.Undo()is called – as well as, importantly, actually executing the action.So this means that when the view model is disposed all of its outstanding undo actions are removed from the undo list and when
Undois called the associated disposable is removed from the view model. And this ensures that you are not keeping references to the view model when it is disposed of.I created a helper function called
SetUndoableValuethat replaced yourvoid AddUndo(string propertyName, object oldValue)method which wasn’t strongly-typed and could cause you to have run-time errors.I made both of these methods
protectedaspublicseemed too promiscuous.The
TestModelis more-or-less the same:And finally, here’s the code that tests the
UndoListfunctions correctly:Please let me know if I can provide any more detail on anything.