DefaultUnDo 2.0.0
dotnet add package DefaultUnDo --version 2.0.0
NuGet\Install-Package DefaultUnDo -Version 2.0.0
<PackageReference Include="DefaultUnDo" Version="2.0.0" />
paket add DefaultUnDo --version 2.0.0
#r "nuget: DefaultUnDo, 2.0.0"
// 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.
<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 | Versions 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. |
-
.NETStandard 2.0
- System.Memory (>= 4.6.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.
- added net9.0