├── .github └── workflows │ └── dotnetcore.yml ├── .gitignore ├── BUILDING.md ├── Directory.Build.props ├── Monitored Undo Framework.sln ├── README.md ├── docs ├── ClassDiagram.png └── README.md ├── license.md ├── samples ├── WpfUndoSample │ ├── App.xaml │ ├── App.xaml.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ ├── Resources.Designer.cs │ │ ├── Resources.resx │ │ ├── Settings.Designer.cs │ │ └── Settings.settings │ ├── WpfUndoSample.csproj │ └── app.config └── WpfUndoSampleMVVM.Core │ ├── App.xaml │ ├── App.xaml.cs │ ├── AssemblyInfo.cs │ ├── AttachedProperties.cs │ ├── EventToCommand.cs │ ├── IEventArgsConverter.cs │ ├── MainWindow.xaml │ ├── MainWindow.xaml.cs │ ├── MainWindowViewModel.cs │ └── WpfUndoSampleMVVM.Core.csproj ├── src ├── .gitignore └── MonitoredUndo │ ├── ChangeFactory.cs │ ├── ChangeKey_T2.cs │ ├── ChangeKey_T3.cs │ ├── ChangeSet.cs │ ├── Changes │ ├── Change.cs │ ├── CollectionAddChange.cs │ ├── CollectionAddRemoveChangeBase.cs │ ├── CollectionChange.cs │ ├── CollectionMoveChange.cs │ ├── CollectionRemoveChange.cs │ ├── CollectionReplaceChange.cs │ ├── DelegateChange.cs │ ├── DictionaryAddChange.cs │ ├── DictionaryAddRemoveChangeBase.cs │ ├── DictionaryChange.cs │ ├── DictionaryRemoveChange.cs │ ├── DictionaryReplaceChange.cs │ └── PropertyChange.cs │ ├── DefaultChangeFactory.cs │ ├── ISupportsUndo.cs │ ├── ISupportsUndoNotification.cs │ ├── IUndoMetadata.cs │ ├── MonitoredUndo.csproj │ ├── MonitoredUndo.csproj.vspscc │ ├── UndoBatch.cs │ ├── UndoRoot.cs │ ├── UndoService.cs │ └── WeakReferenceComparer.cs └── tests └── MonitoredUndo.Tests ├── ChildA.cs ├── ChildB.cs ├── MonitoredUndo.Tests.csproj ├── ObservableDictionary.cs ├── RootDocument.cs └── UndoTests.cs /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - feature/* 8 | pull_request: 9 | branches: 10 | - master 11 | - feature/* 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Setup .NET Core 21 | uses: actions/setup-dotnet@v1 22 | with: 23 | dotnet-version: 3.1.101 24 | - name: Install dependencies 25 | run: dotnet restore tests/MonitoredUndo.Tests/MonitoredUndo.Tests.csproj 26 | - name: Build 27 | run: dotnet build tests/MonitoredUndo.Tests/MonitoredUndo.Tests.csproj --configuration Release --no-restore 28 | - name: Test 29 | run: dotnet test tests/MonitoredUndo.Tests/MonitoredUndo.Tests.csproj --no-restore --verbosity normal 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## Visual Studio 3 | ################# 4 | 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.sln.docstates 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Rr]elease/ 16 | *_i.c 17 | *_p.c 18 | *.ilk 19 | *.meta 20 | *.obj 21 | *.pch 22 | *.pdb 23 | *.pgc 24 | *.pgd 25 | *.rsp 26 | *.sbr 27 | *.tlb 28 | *.tli 29 | *.tlh 30 | *.tmp 31 | *.vspscc 32 | .builds 33 | *.dotCover 34 | 35 | ## TODO: If you have NuGet Package Restore enabled, uncomment this 36 | packages/ 37 | 38 | # Visual Studio profiler 39 | *.psess 40 | *.vsp 41 | 42 | # ReSharper is a .NET coding add-in 43 | _ReSharper* 44 | 45 | # Others 46 | /releases/ 47 | [Bb]in 48 | [Oo]bj 49 | sql 50 | TestResults 51 | *.Cache 52 | ClientBin 53 | stylecop.* 54 | ~$* 55 | *.dbmdl 56 | Generated_Code #added for RIA/Silverlight projects 57 | .vs 58 | 59 | ############ 60 | ## Windows 61 | ############ 62 | 63 | # Windows image file caches 64 | Thumbs.db 65 | 66 | # Folder config file 67 | Desktop.ini 68 | 69 | 70 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | # Build / Run / Release 2 | 3 | 4 | ## Build Library and Tests 5 | 6 | Note: The solution includes projects that only run properly on windows machines. 7 | 8 | 1. Clone repo 9 | 2. Open bash shell in root directory of git repo 10 | 2. `dotnet build tests/MonitoredUndo.Tests/MonitoredUndo.Tests.csproj` 11 | 3. `dotnet test tests/MonitoredUndo.Tests/MonitoredUndo.Tests.csproj` 12 | 13 | ## Release 14 | 15 | The solution and projects use the "Directory.Build.props" feature to keep solution-wide 16 | values in the root of the solution. 17 | 18 | To create a Pre-Release nuget package: 19 | 20 | 1. Build the library and run tests as above. 21 | 2. `dotnet pack src/MonitoredUndo/MonitoredUndo.csproj --version-suffix "alpha.1" --include-source --include-symbols` 22 | 3. Upload the package from `src/MonitoredUndo/Debug/` to nuget. 23 | 24 | 25 | To create a Release nuget package: 26 | 27 | 1. Build the library and run tests as above. 28 | 2. `dotnet pack src/MonitoredUndo/MonitoredUndo.csproj --configuration Release --include-source --include-symbols` 29 | 3. Upload the package from `src/MonitoredUndo/Release/` to nuget. 30 | 31 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | IOperation 11 | Full 12 | 13 | 14 | 2.0.0 15 | 16 | 17 | 18 | 19 | $(VersionPrefix) 20 | 21 | 22 | 23 | $(VersionPrefix)-$(VersionSuffix) 24 | 25 | 26 | 27 | Nathan Allen-Wagner 28 | Copyright © Alner LLC 2020 29 | Monitored Undo Framework 30 | Monitored Undo Framework 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /Monitored Undo Framework.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29806.167 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".assets", ".assets", "{7C8B148F-D9DF-4838-AA00-E267C3A9172A}" 7 | ProjectSection(SolutionItems) = preProject 8 | .gitignore = .gitignore 9 | BUILDING.md = BUILDING.md 10 | Directory.Build.props = Directory.Build.props 11 | license.md = license.md 12 | README.md = README.md 13 | EndProjectSection 14 | EndProject 15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{58941425-BFC3-4DC9-8A6C-EDD42B8A9FDB}" 16 | ProjectSection(SolutionItems) = preProject 17 | docs\ClassDiagram.png = docs\ClassDiagram.png 18 | docs\README.md = docs\README.md 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F1F7AC76-9E13-4D8B-93E8-6AD4DAAF599C}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{52ADFB30-284E-4673-87BC-30FBCA144471}" 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{3A82FA65-EE92-400B-B002-64AE2B8A91DF}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitoredUndo", "src\MonitoredUndo\MonitoredUndo.csproj", "{FF127A9A-BACB-4BC3-99DE-9F80275A06D4}" 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MonitoredUndo.Tests", "tests\MonitoredUndo.Tests\MonitoredUndo.Tests.csproj", "{876B42D2-67E8-45B5-886C-9AB8C75E76C4}" 30 | EndProject 31 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WpfUndoSample", "samples\WpfUndoSample\WpfUndoSample.csproj", "{5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WpfUndoSampleMVVM.Core", "samples\WpfUndoSampleMVVM.Core\WpfUndoSampleMVVM.Core.csproj", "{0AFB4172-7839-419B-A434-E77E87071853}" 34 | EndProject 35 | Global 36 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 37 | Debug|Any CPU = Debug|Any CPU 38 | Debug|Mixed Platforms = Debug|Mixed Platforms 39 | Debug|x86 = Debug|x86 40 | Release|Any CPU = Release|Any CPU 41 | Release|Mixed Platforms = Release|Mixed Platforms 42 | Release|x86 = Release|x86 43 | EndGlobalSection 44 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 45 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 48 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 49 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Debug|x86.ActiveCfg = Debug|Any CPU 50 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 53 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Release|Mixed Platforms.Build.0 = Release|Any CPU 54 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}.Release|x86.ActiveCfg = Release|Any CPU 55 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 58 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 59 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Debug|x86.ActiveCfg = Debug|Any CPU 60 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Release|Any CPU.Build.0 = Release|Any CPU 62 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 63 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Release|Mixed Platforms.Build.0 = Release|Any CPU 64 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4}.Release|x86.ActiveCfg = Release|Any CPU 65 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 66 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Debug|Any CPU.Build.0 = Debug|Any CPU 67 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Debug|Mixed Platforms.ActiveCfg = Debug|x86 68 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Debug|Mixed Platforms.Build.0 = Debug|x86 69 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Debug|x86.ActiveCfg = Debug|x86 70 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Debug|x86.Build.0 = Debug|x86 71 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Release|Any CPU.ActiveCfg = Release|x86 72 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Release|Mixed Platforms.ActiveCfg = Release|x86 73 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Release|Mixed Platforms.Build.0 = Release|x86 74 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Release|x86.ActiveCfg = Release|x86 75 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}.Release|x86.Build.0 = Release|x86 76 | {0AFB4172-7839-419B-A434-E77E87071853}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {0AFB4172-7839-419B-A434-E77E87071853}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {0AFB4172-7839-419B-A434-E77E87071853}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU 79 | {0AFB4172-7839-419B-A434-E77E87071853}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU 80 | {0AFB4172-7839-419B-A434-E77E87071853}.Debug|x86.ActiveCfg = Debug|Any CPU 81 | {0AFB4172-7839-419B-A434-E77E87071853}.Debug|x86.Build.0 = Debug|Any CPU 82 | {0AFB4172-7839-419B-A434-E77E87071853}.Release|Any CPU.ActiveCfg = Release|Any CPU 83 | {0AFB4172-7839-419B-A434-E77E87071853}.Release|Any CPU.Build.0 = Release|Any CPU 84 | {0AFB4172-7839-419B-A434-E77E87071853}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU 85 | {0AFB4172-7839-419B-A434-E77E87071853}.Release|Mixed Platforms.Build.0 = Release|Any CPU 86 | {0AFB4172-7839-419B-A434-E77E87071853}.Release|x86.ActiveCfg = Release|Any CPU 87 | {0AFB4172-7839-419B-A434-E77E87071853}.Release|x86.Build.0 = Release|Any CPU 88 | EndGlobalSection 89 | GlobalSection(SolutionProperties) = preSolution 90 | HideSolutionNode = FALSE 91 | EndGlobalSection 92 | GlobalSection(NestedProjects) = preSolution 93 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4} = {F1F7AC76-9E13-4D8B-93E8-6AD4DAAF599C} 94 | {876B42D2-67E8-45B5-886C-9AB8C75E76C4} = {52ADFB30-284E-4673-87BC-30FBCA144471} 95 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D} = {3A82FA65-EE92-400B-B002-64AE2B8A91DF} 96 | {0AFB4172-7839-419B-A434-E77E87071853} = {3A82FA65-EE92-400B-B002-64AE2B8A91DF} 97 | EndGlobalSection 98 | GlobalSection(ExtensibilityGlobals) = postSolution 99 | SolutionGuid = {E2B57A17-7A82-4907-91F3-69A1CAF1EA7E} 100 | EndGlobalSection 101 | GlobalSection(TestCaseManagementSettings) = postSolution 102 | CategoryFile = Monitored Undo Framework.vsmdi 103 | EndGlobalSection 104 | EndGlobal 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Monitored Undo Framework 2 | Monitored Undo is a .NET Standard 2.0 Undo / Redo framework. The framework "monitors" changes 3 | to the model, keeps a history of undo / redo operations, and assists with applying an undo 4 | back to the model. 5 | 6 | # Documentation 7 | Please refer to the [documentation folder](docs/) in the repo for details on usage. 8 | 9 | ## Quick Start... 10 | To get a quick idea of how it works, check out the unit tests and the sample model classes. 11 | 12 | ## Sample Application 13 | There is a WPF Sample application in the source code tree. This is a very simple app that 14 | shows a couple features of the undo framework. It does not follow best practices, but does 15 | illustrate how to hook things up. 16 | 17 | ## Reference Application 18 | For a more complete example application, consider the 19 | [Photo Tagger reference app](https://nathan.alner.net/2010/10/13/wpf-amp-entity-framework-4-tales-from-the-trenches/) 20 | sample application. It was created for a presentation related to WPF applications, EF, and undo. 21 | 22 | # NuGet 23 | MUF is [available on NuGet](http://nuget.org/List/Packages/MUF). 24 | -------------------------------------------------------------------------------- /docs/ClassDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathanaw/muf/9db36ddb4412380024e25e3eece5938778e3921c/docs/ClassDiagram.png -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Design 2 | 3 | ## Design Goals 4 | 5 | 1. Simple usage patterns. 6 | 2. A __change monitoring__ approach, rather than a __command model__ approach. 7 | 8 | 9 | ## Change Monitoring 10 | 11 | "Change Monitoring" observes and captures the changes that result from a given action. 12 | 13 | This is kind of like putting a net under the tree, shaking it, and catching what falls out. 14 | 15 | The benefits is simplicity. It requires relatively straight forward changes to the codebase, 16 | minimal up-front design impact, and robust change handling. 17 | 18 | ## Command Model Challenges 19 | 20 | The "Command Model" typically prescribes that actions against a system should be done via a 21 | class that knows how to perform the action, and also how to undo that action. 22 | 23 | This requires careful planning and design. 24 | 25 | Challenges: 26 | - Requires full understanding of the downstream consequences of an action 27 | - In a "reactive" system, this can be challenging to implement since one action could 28 | result in a cascade of changes through the system. 29 | - Prevents (or complicates) usage of other patterns, like WPF's two-way bindings. 30 | - A pure command based implementation would only be able to use one-way bindings, 31 | preferring to push updates through a command rather than a binding. 32 | 33 | 34 | 35 | # Classes 36 | 37 | ![Class Diagram](ClassDiagram.png) 38 | 39 | 40 | ## UndoService 41 | 42 | `UndoService` is the top level of the undo / redo system. It contains one or more `UndoRoots`, 43 | accessible via the indexer on the `UndoService`. 44 | 45 | `UndoService.Current` property will return the singleton instance of the `UndoService`. 46 | Use this when interacting with the undo service. 47 | 48 | `UndoService.Current[modelRoot]` will return an instance of `UndoRoot` for the specified modelRoot. 49 | 50 | 51 | 52 | ## UndoRoot 53 | 54 | `UndoRoot` collects changes related to a specific document or instance of a model. This allows an application to 55 | track multiple, distinct undo stacks. This class has most of the public API methods that you'll use to undo, 56 | redo, and add changes. 57 | 58 | Contains FIFO stacks of `ChangeSets` for undo and redo actions. Includes the logic to manage the undo and redo stacks. 59 | For example, the redo stack is cleared whenever a new undo `ChangeSet` is added. 60 | 61 | `UndoRoot.Undo()` will undo the last operation. (Overloads available) 62 | 63 | `UndoRoot.Redo()` will redo the last operation. (Overloads available) 64 | 65 | `UndoRoot.AddChange()` will add a new `Change` to the system. `ChangeSets` are automatically created as needed. 66 | 67 | `UndoRoot.Clear()` will clear the undo and redo stacks. 68 | 69 | 70 | 71 | ## ChangeSet 72 | 73 | `ChangeSet` has a collection of `Change` instances. It represents a unit of undo or redo work. 74 | 75 | 76 | 77 | ## Change 78 | 79 | `Change` is an individual action to perform when undoing or redoing a `ChangeSet`. 80 | The `Change` class contains `Action()` delegates (or lambdas) that perform the undo and 81 | redo operations. 82 | 83 | 84 | 85 | ## UndoBatch 86 | 87 | Is a "scope based" helper that can group changes into a single `ChangeSet`. 88 | It detects new `Changes` that occur and automatically groups them into a single `ChangeSet`. 89 | 90 | `UndoBatch` leverages `IDisposable` so that the batch can be contained in a `using` block. 91 | `UndoBatch` will start a new batch at the start of the using block and then close that 92 | batch at the end of the using statement, when the `Dispose()` method is called. 93 | 94 | `UndoBatch` supports nested usage, but will only ever start a single `ChangeSet`. 95 | This means that the top-most `UndoBatch` controls the batching boundary. 96 | 97 | 98 | 99 | ## DefaultChangeFactory 100 | 101 | `DefaultChangeFactory` is a static utility class that helps populate the undo system with 102 | `ChangeSet` and `Change` instances. The default implementation uses reflection to access 103 | the properties of a class. 104 | 105 | If implemented, the `DefaultChangeFactory` will take advantage of the interfaces 106 | (mentioned below) to allow more control over the undo / redo process. 107 | 108 | 109 | 110 | # Interfaces 111 | 112 | The following are used by the `DefaultChangeFactory` and other change factories. The interfaces make 113 | it simple for these factories to create the undo / redo actions and provide the class a way to 114 | intercept or influence this process. 115 | 116 | ## ISupportsUndo 117 | 118 | Should be implemented on classes that want to participate in undo / redo operations. 119 | 120 | `ISupportsUndo.GetUndoRoot()` should be implemented and return a reference to the "model root" 121 | or "document root" that represents the undo boundary. 122 | 123 | This is not required to use the undo system, but is required by the `DefaultChangeFactory`. 124 | 125 | ## ISupportsUndoNotification 126 | 127 | An optional interface that helps classes react to undo and redo operations. 128 | 129 | This is not required to use the undo system, but is required by the `DefaultChangeFactory`. 130 | 131 | ## IUndoMetadata 132 | 133 | Allows a class to influence whether a given property or collection change should be tracked for undo. 134 | 135 | This is not required to use the undo system, but is required by the `DefaultChangeFactory`. 136 | 137 | 138 | 139 | # Notes and Common Issues 140 | 141 | ## Step Zero... Review the Unit Tests 142 | 143 | The unit tests for the Undo system is a great place to start. It will show you the way that the 144 | classes are supposed to be used, and you can even step through them in debug to understand how 145 | things work. 146 | 147 | ## "Undo doesn't seem to work..." 148 | 149 | If you are hitting "Undo", but your user interface isn't changing, then the problem might be 150 | with your `INotifyPropertyChanged` (aka INPC) implementation. Often times, the Undo service is 151 | actually undoing the changes by updating the model and/or view models. However, if your UI is 152 | bound to a property that doesn't raise the `PropertyChanged` or `CollectionChanged` event when 153 | the underlying model changes, then the UI won't update. 154 | 155 | One way to check this is to undo some actions, save the model, and then re-open the model. 156 | If the values are undone, then there is an `INotifyPropertyChanged` gap between the UI's bound 157 | property and the underlying model. 158 | 159 | ## "I need to group a set of changes together..." 160 | 161 | In some cases, you want to click "Undo" and have it undo a set of changes, not just one change. 162 | To do this, you'll need to use the `UndoBatch` class to group these changes. 163 | 164 | Example of grouping changes: 165 | 166 | ```c# 167 | 168 | [TestMethod] 169 | public void UndoRoot_Supports_Starting_a_Batch_Of_Changes() 170 | { 171 | var orig = Document1.A.Name; 172 | var firstChange = "First Change"; 173 | var secondChange = "Second Change"; 174 | var root = UndoService.Current[Document1]; 175 | 176 | using (new UndoBatch(Document1, "Change Name", false)) 177 | { 178 | Document1.A.Name = firstChange; 179 | Document1.A.Name = secondChange; 180 | } 181 | 182 | Assert.AreEqual(1, root.UndoStack.Count()); 183 | Assert.AreEqual(0, root.RedoStack.Count()); 184 | 185 | root.Undo(); 186 | Assert.AreEqual(orig, Document1.A.Name); 187 | Assert.AreEqual(0, root.UndoStack.Count()); 188 | Assert.AreEqual(1, root.RedoStack.Count()); 189 | 190 | root.Redo(); 191 | Assert.AreEqual(secondChange, Document1.A.Name); 192 | Assert.AreEqual(1, root.UndoStack.Count()); 193 | Assert.AreEqual(0, root.RedoStack.Count()); 194 | } 195 | 196 | ``` 197 | 198 | ## "I'm doing a mouse operation or a calculation that is changing the same field multiple times within the same undo batch. I only need the last value." 199 | 200 | If you have a mouse based operation, then your model or view model might be changing repeatedly 201 | as the user drags the mouse around. This can result in one Change for each discrete position of the mouse. 202 | 203 | Typically, the system and the undo service only need to remember the last value. Undoing the operation 204 | reverts to the original value before dragging the mouse. Redoing applies the value from when they 205 | stopped dragging the mouse. All intermediate values are irrelevant. 206 | 207 | To handle this scenario, the top level `UndoBatch` constructor takes the `bool consolidateChangesForSameInstance` 208 | parameter. This parameter will tell the undo system that it should only keep the last value for each changed 209 | property within the batch. 210 | 211 | ### Note: 212 | This functionality takes a little more processing time, but reduces the memory used. 213 | 214 | ### Note: 215 | This functionality requires that the `Change` instance have a reliable "token" to uniquely identify the 216 | property that it is for. The built-in `DefaultChangeFactory` class handles this automatically, but if you are 217 | manually creating `Change` instances, you'll need to ensure that you have a unique "token" for the property. 218 | 219 | A simple implementation is to use the `Tuple<>` class with a sufficient number of parameters to uniquely 220 | identify the object instance, and the property on that instance. See the `DefaultChangeFactory` for an 221 | example of this. 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT 2 | Copyright (c) 2010 Nathan Allen-Wagner 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /samples/WpfUndoSample/App.xaml: -------------------------------------------------------------------------------- 1 |  5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /samples/WpfUndoSample/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.Data; 5 | using System.Linq; 6 | using System.Windows; 7 | 8 | namespace WpfUndoSample 9 | { 10 | /// 11 | /// Interaction logic for App.xaml 12 | /// 13 | public partial class App : Application 14 | { 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/WpfUndoSample/MainWindow.xaml: -------------------------------------------------------------------------------- 1 |  9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 36 | 37 | 38 | 46 | 47 | 48 | 55 | 56 | 57 |