Боковая панель

Отмена и возврат изменений модели

В программном комплексе Топоматик Робур предусмотрена история изменений произошедших во время работы программы с возможностью отмены этих изменений при необходимости.

Классы, используемые для реализации моделей с поддержкой истории изменений расположены в пространстве имён Topomatic.FoundationClasses.Undo.

Для реализации поддержки истории изменений в модели необходимо:

Если необходимо отслеживать изменение значения полей или содержимого списков, то вместо BaseTransactableField<T> и BaseTransactableList<T> нужно использовать TransactableField<T> и TransactableList<T>. Эти классы поддерживают события позволяющие следить за изменением своего содержимого.

Содержащие поля данных, списки и словари объекты должны поддерживать интерфейс ITransactable.

Для совершения изменений модели необходимо выполнить следующую последовательность действий:

  • Вызвать у родительского объекта, в котором происходят изменения, метод BeginUpdate перед началом изменений.
  • Произвести необходимые изменения используя Value у BaseTransactableField<T> или изменив содержимое контейнеров BaseTransactableList<T> и TransactableDictionary<T, U>
  • Вызвать у родительского объекта, в котором происходят изменения, метод EndUpdate после завершения изменений.
При изменении только одного поля BaseTransactableField<T> или одного элемента контейнеров BaseTransactableList<T> и TransactableDictionary<T, U> допустимо не вызывать методы BeginUpdate и EndUpdate. В этом случае они будут вызваны автоматически перед началом и в конце изменения соответственно.

Для работы истории изменений, необходимо чтобы свойству TransactionManager интерфейса ITransactable был назначен корректный экземпляр интерфейса ITransactionManager. Для этого все основные объекты программного комплекса Топоматик Робур наследуются от базового класса UpdatableObject и поддерживают интерфейс IOwned.

Если необходимо отслеживать изменение объекта, при изменении каких либо дочерних элементов, то необходимо наследоваться от UndoObject. Это наследник от UpdatableObject в котором дополнительно поддерживаются события Changed и Undo

Класс UpdatableObject отвечает за реализацию интерфейса ITransactable, а интерфейс IOwned позволяет реализовать свойство TransactionManager запрашивая его по цепочке родителей с самого верхнего. В этом случае экземпляр TransactionManager назначается самому верхнему элементу в цепочке, обычно это наследник от StateControllerObject, а все дочерние элементы получают его автоматически.

Часто возникает ситуация, когда в модель необходимо внести какие либо изменения, а потом либо отменить внесенные изменения без записи в истории либо применить их. В таких случаях перед началом изменений нужно вызвать метод BeginTransaction а в конце в либо метод Commit чтобы применить изменения, либо метод Rollback для сброса изменений

Создайте новую модель и видовой слой и подключите к программному комплексу Топоматик Робур.

Измените класс Points.cs. Сделаем его наследником от UpdatableObject и поддержим интерфейс IOwned, а список сохраняющий значения точек заменяем на BaseTransactableList<T>.

    //Класс для хранения точек в нашей модели
    //поддерживаем интерфейсы IStgSerializable для сохранения и IOwned для определения владельца списка точек
    //Также поддерживаем интерфейс IObjectDisjoiner для привязок
    class Points : UpdatableObject, IStgSerializable, IOwned, IObjectDisjoiner
    {
        //Поддержка цепочки родителей
        private Model m_Owner;
 
        //Список структур с поддержкой истории изменений
        private TransactableList<Vector2D> m_Points;
 
        public Points(Model owner)
        {
            m_Owner = owner;
            m_Points = new TransactableList<Vector2D>(this);
        }
 
        //Метод для добавления точки в список
        public void AddPoint(Vector2D point)
        {
            m_Points.Add(point);
        }
 
        //Метод для удаления точки из списка
        public void RemovePoint(int index)
        {
            m_Points.RemoveAt(index);
        }
 
        //Метод для получения точки из списка
        public Vector2D this[int index]
        {
            get
            {
                return m_Points[index];
            }
            set
            {
                m_Points[index] = value;
            }
        }
 
        //Количество точек в списке
        //Свойство декорировано атрибутом DisplayName для отображения в инспекторе объектов
        [DisplayName("Количество точек")]
        public int Count
        {
            get
            {
                return m_Points.Count;
            }
        }
 
        //Владелец нашего списка точек, у нас это наша модель
        //Свойство декорировано атрибутом Browsable чтобы исключить отображение в инспекторе объектов
        [Browsable(false)]
        public object Owner
        {
            get
            {
                return m_Owner;
            }
            set
            {
                //Владелец назначается один раз на конструкторе объекта, назначение отдельно недопустимо
                throw new NotSupportedException();
            }
        }
 
        //Реализация загрузки
        public void LoadFromStg(StgNode node)
        {
            //обратите внимание, загрузку мы производим во внутренний список
            //это позволяет избежать записи значений в историю изменений
            m_Points.InnerList.Clear();
            //При загрузке массива указывается тип составляющих массив значений
            var array = node.GetArray("Values", StgType.Node);
            for (int i = 0; i < array.Count; i++)
            {
                m_Points.InnerList.Add(Vector2D.LoadFromStg(array.GetNode(i)));
            }
        }
 
        //Реализация сохранения
        public void SaveToStg(StgNode node)
        {
            //Сохраняем значения в узел
            //Сохраняем массив с указанием типа значений
            var array = node.AddArray("Values", StgType.Node);
            for (int i = 0; i < m_Points.Count; i++)
            {
                m_Points[i].SaveToStg(array.AddNode());
            }
        }
 
        //Дополнительно перекрываем метод ToString() для отображения типа объекта в инстпекторе объектов
        public override string ToString()
        {
            return "Точки модели";
        }
 
        //Привязка  КОНЕЧНАЯ ТОЧКА
        public void GetEndPoint(ObjectsDisjointerArgs e, IList<Vector3D> list)
        {
            for (int i = 0; i < m_Points.Count; i++)
            {
                list.Add(m_Points[i]);
            }
        }
 
        //Привязка  ЦЕНТРАЛЬНАЯ ТОЧКА
        public void GetCenterPoint(ObjectsDisjointerArgs e, IList<Vector3D> list)
        {
            //Do nothing
        }
 
        //Привязка  СЕРЕДИНА ОТРЕЗКА
        public void GetMiddlePoint(ObjectsDisjointerArgs e, IList<Vector3D> list)
        {
            for (int i = 1; i < m_Points.Count; i++)
            {
                list.Add((m_Points[i - 1] + m_Points[i]) * 0.5);
            }
        }
 
        //Привязка  УЗЕЛ
        public void GetNodePoint(ObjectsDisjointerArgs e, IList<Vector3D> list)
        {
            //Do nothing
        }
 
        //Привязка  КВАДРАНТ
        public void GetQuadrantPoint(ObjectsDisjointerArgs e, IList<Vector3D> list)
        {
            //Do nothing
        }
 
        //Привязка  ТОЧКА ВСТАВКИ
        public void GetInsertionPoint(ObjectsDisjointerArgs e, IList<Vector3D> list)
        {
            //Do nothing
        }
 
        //Привязка  БЛИЖАЙШАЯ ТОЧКА, КАСАТЕЛЬНАЯ и т.п.
        public void GetSegments(ObjectsDisjointerArgs e, IList<ArcSegment> arcList, IList<LineSegment> lineList)
        {
            for (int i = 1; i < m_Points.Count; i++)
            {
                lineList.Add( new LineSegment() { StartPoint = m_Points[i - 1], EndPoint = m_Points[i] } );
            }
        }
    }

Также изменяем Model.cs. Список содержащий экземпляры класса Points заменяем на BaseTransactableList<T>

class Model : StateControllerObject, IStgSerializable
    {
        private bool m_ReadOnly = false;
 
        //Список объектов точек с поддержкой истории изменений
        private BaseTransactableList<Points> m_Points;
 
        public Model()
        {
            m_Points = new BaseTransactableList<Points>(this);
        }
 
        //Метод для добавления списка точек в модель
        public Points Add()
        {
            var p = new Points(this);
            m_Points.Add(p);
            return p;
        }
 
        //Метод для получения списка точек из модели
        public Points this[int index]
        {
            get
            {
                return m_Points[index];
            }
        }
 
        //Метод для удаления списка точек из модели
        public bool Remove(Points points)
        {
            return m_Points.Remove(points);
        }
 
        //Количество списков в модели
        public int Count
        {
            get
            {
                return m_Points.Count;
            }
        }
 
        //флаг только для чтения
        public override bool ReadOnly
        {
            get
            {
                return m_ReadOnly;
            }
 
            set
            {
                m_ReadOnly = value;
            }
        }
 
        //Загрузка из узла
        public void LoadFromStg(StgNode node)
        {
            m_Points.InnerList.Clear();
            //При загрузке массива указывается тип составляющих массив значений
            var array = node.GetArray("Points", StgType.Node);
            for (int i = 0; i < array.Count; i++)
            {
                var p = new Points(this);
                p.LoadFromStg(array.GetNode(i));
                m_Points.InnerList.Add(p);
            }
        }
 
        //Сохранение в узел
        public void SaveToStg(StgNode node)
        {
            //Сохраняем значения в узел
            //Сохраняем массив с указанием типа значений
            var array = node.AddArray("Points", StgType.Node);
            for (int i = 0; i < m_Points.Count; i++)
            {
                m_Points[i].SaveToStg(array.AddNode());
            }
        }
    }

Также вносим изменения в Module.cs в функцию редактирования модели. Теперь перед изменением значений мы вызываем метод BeginUpdate а после метод EndUpdate. Кроме того теперь нет необходимости выставлять свойство Modified вручную, при изменении истории значение свойства определяется автоматически.

        [cmd("edit_pointsmodel")]
        public void EditModel()
        {
            var cadView = CadView;
            if (cadView != null)
            {
                var model_layer = ModelLayer.GetModelLayer(cadView);
                if (model_layer != null)
                {
                    //список выбранных точек
                    var positions = new List<Vector2D>();
                    //делегат для динамической отрисовки
                    DrawCursorEvent dynamic_draw = delegate (CadPen pen, Vector3D vertex)
                    {
                        //если есть точки в списке, нужно их нарисовать и нарисовать линию
                        //от последней выбранной точки, до текущего положения курсора
                        if (positions.Count > 0)
                        {
                            pen.Color = Color.Lime;
                            pen.BeginDraw();
                            try
                            {
                                for (int i = 1; i < positions.Count; i++)
                                {
                                    pen.DrawLine(positions[i - 1], positions[i]);
                                }
                                pen.DrawLine(positions[positions.Count - 1], vertex.Pos);
                            }
                            finally
                            {
                                pen.EndDraw();
                            }
                        }
                    };
                    //подписываемся на событие отрисовки
                    cadView.DynamicDraw += dynamic_draw;
                    try
                    {
                        Vector3D pos;
                        //просим пользователя указать несколько точек
                        while (CadCursors.GetPoint(cadView, out pos, "Укажите точку"))
                        {
                            positions.Add(pos.Pos);
                        }
                        //если точки заданы, то изменяем нашу модель
                        if (positions.Count > 0)
                        {
                            //получаем её со слоя
                            var model = model_layer.Model;
                            //начинаем групповое изменение модели
                            model.BeginUpdate();
                            try
                            {
                                //добавляем новые точки
                                var points = model.Add();
                                for (int i = 0; i < positions.Count; i++)
                                {
                                    points.AddPoint(positions[i]);
                                }
                            }
                            finally
                            {
                                //заканчиваем групповое изменение модели
                                model.EndUpdate();
                            }
                            //обновляем видовой экран
                            cadView.Unlock();
                            cadView.Invalidate();
                        }
                    }
                    finally
                    {
                        //отписываемся от события отрисовки
                        cadView.DynamicDraw -= dynamic_draw;
                    }
                }
            }
        }

В результате мы получим возможность использовать историю изменений и отменять изменения в нашей модели с помощью этого механизма.

Исходный код примера расположен в проекте «tutorial10».
developers/tutorial/undoredo.txt · Последние изменения: 2022/03/15 19:16 — proxor