DefaultUnDo 2.0.0

dotnet add package DefaultUnDo --version 2.0.0                
NuGet\Install-Package DefaultUnDo -Version 2.0.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DefaultUnDo" Version="2.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add DefaultUnDo --version 2.0.0                
#r "nuget: DefaultUnDo, 2.0.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install DefaultUnDo as a Cake Addin
#addin nuget:?package=DefaultUnDo&version=2.0.0

// Install DefaultUnDo as a Cake Tool
#tool nuget:?package=DefaultUnDo&version=2.0.0                

DefaultUnDo

DefaultUnDo is a simple Command pattern implementation to ease the addition of an undo/redo feature.

NuGet preview package continuous integration status Coverage Status

<a name='Requirement'></a>

Requirement

Compatible from .NETStandard 2.0.
For development, net framework 4.8 and net8.0 are required to build and run all tests.

<a name='Overview'></a>

Overview

Easy to use, just instanciate a UnDoManager and get going. Numerous extension methods are available to ease the integration.

IUnDoManager manager = new UnDoManager();

// do an action and record it in the manager, undoAction being the undo equivalent of the action
manager.Do(doAction, undoAction);

if (manager.CanUndo)
{
    manager.Undo();
}

if (Manager.CanRedo)
{
    manager.Redo();
}

// clean any recorded action
manager.Clear();

Example of how to set a value

IUnDoManager manager = new UnDoManager();

int field = 42;

manager.Do(v => field = v, 1337, field);

// In mvvm we all have some kind of base type
public abstract class ANotifyPropertyChanged : INotifyPropertyChanged
{
    private sealed class UnDoProperty<T> : UnDoField<T>
    {
        private readonly ANotifyPropertyChanged _parent;
        private readonly string _propertyName;

        public UnDoProperty(ANotifyPropertyChanged parent, IUnDoManager unDoManager, string propertyName)
            : base(unDoManager, _ => $"Changed {typeof(T).GetFriendlyShortName()} {propertyName}")
        {
            _parent = parent;
            _propertyName = propertyName;
        }

        // to call PropertyChanged when changing value
        protected override void PostSet(T value) => _parent.NotifyPropertyChanged(_propertyName);
    }

    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

    protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
    {
        if ((field is IEquatable<T> equatable && equatable.Equals(value))
            || (typeof(T).IsValueType && Equals(field, value))
            || ReferenceEquals(field, value))
        {
            return false;
        }

        field = value;

        NotifyPropertyChanged(propertyName);

        return true;
    }

    // need to pass the unDoManager in case the UnDoField is not initialised, this is mainly to keep the same signature between normal field and UnDoField (ref field, value)
    // but you can ommit the IUnDoManager and ref if you choose to initialize UnDoField in the constructor
    protected bool SetProperty<T>(IUnDoManager unDoManager, ref UnDoField<T> field, T value, [CallerMemberName] string propertyName = null)
    {
        field ??= new UnDoProperty<T>(this, unDoManager, propertyName);
        T oldValue = field;

        if ((oldValue is IEquatable<T> equatable && equatable.Equals(value))
            || (typeof(T).IsValueType && Equals(oldValue, value))
            || ReferenceEquals(oldValue, value))
        {
            return false;
        }

        field.Value = value;

        return true;
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

// usage in derrived types
private UnDoField<string> _field;

public string Field
{
    get => _field;
    set => SetProperty(manager, ref _field, value);
}

// events interraction
manager.Do(
    () => PropertyChanged += OnPropertyChanged,  // executed on Do/Redo
    () => PropertyChanged -= OnPropertyChanged); // executed on Undo

// Need something to only happen in Do/Redo
manager.DoOnDo(() => NotifyPropertyChanged(nameof(MyProperty)));

// Or only on Undo
manager.DoOnUndo(() => NotifyPropertyChanged(nameof(MyProperty)));

ICollection<T>, IList<T>, IDictionary<TKey, TValue> and ISet<T> can be coverterted to an undo instance so that any action performed on them will generate a IUnDo action on the manager.

IUnDoManager manager = new UnDoManager();

// use myList as you would use your list normaly
IList<int> myList = new List<int>().AsUnDo(manager);

// use myCollection as you would use your collection normaly
// note than the returned collection also implement INotifyCollectionChanged
ICollection<int> myCollection = new ObservableCollection<int>().AsUnDo(manager);

// use myDictionary as you would use your dictionary normaly
IDictionary<int, string> myDictionary = new Dictionary<int, string>().AsUnDo(manager);

// use mySet as you would use your set normaly
ISet<int> mySet = new HashSet<int>().AsUnDo(manager);

To generate a custom description when changes occure on those undo collection, the AsUnDo extension method can take a Func<UnDoCollectionOperation, object> descriptionFactory parameter.

It is possible to declare a transaction scope for your operations so a single undo/redo will execute all the contained operations.

IUnDoManager manager = new UnDoManager();

using (IUnDoTransaction transaction = manager.BeginTransaction())
{
    manager.Do(action1, undo1);
    manager.Do(action2, undo2);

    // if you do not commit the transaction, all operations inside the scope will be undone on transaction dispose
    transaction.Commit();
}

// both undo2 and undo1 will be called in this order
manager.Undo();

// both action1 and action2 will be called in this order
manager.Redo();

If a group scope is declared inside an other group scope, all operations will be grouped in the same undo/redo operation.

IUnDoManager manager = new UnDoManager();

using (IUnDoTransaction transaction1 = manager.BeginTransaction())
{
    manager.Do(action1, undo1);

    using (IUnDoTransaction transaction2 = manager.BeginTransaction())
    {
        manager.Do(action2, undo2);

        transaction2.Commit();
    }

    transaction1.Commit();
}

// both undo2 and undo1 will be called in this order
manager.Undo();

// both action1 and action2 will be called in this order
manager.Redo();

IUnDoManager.Undo and IUnDoManager.Redo calls are not supported when inside a transaction.

To keep track of the modification, a Version property is available on the manager.

Missing something? you can easily extend what you need by creating your own implementations of the IUnDo interface and extension methods to ease the usage. Feel free to open an issue or send a pull request.

<a name='Dependencies'></a> Relies on these awesome projects:

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

  • .NETStandard 2.1

    • No dependencies.
  • net8.0

    • No dependencies.
  • net9.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
2.0.0 96 11/15/2024
1.0.0 96 10/24/2024
0.3.0 455 10/3/2021
0.2.0 339 1/15/2021
0.1.0 482 8/18/2020
0.0.1 448 8/9/2020

- added net9.0