我正在尝试修复使用以下撤消堆栈模型的MVVM应用程序的垃圾收集问题。
该示例非常简约,真实世界代码有很大不同,每个ViewModel使用工厂类撤消列表而不是单个undolist但是具有代表性:
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
}
}
}
您会看到当任何viewModel超出范围时,UndoList中的操作会保留对它们的引用。真实代码将各种视图模型分组为分组的undolists(包含子视图模型的viewModel共享相同的撤消堆栈),因此很难知道清除的时间和位置。
我想知道是否有一些方法可以使这些操作过期,如果他们是唯一一个保持对其中变量的引用的方法?
建议欢迎!
答案 0 :(得分:4)
我有一个解决方案给你。我不喜欢使用UndoList
作为单身人士,但我保留了它以便为您提供问题的直接答案。在实践中,我不会使用单身人士。
现在,您将发现很难避免在操作中捕获对视图模型的引用。如果你尝试过,它会让你的代码变得非常丑陋。最好的方法是让您的视图模型实现IDisposable
,并确保在超出范围时将其丢弃。请记住,垃圾收集器从不调用Dispose
,因此您必须这样做。
使用
IDisposable
是清理时的标准模型 不再需要实例。
因此,首先要定义的是一个帮助器类,它在处理时执行一个动作。
public sealed class AnonymousDisposable : IDisposable
{
private readonly Action _dispose;
private int _isDisposed;
public AnonymousDisposable(Action dispose)
{
_dispose = dispose;
}
public void Dispose()
{
if (Interlocked.Exchange(ref _isDisposed, 1) == 0)
{
_dispose();
}
}
}
现在我可以编写这样的内容来删除列表中的元素:
var disposable = new AnonymousDisposable(() => list.Remove(item));
稍后,当我致电disposable.Dispose()
时,该项目将从列表中删除。
现在,这是您的代码重新实现。
我已将UndoList
更改为静态类,而不是单例。如果需要,您可以将其更改回来。
public static class UndoList
{
public static bool IsUndoing { get; private set; }
private static List<Action> _undos = new List<Action>();
public static IDisposable AddUndo(Action action)
{
var disposable = (IDisposable)null;
if (!IsUndoing)
{
disposable = new AnonymousDisposable(() => _undos.Remove(action));
_undos.Add(action);
}
return disposable ?? new AnonymousDisposable(() => { });
}
public static bool Undo()
{
IsUndoing = true;
var result = _undos.Count > 0;
if (result)
{
var action = _undos[_undos.Count - 1];
_undos.Remove(action);
action();
}
IsUndoing = false;
return result;
}
}
你会注意到我用一个列表替换了堆栈。我这样做是因为我需要从列表中删除项目。
此外,您可以看到AddUndo
现在返回IDisposable
。调用代码需要保持返回的一次性,并在想要从列表中删除操作时调用Dispose
。
我还内化了Undo
行动。在视图模型中使用它没有意义。调用Undo
会有效地弹出列表中的顶部项目并执行操作并返回true
。但是,如果列表为空,则返回false
。您可以将其用于测试目的。
ViewModel
类现在看起来像这样:
public class ViewModel : IDisposable, INotifyPropertyChanged
{
public ViewModel()
{
_disposables = new List<IDisposable>();
_disposable = new AnonymousDisposable(() =>
_disposables.ForEach(d => d.Dispose()));
}
private readonly List<IDisposable> _disposables;
private readonly IDisposable _disposable;
public void Dispose()
{
_disposable.Dispose();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void AddUndo(Action action)
{ ... }
protected void SetUndoableValue<T>(Action<T> action, T newValue, T oldValue)
{ ... }
}
它实现IDisposable
并在内部跟踪一次性用品列表和匿名一次性用品,当视图模型本身被丢弃时,它将处理列表中的项目。呼!满口,但我希望这是有道理的。
AddUndo
方法正文如下:
protected void AddUndo(Action action)
{
var disposable = (IDisposable)null;
Action inner = () =>
{
_disposables.Remove(disposable);
action();
};
disposable = UndoList.AddUndo(inner);
_disposables.Add(disposable);
}
在内部调用UndoList.AddUndo
传递一个操作,该操作将在调用IDisposable
时从视图模型的撤消操作列表中删除返回的UndoList.Undo()
- 以及重要的是,实际执行行动。
因此,这意味着在处理视图模型时,将从撤消列表中删除所有未完成的撤消操作,并且在调用Undo
时,将从视图模型中删除关联的一次性。 这可确保您在处理视图模型时不会保留对视图模型的引用。
我创建了一个名为SetUndoableValue
的辅助函数,它取代了您的void AddUndo(string propertyName, object oldValue)
方法,该方法不是强类型的,可能会导致运行时错误。
protected void SetUndoableValue<T>(Action<T> action, T newValue, T oldValue)
{
this.AddUndo(() => action(oldValue));
action(newValue);
}
我将这两种方法protected
视为public
似乎过于混乱。
TestModel
或多或少相同:
public class TestModel : ViewModel
{
private bool _testProperty;
public bool TestProperty
{
get { return _testProperty; }
set
{
this.SetUndoableValue(v => _testProperty = v, value, _testProperty);
}
}
public bool HasBusinessActionBeenDone { get; set; }
public void DoBusinessAction()
{
this.AddUndo(this.inverseBusinessAction);
businessAction();
}
private void businessAction()
{
this.HasBusinessActionBeenDone = true;
}
private void inverseBusinessAction()
{
this.HasBusinessActionBeenDone = false;
}
}
最后,这是正确测试UndoList
函数的代码:
using (var vm = new TestModel())
{
Debug.Assert(UndoList.Undo() == false);
vm.TestProperty = true;
Debug.Assert(UndoList.Undo() == true);
Debug.Assert(UndoList.Undo() == false);
Debug.Assert(vm.TestProperty == false);
vm.DoBusinessAction();
Debug.Assert(UndoList.Undo() == true);
Debug.Assert(vm.HasBusinessActionBeenDone == false);
vm.DoBusinessAction();
}
Debug.Assert(UndoList.Undo() == false);
如果我能提供任何更详细的信息,请告诉我。
答案 1 :(得分:0)
如果你不能以任何其他方式清理它,你可以使用WeakReference来保存属性,但我认为会有其他问题,因为这仍然会导致一个Action实例存在并附加一个空引用。
快速浏览一下,我更倾向于使用单例来保持对模型的注册,并让模型管理附加到其上的所有撤消操作的实例列表。当模型超出范围时,调用它上面的清理方法或在其上实现IDisposable类型接口,如果这适合的话。但是,根据实施情况,您可能不需要单身人士。