У меня есть решение для вас.Мне не нравится использование 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)
, который не былt строго напечатан и может привести к ошибкам во время выполнения.
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);
Пожалуйста, дайте мне знать, если смогупредоставить более подробную информацию о чем-либо.