File: System\ComponentModel\Design\UndoEngine.UndoUnit.cs
Web Access
Project: src\src\System.Windows.Forms.Design\src\System.Windows.Forms.Design.csproj (System.Windows.Forms.Design)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections;
using System.Reflection;
 
namespace System.ComponentModel.Design;
 
public abstract partial class UndoEngine
{
    /// <summary>
    ///  This class embodies a unit of undoable work. The undo engine creates an undo unit when a change to
    ///  the designer is about to be made. The undo unit is responsible for tracking changes. The undo engine will
    ///  call Close on the unit when it no longer needs to track changes.
    /// </summary>
    protected partial class UndoUnit
    {
        private List<UndoEvent>? _events; // the list of events we've captured
        private List<ChangeUndoEvent>? _changeEvents; // the list of change events we're currently capturing. Only valid until Commit is called.
        private List<AddRemoveUndoEvent>? _removeEvents; // the list of remove events we're currently capturing. Only valid until a matching Removed is encountered.
        private List<IComponent>? _ignoreAddingList; // the list of objects that are currently being added. We ignore change events between adding and added.
        private List<IComponent>? _ignoreAddedList; // the list of objects that are added. We do not serialize before state for change events that happen in the same transaction
        private bool _reverse; // if true, we walk the events list from the bottom up
        private readonly Dictionary<string, IContainer>? _lastSelection; // the selection as it was before we gathered undo info
 
        public UndoUnit(UndoEngine engine, string? name)
        {
            Name = name ?? string.Empty;
 
            UndoEngine = engine.OrThrowIfNull();
            _reverse = true;
            if (UndoEngine.TryGetService(out ISelectionService? ss))
            {
                ICollection selection = ss.GetSelectedComponents();
                Dictionary<string, IContainer> selectedNames = [];
                foreach (object sel in selection)
                {
                    if (sel is IComponent { Site: ISite site })
                    {
                        selectedNames[site.Name!] = site.Container!;
                    }
                }
 
                _lastSelection = selectedNames;
            }
        }
 
        public string Name { get; }
 
        /// <summary>
        ///  This returns true if the undo unit has nothing in it to undo. The unit will be discarded.
        /// </summary>
        public virtual bool IsEmpty => _events is null || _events.Count == 0;
 
        protected UndoEngine UndoEngine { get; }
 
        /// <summary>
        ///  Adds the given event to our event list.
        /// </summary>
        private void AddEvent(UndoEvent e)
        {
            _events ??= [];
 
            _events.Add(e);
        }
 
        /// <summary>
        ///  Called by the undo engine when it wants to close this unit. The unit should do any final work it needs to do to close.
        /// </summary>
        public virtual void Close()
        {
            if (_changeEvents is not null)
            {
                foreach (ChangeUndoEvent e in _changeEvents)
                {
                    e.Commit();
                }
            }
 
            if (_removeEvents is not null)
            {
                foreach (AddRemoveUndoEvent e in _removeEvents)
                {
                    e.Commit();
                }
            }
 
            // At close time we are done with this list. All change events were simultaneously added to the _events list.
            _changeEvents = null;
            _removeEvents = null;
            _ignoreAddingList = null;
            _ignoreAddedList = null;
        }
 
        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component added event.
        /// </summary>
        public virtual void ComponentAdded(ComponentEventArgs e)
        {
            if (e.Component!.Site?.Container is INestedContainer)
            {
                // do nothing
            }
            else
            {
                AddEvent(new AddRemoveUndoEvent(UndoEngine, e.Component, true));
            }
 
            _ignoreAddingList?.Remove(e.Component);
 
            _ignoreAddedList ??= [];
 
            _ignoreAddedList.Add(e.Component);
        }
 
        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component adding event.
        /// </summary>
        public virtual void ComponentAdding(ComponentEventArgs e)
        {
            _ignoreAddingList ??= [];
 
            _ignoreAddingList.Add(e.Component!);
        }
 
        private static bool ChangeEventsSymmetric(
            [NotNullWhen(true)] ComponentChangingEventArgs? changing,
            [NotNullWhen(true)] ComponentChangedEventArgs? changed)
        {
            if (changing is null || changed is null)
            {
                return false;
            }
 
            return changing.Component == changed.Component && changing.Member == changed.Member;
        }
 
        private bool CanRepositionEvent(int startIndex, ComponentChangedEventArgs e)
        {
            bool containsAdd = false;
            bool containsRename = false;
            bool containsSymmetricChange = false;
            for (int i = startIndex + 1; i < _events!.Count; i++)
            {
                if (_events[i] is AddRemoveUndoEvent addEvt && !addEvt.NextUndoAdds)
                {
                    containsAdd = true;
                }
                else if (_events[i] is ChangeUndoEvent changeEvt && ChangeEventsSymmetric(changeEvt.ComponentChangingEventArgs, e))
                {
                    containsSymmetricChange = true;
                }
                else if (_events[i] is RenameUndoEvent)
                {
                    containsRename = true;
                }
            }
 
            return containsAdd && !containsRename && !containsSymmetricChange;
        }
 
        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component changed event.
        /// </summary>
        public virtual void ComponentChanged(ComponentChangedEventArgs e)
        {
            if (_events is not null && e is not null)
            {
                for (int i = 0; i < _events.Count; i++)
                {
                    // Determine if we've located the UndoEvent which was created as a result of a corresponding ComponentChanging event.
                    // If so, reposition to the "Changed" spot in the list if the following is true:
                    //          - It must be for a DSV.Content property
                    //          - There must be a AddEvent between the Changing and Changed
                    //          - There are no renames in between Changing and Changed.
                    if (_events[i] is ChangeUndoEvent ce && ChangeEventsSymmetric(ce.ComponentChangingEventArgs, e) && i != _events.Count - 1)
                    {
                        if (e.Member is not null && e.Member.Attributes.Contains(DesignerSerializationVisibilityAttribute.Content) &&
                            CanRepositionEvent(i, e))
                        {
                            _events.RemoveAt(i);
                            _events.Add(ce);
                        }
                    }
                }
            }
        }
 
        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component changing event.
        /// </summary>
        public virtual void ComponentChanging(ComponentChangingEventArgs e)
        {
            // If we are in the process of adding this component, ignore any changes to it.
            // The ending "Added" event will capture the component's state. This not just an optimization.
            // If we get a change during an add, we can have an undo order that specifies a remove,
            // and then a change to a removed component.
            if (_ignoreAddingList is not null && _ignoreAddingList.Contains(e.Component))
            {
                return;
            }
 
            _changeEvents ??= [];
 
            // The site check here is done because the data team is calling us for components that are not yet sited.
            // We end up writing them out as Guid-named locals.
            // That's fine, except that we cannot capture after state for these types of things so we assert.
            if (UndoEngine.GetName(e.Component, false) is not null)
            {
                // The caller provided us with a component. This is the common case. We will add a new change event
                // provided there is not already one open for this component.
                bool hasChange = false;
 
                for (int idx = 0; idx < _changeEvents.Count; idx++)
                {
                    ChangeUndoEvent ce = _changeEvents[idx];
                    if (ce.OpenComponent == e.Component && ce.ContainsChange(e.Member))
                    {
                        hasChange = true;
                        break;
                    }
                }
 
                if (!hasChange ||
                    (e.Member?.Attributes is not null && e.Member.Attributes.Contains(DesignerSerializationVisibilityAttribute.Content)))
                {
#if DEBUG
                    string? name = UndoEngine.GetName(e.Component, false);
 
                    if (name is not null)
                    {
                        string memberName = e.Member?.Name ?? "(none)";
                    }
                    else
                    {
                        Debug.Fail("UndoEngine: GetName is failing on successive calls");
                    }
#endif
                    ChangeUndoEvent? changeEvent = null;
                    bool serializeBeforeState = true;
                    // perf: if this object was added in this undo unit we do not want to serialize before
                    // state for ChangeEvent since undo will remove it anyway
                    if (_ignoreAddedList is not null && _ignoreAddedList.Contains(e.Component))
                    {
                        serializeBeforeState = false;
                    }
 
                    if (e.Component is IComponent { Site: not null })
                    {
                        changeEvent = new ChangeUndoEvent(UndoEngine, e, serializeBeforeState);
                    }
                    else if (e.Component is not null)
                    {
                        if (GetService(typeof(IReferenceService)) is IReferenceService rs)
                        {
                            IComponent? owningComp = rs.GetComponent(e.Component);
 
                            if (owningComp is not null)
                            {
                                changeEvent = new ChangeUndoEvent(UndoEngine, new ComponentChangingEventArgs(owningComp, null), serializeBeforeState);
                            }
                        }
                    }
 
                    if (changeEvent is not null)
                    {
                        AddEvent(changeEvent);
                        _changeEvents.Add(changeEvent);
                    }
                }
            }
        }
 
        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component removed event.
        /// </summary>
        public virtual void ComponentRemoved(ComponentEventArgs e)
        {
            // We should gather undo state in ComponentRemoved, but by this time the component's designer
            // has been destroyed so it's too late. Instead, we captured state in the Removing method.
            // But, it is possible for there to be component changes to other objects
            // that happen between removing and removed, so we need to reorder the removing
            // event so it's positioned after any changes.
            if (_events is not null && e is not null)
            {
                ChangeUndoEvent? changeEvt = null;
                int changeIdx = -1;
                for (int idx = _events.Count - 1; idx >= 0; idx--)
                {
                    if (changeEvt is null)
                    {
                        changeEvt = _events[idx] as ChangeUndoEvent;
                        changeIdx = idx;
                    }
 
                    if (_events[idx] is AddRemoveUndoEvent evt && evt.OpenComponent == e.Component)
                    {
                        evt.Commit();
                        // We should only reorder events if there are change events coming between
                        // OnRemoving and OnRemoved. If there are other events (such as AddRemoving),
                        // the serialization done in OnComponentRemoving might refer to components
                        // that aren't available.
                        if (idx != _events.Count - 1 && changeEvt is not null)
                        {
                            // ensure only change change events exist between these two events
                            bool onlyChange = true;
                            for (int i = idx + 1; i < changeIdx; i++)
                            {
                                if (_events[i] is not ChangeUndoEvent)
                                {
                                    onlyChange = false;
                                    break;
                                }
                            }
 
                            if (onlyChange)
                            {
                                // reposition event after final ComponentChangingEvent
                                _events.RemoveAt(idx);
                                _events.Insert(changeIdx, evt);
                            }
                        }
 
                        break;
                    }
                }
            }
        }
 
        /// <summary>
        ///  The undo engine will call this on the active undo unit in response to a component removing event.
        /// </summary>
        public virtual void ComponentRemoving(ComponentEventArgs e)
        {
            if (e.Component!.Site is INestedContainer)
            {
                return;
            }
 
            _removeEvents ??= [];
 
            try
            {
                AddRemoveUndoEvent evt = new(UndoEngine, e.Component, false);
                AddEvent(evt);
                _removeEvents.Add(evt);
            }
            catch (TargetInvocationException) { }
        }
 
        /// <summary>
        ///  The undo engine will cal this on the active undo unit in response to a component rename event.
        /// </summary>
        public virtual void ComponentRename(ComponentRenameEventArgs e) =>
            AddEvent(new RenameUndoEvent(e.OldName, e.NewName));
 
        /// <summary>
        ///  Returns an instance of the requested service.
        /// </summary>
        protected object? GetService(Type serviceType) => UndoEngine.GetService(serviceType);
 
        /// <summary>
        ///  Override for object.ToString()
        /// </summary>
        public override string ToString() => Name;
 
        /// <summary>
        ///  Either performs undo, or redo, depending on the state of the unit.
        ///  UndoUnit initially assumes that the undoable work has already been "done",
        ///  so the first call to undo will undo the work. The next call will undo the "undo", performing a redo.
        /// </summary>
        public void Undo()
        {
            UndoUnit? savedUnit = UndoEngine._executingUnit;
            UndoEngine._executingUnit = this;
            DesignerTransaction? transaction = null;
            try
            {
                if (savedUnit is null)
                {
                    UndoEngine.OnUndoing(EventArgs.Empty);
                }
 
                // create a transaction here so things that do work on componentchanged can ignore
                // that while the transaction is opened...big perf win.
                transaction = UndoEngine._host.CreateTransaction();
                UndoCore();
            }
            catch (CheckoutException)
            {
                transaction!.Cancel();
                transaction = null;
                throw;
            }
            finally
            {
                transaction?.Commit();
 
                UndoEngine._executingUnit = savedUnit;
                if (savedUnit is null)
                {
                    UndoEngine.OnUndone(EventArgs.Empty);
                }
            }
        }
 
        /// <summary>
        ///  The undo method invokes this method to perform the actual undo / redo work.
        ///  You should never call this method directly; override it if you wish, but always call the public Undo method
        ///  to perform undo work. Undo notifies the undo engine to suspend undo data gathering until this
        ///  undo is completed, which prevents new undo units from being created in response to this unit doing work.
        /// </summary>
        protected virtual void UndoCore()
        {
            if (_events is not null)
            {
                if (_reverse)
                {
                    // How does BeforeUndo work? You'd think you should just call this in one pass,
                    // and then call Undo in another, but you'd be wrong. The complexity arises because
                    // there are undo events that have dependencies on other undo events.
                    // There are also undo events that have side effects with respect to other events.
                    // Here are examples:
                    // Rename:
                    //     Is an undo event that other undo events depend on, because they store names.
                    //     It must be performed in the right order and it must be performed before any
                    //     subsequent event's BeforeUndo is called.
                    // Property change:
                    //     Is an undo event that may have an unknown side effect if changing the
                    //     property results in other property changes
                    //     (for example, reparenting a control removes the control from its former parent).
                    // A property change undo event:
                    //     Must have all BeforeUndo methods called before any Undo method is called. To do this,
                    //     we have a property on UndoEvent called CausesSideEffects.
                    // As we run through UndoEvents, consecutive events that return true from this property
                    // are grouped so that their BeforeUndo methods are all called before their Undo methods.
                    // For events that do not have side effects, their BeforeUndo and Undo are invoked immediately.
                    for (int idx = _events.Count - 1; idx >= 0; idx--)
                    {
                        int groupEndIdx = idx;
                        for (int groupIdx = idx; groupIdx >= 0; groupIdx--)
                        {
                            if (_events[groupIdx].CausesSideEffects)
                            {
                                groupEndIdx = groupIdx;
                            }
                            else
                            {
                                break;
                            }
                        }
 
                        for (int beforeIdx = idx; beforeIdx >= groupEndIdx; beforeIdx--)
                        {
                            (_events[beforeIdx]).BeforeUndo(UndoEngine);
                        }
 
                        for (int undoIdx = idx; undoIdx >= groupEndIdx; undoIdx--)
                        {
                            (_events[undoIdx]).Undo(UndoEngine);
                        }
 
                        Debug.Assert(idx >= groupEndIdx, "We're going backwards");
                        idx = groupEndIdx;
                    }
 
                    // Now, if we have a selection, apply it.
                    if (_lastSelection is not null)
                    {
                        if (UndoEngine.TryGetService(out ISelectionService? ss))
                        {
                            List<IComponent> list = new(_lastSelection.Count);
                            foreach ((string name, IContainer container) in _lastSelection)
                            {
                                IComponent? comp = container.Components[name];
                                if (comp is not null)
                                {
                                    list.Add(comp);
                                }
                            }
 
                            ss.SetSelectedComponents(list, SelectionTypes.Replace);
                        }
                    }
                }
                else
                {
                    int count = _events.Count;
                    for (int idx = 0; idx < count; idx++)
                    {
                        int groupEndIdx = idx;
 
                        for (int groupIdx = idx; groupIdx < count; groupIdx++)
                        {
                            if (_events[groupIdx].CausesSideEffects)
                            {
                                groupEndIdx = groupIdx;
                            }
                            else
                            {
                                break;
                            }
                        }
 
                        for (int beforeIdx = idx; beforeIdx <= groupEndIdx; beforeIdx++)
                        {
                            (_events[beforeIdx]).BeforeUndo(UndoEngine);
                        }
 
                        for (int undoIdx = idx; undoIdx <= groupEndIdx; undoIdx++)
                        {
                            (_events[undoIdx]).Undo(UndoEngine);
                        }
 
                        Debug.Assert(idx <= groupEndIdx, "We're going backwards");
                        idx = groupEndIdx;
                    }
                }
            }
 
            _reverse = !_reverse;
        }
    }
}