├── .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 | 
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 |
60 |
61 |
69 |
70 |
78 |
79 |
82 |
83 |
84 |
85 |
86 |
91 |
92 |
93 |
94 |
96 |
97 |
99 |
100 |
101 |
102 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Windows;
6 | using System.Windows.Controls;
7 | using System.Windows.Data;
8 | using System.Windows.Documents;
9 | using System.Windows.Input;
10 | using System.Windows.Media;
11 | using System.Windows.Media.Imaging;
12 | using System.Windows.Navigation;
13 | using System.Windows.Shapes;
14 | using MonitoredUndo;
15 | using System.ComponentModel;
16 |
17 | namespace WpfUndoSample
18 | {
19 |
20 | // ********************************************************************************************************************
21 | // NOTE:
22 | // For this sample, the window is the "model" and also the "root" of the undo document hierarchy.
23 | // As a result, this window is used for bindings and therefor must support INotifyPropertyChanged and ISupportsUndo.
24 | //
25 | // In a production application, using the code-behind like this is probably not the "best practice".
26 | // However, for a sample, it simplifies the number of concepts needed to understand how the undo system works.
27 | // ********************************************************************************************************************
28 | public partial class MainWindow
29 | : Window, INotifyPropertyChanged, ISupportsUndo
30 | {
31 |
32 |
33 |
34 | public MainWindow()
35 | {
36 | InitializeComponent();
37 | }
38 |
39 |
40 |
41 |
42 |
43 | private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
44 | {
45 | // The undo / redo stack collections are not "Observable", so we
46 | // need to manually refresh the UI when they change.
47 | var root = UndoService.Current[this];
48 | root.UndoStackChanged += new EventHandler(OnUndoStackChanged);
49 | root.RedoStackChanged += new EventHandler(OnRedoStackChanged);
50 | FirstNameTextbox.Focus();
51 | }
52 |
53 | // Refresh the UI when the undo stack changes.
54 | void OnUndoStackChanged(object sender, EventArgs e)
55 | {
56 | RefreshUndoStackList();
57 | }
58 |
59 | // Refresh the UI when the redo stack changes.
60 | void OnRedoStackChanged(object sender, EventArgs e)
61 | {
62 | RefreshUndoStackList();
63 | }
64 |
65 |
66 | // The following 4 event handlers support the "CommandBindings" in the window.
67 | // These hook to the Undo and Redo commands.
68 |
69 | private void Undo_CanExecute(object sender, CanExecuteRoutedEventArgs e)
70 | {
71 | // Tell the UI whether Undo is available.
72 | e.CanExecute = UndoService.Current[this].CanUndo;
73 | }
74 |
75 | private void Redo_CanExecute(object sender, CanExecuteRoutedEventArgs e)
76 | {
77 | // Tell the UI whether Redo is available.
78 | e.CanExecute = UndoService.Current[this].CanRedo;
79 | }
80 |
81 | private void Undo_Executed(object sender, ExecutedRoutedEventArgs e)
82 | {
83 | // Get the document root. In this case, we pass in "this", which
84 | // implements ISupportsUndo. The ISupportsUndo interface is used
85 | // by the UndoService to locate the appropriate root node of an
86 | // undoable document.
87 | // In this case, we are treating the window as the root of the undoable
88 | // document, but in a larger system the root would probably be your
89 | // domain model.
90 | var undoRoot = UndoService.Current[this];
91 | undoRoot.Undo();
92 | }
93 |
94 | private void Redo_Executed(object sender, ExecutedRoutedEventArgs e)
95 | {
96 | // A shorthand version of the above call to Undo, except
97 | // that this calls Redo.
98 | UndoService.Current[this].Redo();
99 | }
100 |
101 |
102 |
103 | private void Slider_MouseDown(object sender, MouseButtonEventArgs e)
104 | {
105 | if (!BatchAgeChanges)
106 | return;
107 |
108 | // Start a batch to collect all subsequent undo events (for this root)
109 | // into a single changeset.
110 | //
111 | // Passing "false" for the last parameter tells the system to keep
112 | // each individual change that is made. If desired, pass "true" to
113 | // de-dupe these changes and reduce the memory requirements of the
114 | // changeset.
115 | UndoService.Current[this].BeginChangeSetBatch("Age Changed", false);
116 |
117 | e.Handled = false;
118 | }
119 |
120 | private void Slider_LostMouseCapture(object sender, MouseEventArgs e)
121 | {
122 | if (!BatchAgeChanges)
123 | return;
124 |
125 | UndoService.Current[this].EndChangeSetBatch();
126 |
127 | e.Handled = false;
128 | }
129 |
130 |
131 |
132 |
133 |
134 |
135 | // Below are properties bound to the UI with a XAML binding.
136 | // NOTE that these properly implement INotifyPropertyChange.
137 | // This is critical if the UI is going to stay in sync
138 | // with the changes to the data.
139 |
140 |
141 | private string _FirstName;
142 | public string FirstName
143 | {
144 | get { return _FirstName; }
145 | set
146 | {
147 | if (value == _FirstName)
148 | return;
149 |
150 | // Store this change in the Undo system.
151 | // This uses the "DefaultChangeFactory" to construct the change, but you can
152 | // store changes any way you like.
153 | DefaultChangeFactory.OnChanging(this, "FirstName", _FirstName, value, "First Name Changed");
154 |
155 | _FirstName = value;
156 | OnPropertyChanged("FirstName"); // Tells the UI that this property has changed.
157 | OnPropertyChanged("FullName"); // If FirstName changes, then FullName is also affected.
158 | }
159 | }
160 |
161 | private string _LastName;
162 | public string LastName
163 | {
164 | get { return _LastName; }
165 | set
166 | {
167 | if (value == _LastName)
168 | return;
169 |
170 | // Store this change in the Undo system.
171 | // This uses the "DefaultChangeFactory" to construct the change, but you can
172 | // store changes any way you like.
173 | DefaultChangeFactory.OnChanging(this, "LastName", _LastName, value, "Last Name Changed");
174 |
175 | _LastName = value;
176 | OnPropertyChanged("LastName"); // Tells the UI that this property changed.
177 | OnPropertyChanged("FullName"); // If LastName changes, then FullName is also affected.
178 | }
179 | }
180 |
181 | public string FullName
182 | {
183 | get
184 | {
185 | return string.Format("{0} {1}", FirstName, LastName);
186 | }
187 | }
188 |
189 |
190 | private int _Age;
191 | public int Age
192 | {
193 | get { return _Age; }
194 | set
195 | {
196 | if (value == _Age)
197 | return;
198 |
199 | // Store this change in the Undo system.
200 | // This uses the "DefaultChangeFactory" to construct the change, but you can
201 | // store changes any way you like.
202 | DefaultChangeFactory.OnChanging(this, "Age", _Age, value, "Age Changed");
203 |
204 | _Age = value;
205 | OnPropertyChanged("Age");
206 | }
207 | }
208 |
209 |
210 | private bool _BatchAgeChanges = true;
211 | public bool BatchAgeChanges
212 | {
213 | get { return _BatchAgeChanges; }
214 | set
215 | {
216 | if (value == _BatchAgeChanges)
217 | return;
218 |
219 | _BatchAgeChanges = value;
220 | OnPropertyChanged("BatchAgeChanges");
221 | }
222 | }
223 |
224 |
225 | // Expose the undo and redo stacks to the UI for binding.
226 |
227 | public IEnumerable UndoStack
228 | {
229 | get
230 | {
231 | return UndoService.Current[this].UndoStack;
232 |
233 | }
234 | }
235 |
236 | public IEnumerable RedoStack
237 | {
238 | get
239 | {
240 | return UndoService.Current[this].RedoStack;
241 |
242 | }
243 | }
244 |
245 |
246 |
247 |
248 |
249 | // Refresh the UI when the undo / redo stacks change.
250 | private void RefreshUndoStackList()
251 | {
252 | // Calling refresh on the CollectionView will tell the UI to rebind the list.
253 | // If the list were an ObservableCollection, or implemented INotifyCollectionChanged, this would not be needed.
254 | var cv = CollectionViewSource.GetDefaultView(UndoStack);
255 | cv.Refresh();
256 |
257 | cv = CollectionViewSource.GetDefaultView(RedoStack);
258 | cv.Refresh();
259 | }
260 |
261 |
262 |
263 |
264 |
265 | // This interface is needed on objects that are part of the
266 | // document hierarchy. It allows this object to be passed
267 | // in to the UndoService.Current[] indexer.
268 | // This method should return the "root" of the document.
269 | // In this case, we are treating the window as the "root" of the
270 | // document. In other cases, the root might be your domain model.
271 | //
272 | // See the unit tests for a better example of how this comes into
273 | // use for a multi-object document hierarchy.
274 |
275 | public object GetUndoRoot()
276 | {
277 | return this;
278 | }
279 |
280 |
281 |
282 |
283 |
284 | ///
285 | /// The PropertyChanged event is used by consuming code
286 | /// (like WPF's binding infrastructure) to detect when
287 | /// a value has changed.
288 | ///
289 | public event PropertyChangedEventHandler PropertyChanged;
290 |
291 | ///
292 | /// Raise the PropertyChanged event for the
293 | /// specified property.
294 | ///
295 | ///
296 | /// A string representing the name of
297 | /// the property that changed.
298 | ///
299 | /// Only raise the event if the value of the property
300 | /// has changed from its previous value
301 | protected void OnPropertyChanged(string propertyName)
302 | {
303 | // Validate the property name in debug builds
304 | VerifyProperty(propertyName);
305 |
306 | if (null != PropertyChanged)
307 | {
308 | PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
309 | }
310 | }
311 |
312 | ///
313 | /// Verifies whether the current class provides a property with a given
314 | /// name. This method is only invoked in debug builds, and results in
315 | /// a runtime exception if the method
316 | /// is being invoked with an invalid property name. This may happen if
317 | /// a property's name was changed but not the parameter of the property's
318 | /// invocation of .
319 | ///
320 | /// The name of the changed property.
321 | [System.Diagnostics.Conditional("DEBUG")]
322 | private void VerifyProperty(string propertyName)
323 | {
324 | Type type = this.GetType();
325 |
326 | // Look for a *public* property with the specified name
327 | System.Reflection.PropertyInfo pi = type.GetProperty(propertyName);
328 | if (pi == null)
329 | {
330 | // There is no matching property - notify the developer
331 | string msg = "OnPropertyChanged was invoked with invalid " +
332 | "property name {0}. {0} is not a public " +
333 | "property of {1}.";
334 | msg = String.Format(msg, propertyName, type.FullName);
335 | System.Diagnostics.Debug.Assert(false, msg);
336 | }
337 | }
338 |
339 |
340 |
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Resources;
3 | using System.Runtime.CompilerServices;
4 | using System.Runtime.InteropServices;
5 | using System.Windows;
6 |
7 | // General Information about an assembly is controlled through the following
8 | // set of attributes. Change these attribute values to modify the information
9 | // associated with an assembly.
10 | [assembly: AssemblyTitle("Monitored Undo Framework Sample")]
11 | [assembly: AssemblyDescription("Monitored Undo Framework Sample")]
12 | [assembly: AssemblyConfiguration("")]
13 | [assembly: AssemblyCompany("")]
14 | [assembly: AssemblyProduct("Monitored Undo Framework")]
15 | [assembly: AssemblyCopyright("Copyright © Alner LLC 2020")]
16 | [assembly: AssemblyTrademark("")]
17 | [assembly: AssemblyCulture("")]
18 |
19 | // Setting ComVisible to false makes the types in this assembly not visible
20 | // to COM components. If you need to access a type in this assembly from
21 | // COM, set the ComVisible attribute to true on that type.
22 | [assembly: ComVisible(false)]
23 |
24 | //In order to begin building localizable applications, set
25 | //CultureYouAreCodingWith in your .csproj file
26 | //inside a . For example, if you are using US english
27 | //in your source files, set the to en-US. Then uncomment
28 | //the NeutralResourceLanguage attribute below. Update the "en-US" in
29 | //the line below to match the UICulture setting in the project file.
30 |
31 | //[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
32 |
33 |
34 | [assembly: ThemeInfo(
35 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
36 | //(used if a resource is not found in the page,
37 | // or application resource dictionaries)
38 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
39 | //(used if a resource is not found in the page,
40 | // app, or any theme specific resource dictionaries)
41 | )]
42 |
43 |
44 | // Version information for an assembly consists of the following four values:
45 | //
46 | // Major Version
47 | // Minor Version
48 | // Build Number
49 | // Revision
50 | //
51 | // You can specify all the values or you can default the Build and Revision Numbers
52 | // by using the '*' as shown below:
53 | // [assembly: AssemblyVersion("1.0.*")]
54 | [assembly: AssemblyVersion("2.0.0.0")]
55 | [assembly: AssemblyFileVersion("2.0.0.0")]
56 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/Properties/Resources.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace WpfUndoSample.Properties {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | internal class Resources {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal Resources() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | internal static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WpfUndoSample.Properties.Resources", typeof(Resources).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | internal static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/Properties/Resources.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | text/microsoft-resx
107 |
108 |
109 | 2.0
110 |
111 |
112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
113 |
114 |
115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/Properties/Settings.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace WpfUndoSample.Properties {
12 |
13 |
14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")]
16 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
17 |
18 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
19 |
20 | public static Settings Default {
21 | get {
22 | return defaultInstance;
23 | }
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/Properties/Settings.settings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/WpfUndoSample.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Debug
5 | x86
6 | 8.0.30703
7 | 2.0
8 | {5AF2EE5C-BE23-49A6-A850-FE04BB0DAF6D}
9 | WinExe
10 | Properties
11 | WpfUndoSample
12 | WpfUndoSample
13 | v4.6.2
14 |
15 |
16 | 512
17 | {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
18 | 4
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | x86
30 | true
31 | full
32 | false
33 | bin\Debug\
34 | DEBUG;TRACE
35 | prompt
36 | 4
37 | false
38 |
39 |
40 | x86
41 | pdbonly
42 | true
43 | bin\Release\
44 | TRACE
45 | prompt
46 | 4
47 | false
48 |
49 |
50 | true
51 | bin\Debug\
52 | DEBUG;TRACE
53 | full
54 | AnyCPU
55 | 7.3
56 | prompt
57 | MinimumRecommendedRules.ruleset
58 |
59 |
60 | bin\Release\
61 | TRACE
62 | true
63 | pdbonly
64 | AnyCPU
65 | 7.3
66 | prompt
67 | MinimumRecommendedRules.ruleset
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 | 4.0
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | MSBuild:Compile
87 | Designer
88 |
89 |
90 | MSBuild:Compile
91 | Designer
92 |
93 |
94 | App.xaml
95 | Code
96 |
97 |
98 | MainWindow.xaml
99 | Code
100 |
101 |
102 |
103 |
104 | Code
105 |
106 |
107 | True
108 | True
109 | Resources.resx
110 |
111 |
112 | True
113 | Settings.settings
114 | True
115 |
116 |
117 | ResXFileCodeGenerator
118 | Resources.Designer.cs
119 |
120 |
121 |
122 | SettingsSingleFileGenerator
123 | Settings.Designer.cs
124 |
125 |
126 |
127 |
128 |
129 | {FF127A9A-BACB-4BC3-99DE-9F80275A06D4}
130 | MonitoredUndo
131 |
132 |
133 |
134 |
141 |
--------------------------------------------------------------------------------
/samples/WpfUndoSample/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/App.xaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/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.Threading.Tasks;
7 | using System.Windows;
8 |
9 | namespace WpfUndoSampleMVVM.Core
10 | {
11 | ///
12 | /// Interaction logic for App.xaml
13 | ///
14 | public partial class App : Application
15 | {
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 |
3 | [assembly: ThemeInfo(
4 | ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
5 | //(used if a resource is not found in the page,
6 | // or application resource dictionaries)
7 | ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
8 | //(used if a resource is not found in the page,
9 | // app, or any theme specific resource dictionaries)
10 | )]
11 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/AttachedProperties.cs:
--------------------------------------------------------------------------------
1 | using System.Windows;
2 | using System.Windows.Input;
3 |
4 | namespace WpfUndoSampleMVVM.Core
5 | {
6 | public class AttachedProperties
7 | {
8 | public static DependencyProperty RegisterCommandBindingsProperty =
9 | DependencyProperty.RegisterAttached("RegisterCommandBindings", typeof(CommandBindingCollection), typeof(AttachedProperties), new PropertyMetadata(null, OnRegisterCommandBindingChanged));
10 |
11 | public static void SetRegisterCommandBindings(UIElement element, CommandBindingCollection value)
12 | {
13 | if (element != null)
14 | element.SetValue(RegisterCommandBindingsProperty, value);
15 | }
16 |
17 | public static CommandBindingCollection GetRegisterCommandBindings(UIElement element)
18 | {
19 | return (element != null ? (CommandBindingCollection)element.GetValue(RegisterCommandBindingsProperty) : null);
20 | }
21 |
22 | private static void OnRegisterCommandBindingChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
23 | {
24 | UIElement element = sender as UIElement;
25 | if (element != null)
26 | {
27 | CommandBindingCollection bindings = e.NewValue as CommandBindingCollection;
28 | if (bindings != null)
29 | {
30 | element.CommandBindings.AddRange(bindings);
31 | }
32 | }
33 | }
34 |
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/EventToCommand.cs:
--------------------------------------------------------------------------------
1 |
2 | using Microsoft.Xaml.Behaviors;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Text;
6 | using System.Windows;
7 | using System.Windows.Input;
8 |
9 | namespace WpfUndoSampleMVVM.Core
10 | {
11 | ///
12 | /// This can be
13 | /// used to bind any event on any FrameworkElement to an .
14 | /// Typically, this element is used in XAML to connect the attached element
15 | /// to a command located in a ViewModel. This trigger can only be attached
16 | /// to a FrameworkElement or a class deriving from FrameworkElement.
17 | /// To access the EventArgs of the fired event, use a RelayCommand<EventArgs>
18 | /// and leave the CommandParameter and CommandParameterValue empty!
19 | ///
20 | ////[ClassInfo(typeof(EventToCommand),
21 | //// VersionString = "5.2.8",
22 | //// DateString = "201504252130",
23 | //// Description = "A Trigger used to bind any event to an ICommand.",
24 | //// UrlContacts = "http://www.galasoft.ch/contact_en.html",
25 | //// Email = "laurent@galasoft.ch")]
26 | public class EventToCommand : TriggerAction
27 | {
28 | ///
29 | /// Identifies the dependency property
30 | ///
31 | public static readonly DependencyProperty CommandParameterProperty = DependencyProperty.Register(
32 | "CommandParameter",
33 | typeof(object),
34 | typeof(EventToCommand),
35 | new PropertyMetadata(
36 | null,
37 | (s, e) =>
38 | {
39 | var sender = s as EventToCommand;
40 | if (sender == null)
41 | {
42 | return;
43 | }
44 |
45 | if (sender.AssociatedObject == null)
46 | {
47 | return;
48 | }
49 |
50 | sender.EnableDisableElement();
51 | }));
52 |
53 | ///
54 | /// Identifies the dependency property
55 | ///
56 | public static readonly DependencyProperty CommandProperty = DependencyProperty.Register(
57 | "Command",
58 | typeof(ICommand),
59 | typeof(EventToCommand),
60 | new PropertyMetadata(
61 | null,
62 | (s, e) => OnCommandChanged(s as EventToCommand, e)));
63 |
64 | ///
65 | /// Identifies the dependency property
66 | ///
67 | public static readonly DependencyProperty MustToggleIsEnabledProperty = DependencyProperty.Register(
68 | "MustToggleIsEnabled",
69 | typeof(bool),
70 | typeof(EventToCommand),
71 | new PropertyMetadata(
72 | false,
73 | (s, e) =>
74 | {
75 | var sender = s as EventToCommand;
76 | if (sender == null)
77 | {
78 | return;
79 | }
80 |
81 | if (sender.AssociatedObject == null)
82 | {
83 | return;
84 | }
85 |
86 | sender.EnableDisableElement();
87 | }));
88 |
89 | private object _commandParameterValue;
90 |
91 | private bool? _mustToggleValue;
92 |
93 | ///
94 | /// Gets or sets the ICommand that this trigger is bound to. This
95 | /// is a DependencyProperty.
96 | ///
97 | public ICommand Command
98 | {
99 | get
100 | {
101 | return (ICommand)GetValue(CommandProperty);
102 | }
103 |
104 | set
105 | {
106 | SetValue(CommandProperty, value);
107 | }
108 | }
109 |
110 | ///
111 | /// Gets or sets an object that will be passed to the
112 | /// attached to this trigger. This is a DependencyProperty.
113 | ///
114 | public object CommandParameter
115 | {
116 | get
117 | {
118 | return GetValue(CommandParameterProperty);
119 | }
120 |
121 | set
122 | {
123 | SetValue(CommandParameterProperty, value);
124 | }
125 | }
126 |
127 | ///
128 | /// Gets or sets an object that will be passed to the
129 | /// attached to this trigger. This property is here for compatibility
130 | /// with the Silverlight version. This is NOT a DependencyProperty.
131 | /// For databinding, use the property.
132 | ///
133 | public object CommandParameterValue
134 | {
135 | get
136 | {
137 | return _commandParameterValue ?? CommandParameter;
138 | }
139 |
140 | set
141 | {
142 | _commandParameterValue = value;
143 | EnableDisableElement();
144 | }
145 | }
146 |
147 | ///
148 | /// Gets or sets a value indicating whether the attached element must be
149 | /// disabled when the property's CanExecuteChanged
150 | /// event fires. If this property is true, and the command's CanExecute
151 | /// method returns false, the element will be disabled. If this property
152 | /// is false, the element will not be disabled when the command's
153 | /// CanExecute method changes. This is a DependencyProperty.
154 | ///
155 | public bool MustToggleIsEnabled
156 | {
157 | get
158 | {
159 | return (bool)GetValue(MustToggleIsEnabledProperty);
160 | }
161 |
162 | set
163 | {
164 | SetValue(MustToggleIsEnabledProperty, value);
165 | }
166 | }
167 |
168 | ///
169 | /// Gets or sets a value indicating whether the attached element must be
170 | /// disabled when the property's CanExecuteChanged
171 | /// event fires. If this property is true, and the command's CanExecute
172 | /// method returns false, the element will be disabled. This property is here for
173 | /// compatibility with the Silverlight version. This is NOT a DependencyProperty.
174 | /// For databinding, use the property.
175 | ///
176 | public bool MustToggleIsEnabledValue
177 | {
178 | get
179 | {
180 | return _mustToggleValue == null
181 | ? MustToggleIsEnabled
182 | : _mustToggleValue.Value;
183 | }
184 |
185 | set
186 | {
187 | _mustToggleValue = value;
188 | EnableDisableElement();
189 | }
190 | }
191 |
192 | ///
193 | /// Called when this trigger is attached to a FrameworkElement.
194 | ///
195 | protected override void OnAttached()
196 | {
197 | base.OnAttached();
198 | EnableDisableElement();
199 | }
200 |
201 | ///
202 | /// This method is here for compatibility
203 | /// with the Silverlight version.
204 | ///
205 | /// The FrameworkElement to which this trigger
206 | /// is attached.
207 | private FrameworkElement GetAssociatedObject()
208 | {
209 | return AssociatedObject as FrameworkElement;
210 | }
211 |
212 | ///
213 | /// This method is here for compatibility
214 | /// with the Silverlight 3 version.
215 | ///
216 | /// The command that must be executed when
217 | /// this trigger is invoked.
218 | private ICommand GetCommand()
219 | {
220 | return Command;
221 | }
222 |
223 | ///
224 | /// Specifies whether the EventArgs of the event that triggered this
225 | /// action should be passed to the bound RelayCommand. If this is true,
226 | /// the command should accept arguments of the corresponding
227 | /// type (for example RelayCommand<MouseButtonEventArgs>).
228 | ///
229 | public bool PassEventArgsToCommand
230 | {
231 | get;
232 | set;
233 | }
234 |
235 | ///
236 | /// Gets or sets a converter used to convert the EventArgs when using
237 | /// . If PassEventArgsToCommand is false,
238 | /// this property is never used.
239 | ///
240 | public IEventArgsConverter EventArgsConverter
241 | {
242 | get;
243 | set;
244 | }
245 |
246 | ///
247 | /// The dependency property's name.
248 | ///
249 | public const string EventArgsConverterParameterPropertyName = "EventArgsConverterParameter";
250 |
251 | ///
252 | /// Gets or sets a parameters for the converter used to convert the EventArgs when using
253 | /// . If PassEventArgsToCommand is false,
254 | /// this property is never used. This is a dependency property.
255 | ///
256 | public object EventArgsConverterParameter
257 | {
258 | get
259 | {
260 | return GetValue(EventArgsConverterParameterProperty);
261 | }
262 | set
263 | {
264 | SetValue(EventArgsConverterParameterProperty, value);
265 | }
266 | }
267 |
268 | ///
269 | /// Identifies the dependency property.
270 | ///
271 | public static readonly DependencyProperty EventArgsConverterParameterProperty = DependencyProperty.Register(
272 | EventArgsConverterParameterPropertyName,
273 | typeof(object),
274 | typeof(EventToCommand),
275 | new PropertyMetadata(null));
276 |
277 | ///
278 | /// The dependency property's name.
279 | ///
280 | public const string AlwaysInvokeCommandPropertyName = "AlwaysInvokeCommand";
281 |
282 | ///
283 | /// Gets or sets a value indicating if the command should be invoked even
284 | /// if the attached control is disabled. This is a dependency property.
285 | ///
286 | public bool AlwaysInvokeCommand
287 | {
288 | get
289 | {
290 | return (bool)GetValue(AlwaysInvokeCommandProperty);
291 | }
292 | set
293 | {
294 | SetValue(AlwaysInvokeCommandProperty, value);
295 | }
296 | }
297 |
298 | ///
299 | /// Identifies the dependency property.
300 | ///
301 | public static readonly DependencyProperty AlwaysInvokeCommandProperty = DependencyProperty.Register(
302 | AlwaysInvokeCommandPropertyName,
303 | typeof(bool),
304 | typeof(EventToCommand),
305 | new PropertyMetadata(false));
306 |
307 |
308 | ///
309 | /// Provides a simple way to invoke this trigger programatically
310 | /// without any EventArgs.
311 | ///
312 | public void Invoke()
313 | {
314 | Invoke(null);
315 | }
316 |
317 | ///
318 | /// Executes the trigger.
319 | /// To access the EventArgs of the fired event, use a RelayCommand<EventArgs>
320 | /// and leave the CommandParameter and CommandParameterValue empty!
321 | ///
322 | /// The EventArgs of the fired event.
323 | protected override void Invoke(object parameter)
324 | {
325 | if (AssociatedElementIsDisabled()
326 | && !AlwaysInvokeCommand)
327 | {
328 | return;
329 | }
330 |
331 | var command = GetCommand();
332 | var commandParameter = CommandParameterValue;
333 |
334 | if (commandParameter == null
335 | && PassEventArgsToCommand)
336 | {
337 | commandParameter = EventArgsConverter == null
338 | ? parameter
339 | : EventArgsConverter.Convert(parameter, EventArgsConverterParameter);
340 | }
341 |
342 | if (command != null
343 | && command.CanExecute(commandParameter))
344 | {
345 | command.Execute(commandParameter);
346 | }
347 | }
348 |
349 | private static void OnCommandChanged(
350 | EventToCommand element,
351 | DependencyPropertyChangedEventArgs e)
352 | {
353 | if (element == null)
354 | {
355 | return;
356 | }
357 |
358 | if (e.OldValue != null)
359 | {
360 | ((ICommand)e.OldValue).CanExecuteChanged -= element.OnCommandCanExecuteChanged;
361 | }
362 |
363 | var command = (ICommand)e.NewValue;
364 |
365 | if (command != null)
366 | {
367 | command.CanExecuteChanged += element.OnCommandCanExecuteChanged;
368 | }
369 |
370 | element.EnableDisableElement();
371 | }
372 |
373 | private bool AssociatedElementIsDisabled()
374 | {
375 | var element = GetAssociatedObject();
376 |
377 | return AssociatedObject == null
378 | || (element != null
379 | && !element.IsEnabled);
380 | }
381 |
382 | private void EnableDisableElement()
383 | {
384 | var element = GetAssociatedObject();
385 |
386 | if (element == null)
387 | {
388 | return;
389 | }
390 |
391 | var command = GetCommand();
392 |
393 | if (MustToggleIsEnabledValue
394 | && command != null)
395 | {
396 | element.IsEnabled = command.CanExecute(CommandParameterValue);
397 | }
398 | }
399 |
400 | private void OnCommandCanExecuteChanged(object sender, EventArgs e)
401 | {
402 | EnableDisableElement();
403 | }
404 | }
405 | }
406 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/IEventArgsConverter.cs:
--------------------------------------------------------------------------------
1 | namespace WpfUndoSampleMVVM.Core
2 | {
3 | ///
4 | /// The definition of the converter used to convert an EventArgs
5 | /// in the class, if the
6 | /// property is true.
7 | /// Set an instance of this class to the
8 | /// property of the EventToCommand instance.
9 | ///
10 | ////[ClassInfo(typeof(EventToCommand))]
11 | public interface IEventArgsConverter
12 | {
13 | ///
14 | /// The method used to convert the EventArgs instance.
15 | ///
16 | /// An instance of EventArgs passed by the
17 | /// event that the EventToCommand instance is handling.
18 | /// An optional parameter used for the conversion. Use
19 | /// the property
20 | /// to set this value. This may be null.
21 | /// The converted value.
22 | object Convert(object value, object parameter);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/MainWindow.xaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
43 |
44 |
45 |
53 |
54 |
55 |
62 |
63 |
64 |
67 |
68 |
76 |
77 |
83 |
84 |
85 |
87 |
88 |
89 |
91 |
92 |
93 |
94 |
95 |
96 |
99 |
100 |
101 |
102 |
103 |
108 |
109 |
110 |
111 |
113 |
114 |
116 |
117 |
118 |
119 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/MainWindow.xaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 | using System.Windows;
7 | using System.Windows.Controls;
8 | using System.Windows.Data;
9 | using System.Windows.Documents;
10 | using System.Windows.Input;
11 | using System.Windows.Media;
12 | using System.Windows.Media.Imaging;
13 | using System.Windows.Navigation;
14 | using System.Windows.Shapes;
15 |
16 | namespace WpfUndoSampleMVVM.Core
17 | {
18 | ///
19 | /// Interaction logic for MainWindow.xaml
20 | ///
21 | public partial class MainWindow : Window
22 | {
23 | public MainWindow()
24 | {
25 | InitializeComponent();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/MainWindowViewModel.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.ComponentModel;
4 | using System.Windows.Data;
5 | using System.Windows.Input;
6 | using GalaSoft.MvvmLight.Command;
7 | using MonitoredUndo;
8 |
9 | namespace WpfUndoSampleMVVM.Core
10 | {
11 | public class MainWindowViewModel : INotifyPropertyChanged, ISupportsUndo
12 | {
13 | private CommandBindingCollection _commandBindings = new CommandBindingCollection();
14 |
15 | private ICommand _windowLoadedCommand;
16 | private ICommand _sliderMouseDownCommand;
17 | private ICommand _sliderLostMouseCapture;
18 |
19 | public CommandBindingCollection RegisterCommandBindings
20 | {
21 | get
22 | {
23 | return _commandBindings;
24 | }
25 | }
26 |
27 | public MainWindowViewModel()
28 | {
29 | InitialiseCommandBindings();
30 | }
31 |
32 | public ICommand WindowLoadedCommand
33 | {
34 | get
35 | {
36 | return _windowLoadedCommand ?? (_windowLoadedCommand = new RelayCommand(OnWindowLoaded));
37 | }
38 | }
39 |
40 | public ICommand SliderMouseDownCommand
41 | {
42 | get
43 | {
44 | return _sliderMouseDownCommand ??(_sliderMouseDownCommand = new RelayCommand(OnSliderMouseDown));
45 | }
46 | }
47 |
48 | public ICommand SliderLostMouseCapture
49 | {
50 | get
51 | {
52 | return _sliderLostMouseCapture ?? (_sliderLostMouseCapture = new RelayCommand(OnSliderLostMouseCapture));
53 | }
54 | }
55 |
56 | private void OnSliderLostMouseCapture(MouseEventArgs e)
57 | {
58 | if (!BatchAgeChanges)
59 | return;
60 |
61 | UndoService.Current[this].EndChangeSetBatch();
62 |
63 | e.Handled = false;
64 | }
65 |
66 | private void OnSliderMouseDown(MouseButtonEventArgs e)
67 | {
68 | if (!BatchAgeChanges)
69 | return;
70 |
71 | // Start a batch to collect all subsequent undo events (for this root)
72 | // into a single changeset.
73 | //
74 | // Passing "false" for the last parameter tells the system to keep
75 | // each individual change that is made. If desired, pass "true" to
76 | // de-dupe these changes and reduce the memory requirements of the
77 | // changeset.
78 | UndoService.Current[this].BeginChangeSetBatch("Age Changed", false);
79 |
80 | e.Handled = false;
81 | }
82 |
83 | private void OnWindowLoaded()
84 | {
85 | // The undo / redo stack collections are not "Observable", so we
86 | // need to manually refresh the UI when they change.
87 | var root = UndoService.Current[this];
88 | root.UndoStackChanged += new EventHandler(OnUndoStackChanged);
89 | root.RedoStackChanged += new EventHandler(OnRedoStackChanged);
90 | //FirstNameTextbox.Focus();
91 | }
92 |
93 | // Refresh the UI when the undo stack changes.
94 | void OnUndoStackChanged(object sender, EventArgs e)
95 | {
96 | RefreshUndoStackList();
97 | }
98 |
99 | // Refresh the UI when the redo stack changes.
100 | void OnRedoStackChanged(object sender, EventArgs e)
101 | {
102 | RefreshUndoStackList();
103 | }
104 |
105 |
106 |
107 | // Below are properties bound to the UI with a XAML binding.
108 | // NOTE that these properly implement INotifyPropertyChange.
109 | // This is critical if the UI is going to stay in sync
110 | // with the changes to the data.
111 |
112 |
113 | private string _FirstName;
114 | public string FirstName
115 | {
116 | get { return _FirstName; }
117 | set
118 | {
119 | if (value == _FirstName)
120 | return;
121 |
122 | // Store this change in the Undo system.
123 | // This uses the "DefaultChangeFactory" to construct the change, but you can
124 | // store changes any way you like.
125 | DefaultChangeFactory.Current.OnChanging(this, "FirstName", _FirstName, value, "First Name Changed");
126 |
127 | _FirstName = value;
128 | OnPropertyChanged("FirstName"); // Tells the UI that this property has changed.
129 | OnPropertyChanged("FullName"); // If FirstName changes, then FullName is also affected.
130 | }
131 | }
132 |
133 | private string _LastName;
134 | public string LastName
135 | {
136 | get { return _LastName; }
137 | set
138 | {
139 | if (value == _LastName)
140 | return;
141 |
142 | // Store this change in the Undo system.
143 | // This uses the "DefaultChangeFactory" to construct the change, but you can
144 | // store changes any way you like.
145 | DefaultChangeFactory.Current.OnChanging(this, "LastName", _LastName, value, "Last Name Changed");
146 |
147 | _LastName = value;
148 | OnPropertyChanged("LastName"); // Tells the UI that this property changed.
149 | OnPropertyChanged("FullName"); // If LastName changes, then FullName is also affected.
150 | }
151 | }
152 |
153 | public string FullName
154 | {
155 | get
156 | {
157 | return String.Format("{0} {1}", FirstName, LastName);
158 | }
159 | }
160 |
161 |
162 | private int _Age;
163 | public int Age
164 | {
165 | get { return _Age; }
166 | set
167 | {
168 | if (value == _Age)
169 | return;
170 |
171 | // Store this change in the Undo system.
172 | // This uses the "DefaultChangeFactory" to construct the change, but you can
173 | // store changes any way you like.
174 | DefaultChangeFactory.Current.OnChanging(this, "Age", _Age, value, "Age Changed");
175 |
176 | _Age = value;
177 | OnPropertyChanged("Age");
178 | }
179 | }
180 |
181 |
182 | private bool _BatchAgeChanges = true;
183 | public bool BatchAgeChanges
184 | {
185 | get { return _BatchAgeChanges; }
186 | set
187 | {
188 | if (value == _BatchAgeChanges)
189 | return;
190 |
191 | _BatchAgeChanges = value;
192 | OnPropertyChanged("BatchAgeChanges");
193 | }
194 | }
195 |
196 |
197 | // Expose the undo and redo stacks to the UI for binding.
198 |
199 | public IEnumerable UndoStack
200 | {
201 | get
202 | {
203 | return UndoService.Current[this].UndoStack;
204 |
205 | }
206 | }
207 |
208 | public IEnumerable RedoStack
209 | {
210 | get
211 | {
212 | return UndoService.Current[this].RedoStack;
213 |
214 | }
215 | }
216 |
217 |
218 |
219 |
220 |
221 | // Refresh the UI when the undo / redo stacks change.
222 | private void RefreshUndoStackList()
223 | {
224 | // Calling refresh on the CollectionView will tell the UI to rebind the list.
225 | // If the list were an ObservableCollection, or implemented INotifyCollectionChanged, this would not be needed.
226 | var cv = CollectionViewSource.GetDefaultView(UndoStack);
227 | cv.Refresh();
228 |
229 | cv = CollectionViewSource.GetDefaultView(RedoStack);
230 | cv.Refresh();
231 | }
232 |
233 |
234 |
235 | private void InitialiseCommandBindings()
236 | {
237 | // create command binding for undo command
238 | var undoBinding = new CommandBinding(ApplicationCommands.Undo, UndoExecuted, UndoCanExecute);
239 | var redoBinding = new CommandBinding(ApplicationCommands.Redo, RedoExecuted, RedoCanExecute);
240 |
241 | // register the binding to the class
242 | CommandManager.RegisterClassCommandBinding(typeof(MainWindowViewModel), undoBinding);
243 | CommandManager.RegisterClassCommandBinding(typeof(MainWindowViewModel), redoBinding);
244 |
245 | CommandBindings.Add(undoBinding);
246 | CommandBindings.Add(redoBinding);
247 | }
248 |
249 | private void RedoExecuted(object sender, ExecutedRoutedEventArgs e)
250 | {
251 | // A shorthand version of the above call to Undo, except
252 | // that this calls Redo.
253 | UndoService.Current[this].Redo();
254 | }
255 |
256 | private void RedoCanExecute(object sender, CanExecuteRoutedEventArgs e)
257 | {
258 | // Tell the UI whether Redo is available.
259 | e.CanExecute = UndoService.Current[this].CanRedo;
260 | }
261 |
262 | private void UndoExecuted(object sender, ExecutedRoutedEventArgs e)
263 | {
264 | // Get the document root. In this case, we pass in "this", which
265 | // implements ISupportsUndo. The ISupportsUndo interface is used
266 | // by the UndoService to locate the appropriate root node of an
267 | // undoable document.
268 | // In this case, we are treating the window as the root of the undoable
269 | // document, but in a larger system the root would probably be your
270 | // domain model.
271 | var undoRoot = UndoService.Current[this];
272 | undoRoot.Undo();
273 | }
274 |
275 | private void UndoCanExecute(object sender, CanExecuteRoutedEventArgs e)
276 | {
277 | // Tell the UI whether Undo is available.
278 | e.CanExecute = UndoService.Current[this].CanUndo;
279 | }
280 |
281 | public CommandBindingCollection CommandBindings
282 | {
283 | get
284 | {
285 | return _commandBindings;
286 | }
287 | }
288 |
289 |
290 |
291 |
292 | ///
293 | /// The PropertyChanged event is used by consuming code
294 | /// (like WPF's binding infrastructure) to detect when
295 | /// a value has changed.
296 | ///
297 | public event PropertyChangedEventHandler PropertyChanged;
298 |
299 | ///
300 | /// Raise the PropertyChanged event for the
301 | /// specified property.
302 | ///
303 | ///
304 | /// A string representing the name of
305 | /// the property that changed.
306 | ///
307 | /// Only raise the event if the value of the property
308 | /// has changed from its previous value
309 | protected void OnPropertyChanged(string propertyName)
310 | {
311 | // Validate the property name in debug builds
312 | VerifyProperty(propertyName);
313 |
314 | if (null != PropertyChanged)
315 | {
316 | PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
317 | }
318 | }
319 |
320 | ///
321 | /// Verifies whether the current class provides a property with a given
322 | /// name. This method is only invoked in debug builds, and results in
323 | /// a runtime exception if the method
324 | /// is being invoked with an invalid property name. This may happen if
325 | /// a property's name was changed but not the parameter of the property's
326 | /// invocation of .
327 | ///
328 | /// The name of the changed property.
329 | [System.Diagnostics.Conditional("DEBUG")]
330 | private void VerifyProperty(string propertyName)
331 | {
332 | Type type = this.GetType();
333 |
334 | // Look for a *public* property with the specified name
335 | System.Reflection.PropertyInfo pi = type.GetProperty(propertyName);
336 | if (pi == null)
337 | {
338 | // There is no matching property - notify the developer
339 | string msg = "OnPropertyChanged was invoked with invalid " +
340 | "property name {0}. {0} is not a public " +
341 | "property of {1}.";
342 | msg = String.Format(msg, propertyName, type.FullName);
343 | System.Diagnostics.Debug.Assert(false, msg);
344 | }
345 | }
346 |
347 |
348 |
349 |
350 |
351 | // This interface is needed on objects that are part of the
352 | // document hierarchy. It allows this object to be passed
353 | // in to the UndoService.Current[] indexer.
354 | // This method should return the "root" of the document.
355 | // In this case, we are treating the window as the "root" of the
356 | // document. In other cases, the root might be your domain model.
357 | //
358 | // See the unit tests for a better example of how this comes into
359 | // use for a multi-object document hierarchy.
360 |
361 | public object GetUndoRoot()
362 | {
363 | return this;
364 | }
365 |
366 |
367 | }
368 | }
369 |
--------------------------------------------------------------------------------
/samples/WpfUndoSampleMVVM.Core/WpfUndoSampleMVVM.Core.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | WinExe
5 | netcoreapp3.1
6 | true
7 |
8 | false
9 |
10 | WpfUndoSampleMVVM.Core
11 | $(RootNamespace)
12 |
13 |
14 |
15 | Nathan Allen-Wagner, CGrisselin
16 | Monitored Undo Framework - .NET Core MVVM Sample
17 | Monitored Undo Framework - .NET Core MVVM Sample
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/.gitignore:
--------------------------------------------------------------------------------
1 | /report
2 | /packages
3 | /TestResults
4 |
5 |
--------------------------------------------------------------------------------
/src/MonitoredUndo/ChangeFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Collections.Specialized;
6 | using System.Collections;
7 | using System.Globalization;
8 | using System.Collections.ObjectModel;
9 | using System.Reflection;
10 | using MonitoredUndo.Changes;
11 |
12 | namespace MonitoredUndo
13 | {
14 |
15 | public class ChangeFactory
16 | {
17 |
18 | public bool ThrowExceptionOnCollectionResets
19 | {
20 | get { return _ThrowExceptionOnCollectionResets; }
21 | set { _ThrowExceptionOnCollectionResets = value; }
22 | }
23 | private bool _ThrowExceptionOnCollectionResets = true;
24 |
25 |
26 | ///
27 | /// Construct a Change instance with actions for undo / redo.
28 | ///
29 | /// The instance that changed.
30 | /// The property name that changed. (Case sensitive, used by reflection.)
31 | /// The old value of the property.
32 | /// The new value of the property.
33 | /// A Change that can be added to the UndoRoot's undo stack.
34 | public virtual Change GetChange(object instance, string propertyName, object oldValue, object newValue)
35 | {
36 | var undoMetadata = instance as IUndoMetadata;
37 | if (null != undoMetadata)
38 | {
39 | if (!undoMetadata.CanUndoProperty(propertyName, oldValue, newValue))
40 | return null;
41 | }
42 |
43 | var change = new PropertyChange(instance, propertyName, oldValue, newValue);
44 |
45 | return change;
46 | }
47 |
48 | ///
49 | /// Construct a Change instance with actions for undo / redo.
50 | ///
51 | /// The instance that changed.
52 | /// The property name that changed. (Case sensitive, used by reflection.)
53 | /// The old value of the property.
54 | /// The new value of the property.
55 | public virtual void OnChanging(object instance, string propertyName, object oldValue, object newValue)
56 | {
57 | OnChanging(instance, propertyName, oldValue, newValue, propertyName);
58 | }
59 |
60 | ///
61 | /// Construct a Change instance with actions for undo / redo.
62 | ///
63 | /// The instance that changed.
64 | /// The property name that changed. (Case sensitive, used by reflection.)
65 | /// The old value of the property.
66 | /// The new value of the property.
67 | /// A description of this change.
68 | public virtual void OnChanging(object instance, string propertyName, object oldValue, object newValue, string descriptionOfChange)
69 | {
70 | var supportsUndo = instance as ISupportsUndo;
71 | if (null == supportsUndo)
72 | return;
73 |
74 | var root = supportsUndo.GetUndoRoot();
75 | if (null == root)
76 | return;
77 |
78 | Change change = GetChange(instance, propertyName, oldValue, newValue);
79 |
80 | UndoService.Current[root].AddChange(change, descriptionOfChange);
81 | }
82 |
83 | ///
84 | /// Construct a Change instance with actions for undo / redo.
85 | ///
86 | /// The instance that changed.
87 | /// The property name that exposes the collection that changed. (Case sensitive, used by reflection.)
88 | /// The collection that had an item added / removed.
89 | /// The NotifyCollectionChangedEventArgs event args parameter, with info about the collection change.
90 | /// A Change that can be added to the UndoRoot's undo stack.
91 | public virtual IList GetCollectionChange(object instance, string propertyName, object collection, NotifyCollectionChangedEventArgs e)
92 | {
93 | var undoMetadata = instance as IUndoMetadata;
94 | if (null != undoMetadata)
95 | {
96 | if (!undoMetadata.CanUndoCollectionChange(propertyName, collection, e))
97 | return null;
98 | }
99 |
100 | var ret = new List();
101 |
102 | switch (e.Action)
103 | {
104 | case NotifyCollectionChangedAction.Add:
105 | foreach (var item in e.NewItems)
106 | {
107 | Change change = null;
108 | if (collection as IList != null)
109 | {
110 | change = new CollectionAddChange(instance, propertyName, (IList)collection,
111 | e.NewStartingIndex, item);
112 | }
113 | else if (collection as IDictionary != null)
114 | {
115 | // item is a key value pair - get key and value to be recorded in dictionary change
116 | var keyProperty = item.GetType().GetProperty("Key");
117 | var valueProperty = item.GetType().GetProperty("Value");
118 | change = new DictionaryAddChange(instance, propertyName, (IDictionary)collection,
119 | keyProperty.GetValue(item, null), valueProperty.GetValue(item, null));
120 | }
121 | ret.Add(change);
122 | }
123 |
124 | break;
125 |
126 | case NotifyCollectionChangedAction.Remove:
127 | foreach (var item in e.OldItems)
128 | {
129 | Change change = null;
130 | if (collection as IList != null)
131 | {
132 | change = new CollectionRemoveChange(instance, propertyName, (IList)collection,
133 | e.OldStartingIndex, item);
134 | }
135 | else if (collection as IDictionary != null)
136 | {
137 | // item is a key value pair - get key and value to be recorded in dictionary change
138 | var keyProperty = item.GetType().GetProperty("Key");
139 | var valueProperty = item.GetType().GetProperty("Value");
140 | change = new DictionaryRemoveChange(instance, propertyName, (IDictionary)collection,
141 | keyProperty.GetValue(item, null), valueProperty.GetValue(item, null));
142 | }
143 | ret.Add(change);
144 | }
145 |
146 | break;
147 |
148 | case NotifyCollectionChangedAction.Move:
149 | var moveChange = new CollectionMoveChange(instance, propertyName, (IList) collection,
150 | e.NewStartingIndex,
151 | e.OldStartingIndex);
152 | ret.Add(moveChange);
153 | break;
154 |
155 | case NotifyCollectionChangedAction.Replace:
156 | for (int i = 0; i < e.OldItems.Count; i++)
157 | {
158 | Change change = null;
159 |
160 | if (collection as IList != null)
161 | {
162 | change = new CollectionReplaceChange(instance, propertyName, (IList)collection,
163 | e.NewStartingIndex, e.OldItems[i], e.NewItems[i]);
164 | }
165 | else if (collection as IDictionary != null)
166 | {
167 | // item is a key value pair - get key and value to be recorded in dictionary change
168 | var keyProperty = e.OldItems[i].GetType().GetProperty("Key");
169 | var oldValueProperty = e.OldItems[i].GetType().GetProperty("Value");
170 | var newValueProperty = e.OldItems[i].GetType().GetProperty("Value");
171 | change = new DictionaryReplaceChange(
172 | instance, propertyName, (IDictionary)collection, keyProperty.GetValue(e.OldItems[i], null), oldValueProperty.GetValue(e.OldItems[i], null), newValueProperty.GetValue(e.NewItems[i], null));
173 | }
174 | ret.Add(change);
175 | }
176 | break;
177 |
178 | case NotifyCollectionChangedAction.Reset:
179 | if (ThrowExceptionOnCollectionResets)
180 | throw new NotSupportedException("Undoing collection resets is not supported via the CollectionChanged event. The collection is already null, so the Undo system has no way to capture the set of elements that were previously in the collection.");
181 | else
182 | break;
183 |
184 | default:
185 | throw new NotSupportedException();
186 | }
187 |
188 | return ret;
189 | }
190 |
191 | ///
192 | /// Construct a Change instance with actions for undo / redo.
193 | ///
194 | /// The instance that changed.
195 | /// The property name that exposes the collection that changed. (Case sensitive, used by reflection.)
196 | /// The collection that had an item added / removed.
197 | /// The NotifyCollectionChangedEventArgs event args parameter, with info about the collection change.
198 | public virtual void OnCollectionChanged(object instance, string propertyName, object collection, NotifyCollectionChangedEventArgs e)
199 | {
200 | OnCollectionChanged(instance, propertyName, collection, e, propertyName);
201 | }
202 |
203 | ///
204 | /// Construct a Change instance with actions for undo / redo.
205 | ///
206 | /// The instance that changed.
207 | /// The property name that exposes the collection that changed. (Case sensitive, used by reflection.)
208 | /// The collection that had an item added / removed.
209 | /// The NotifyCollectionChangedEventArgs event args parameter, with info about the collection change.
210 | /// A description of the change.
211 | public virtual void OnCollectionChanged(object instance, string propertyName, object collection, NotifyCollectionChangedEventArgs e, string descriptionOfChange)
212 | {
213 | var supportsUndo = instance as ISupportsUndo;
214 | if (null == supportsUndo)
215 | return;
216 |
217 | var root = supportsUndo.GetUndoRoot();
218 | if (null == root)
219 | return;
220 |
221 | // Create the Change instances.
222 | var changes = GetCollectionChange(instance, propertyName, collection, e);
223 | if (null == changes)
224 | return;
225 |
226 | // Add the changes to the UndoRoot
227 | var undoRoot = UndoService.Current[root];
228 | foreach (var change in changes)
229 | {
230 | undoRoot.AddChange(change, descriptionOfChange);
231 | }
232 | }
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/src/MonitoredUndo/ChangeKey_T2.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Globalization;
6 |
7 | namespace MonitoredUndo
8 | {
9 |
10 | ///
11 | /// Used to uniquely identify a change that has a 2-part "key".
12 | ///
13 | public class ChangeKey
14 | {
15 | private T1 m_One;
16 | private T2 m_Two;
17 |
18 | public T1 Item1 { get { return m_One; } }
19 | public T2 Item2 { get { return m_Two; } }
20 |
21 | public ChangeKey(T1 item1, T2 item2)
22 | {
23 | m_One = item1;
24 | m_Two = item2;
25 | }
26 |
27 | public override bool Equals(object obj)
28 | {
29 | if (obj == null)
30 | {
31 | return false;
32 | }
33 | ChangeKey tuple = obj as ChangeKey;
34 | if (tuple == null)
35 | {
36 | return false;
37 | }
38 | if (object.Equals(this.m_One, tuple.m_One))
39 | {
40 | return object.Equals(this.m_Two, tuple.m_Two);
41 | }
42 | return false;
43 | }
44 |
45 | public override int GetHashCode()
46 | {
47 | return CombineHashCodes(m_One.GetHashCode(), m_Two.GetHashCode());
48 | }
49 |
50 | public override string ToString()
51 | {
52 | return string.Format(CultureInfo.CurrentCulture, "Tuple of '{0}', '{1}'", m_One, m_Two);
53 | }
54 |
55 |
56 | internal static int CombineHashCodes(int h1, int h2)
57 | {
58 | return ((h1 << 5) + h1) ^ h2;
59 | }
60 |
61 | }
62 |
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/MonitoredUndo/ChangeKey_T3.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Globalization;
6 |
7 | namespace MonitoredUndo
8 | {
9 |
10 | ///
11 | /// Used to uniquely identify a change that has a 3-part "key".
12 | ///
13 | public class ChangeKey
14 | {
15 | private T1 m_One;
16 | private T2 m_Two;
17 | private T3 m_Three;
18 |
19 | public T1 Item1 { get { return m_One; } }
20 | public T2 Item2 { get { return m_Two; } }
21 | public T3 Item3 { get { return m_Three; } }
22 |
23 | public ChangeKey(T1 item1, T2 item2, T3 item3)
24 | {
25 | m_One = item1;
26 | m_Two = item2;
27 | m_Three = item3;
28 | }
29 |
30 | public override bool Equals(object obj)
31 | {
32 | if (obj == null)
33 | {
34 | return false;
35 | }
36 | ChangeKey tuple = obj as ChangeKey;
37 | if (tuple == null)
38 | {
39 | return false;
40 | }
41 | if (object.Equals(this.m_One, tuple.m_One))
42 | {
43 | if (object.Equals(this.m_Two, tuple.m_Two))
44 | {
45 | return object.Equals(this.m_Three, tuple.m_Three);
46 | }
47 | }
48 | return false;
49 | }
50 |
51 | public override int GetHashCode()
52 | {
53 | return CombineHashCodes(m_One.GetHashCode(), CombineHashCodes(m_Two.GetHashCode(), m_Three.GetHashCode()));
54 | }
55 |
56 | public override string ToString()
57 | {
58 | return string.Format(CultureInfo.CurrentCulture, "Tuple of '{0}', '{1}', '{2}'", m_One, m_Two, m_Three);
59 | }
60 |
61 | internal static int CombineHashCodes(int h1, int h2)
62 | {
63 | return ((h1 << 5) + h1) ^ h2;
64 | }
65 |
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/MonitoredUndo/ChangeSet.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 |
6 | namespace MonitoredUndo
7 | {
8 |
9 | ///
10 | /// A set of changes that represent a single "unit of change".
11 | ///
12 | public class ChangeSet
13 | {
14 |
15 |
16 |
17 | private UndoRoot _UndoRoot;
18 | private string _Description;
19 | private IList _Changes;
20 | private bool _Undone = false;
21 |
22 |
23 |
24 |
25 |
26 | ///
27 | /// Create a ChangeSet for the specified UndoRoot.
28 | ///
29 | /// The UndoRoot that this ChangeSet belongs to.
30 | /// A description of the change.
31 | /// The Change instance that can perform the undo / redo as needed.
32 | public ChangeSet(UndoRoot undoRoot, string description, Change change)
33 | {
34 | _UndoRoot = undoRoot;
35 | _Changes = new List();
36 | _Description = description;
37 |
38 | if (null != change)
39 | AddChange(change);
40 | }
41 |
42 |
43 |
44 |
45 |
46 | ///
47 | /// The associated UndoRoot.
48 | ///
49 | public UndoRoot UndoRoot { get { return _UndoRoot; } }
50 |
51 | ///
52 | /// A description of this set of changes.
53 | ///
54 | public string Description { get { return _Description; } }
55 |
56 | ///
57 | /// Has this ChangeSet been undone.
58 | ///
59 | public bool Undone { get { return _Undone; } }
60 |
61 | ///
62 | /// The changes that are part of this ChangeSet
63 | ///
64 | public IEnumerable Changes
65 | {
66 | get
67 | {
68 | return _Changes;
69 | }
70 | }
71 |
72 |
73 |
74 |
75 |
76 | ///
77 | /// Add a change to this ChangeSet.
78 | ///
79 | ///
80 | internal void AddChange(Change change)
81 | {
82 | if (_UndoRoot.ConsolidateChangesForSameInstance)
83 | {
84 | //var dupes = _Changes.Where(c => null != c.ChangeKey && c.ChangeKey.Equals(change.ChangeKey)).ToList();
85 | //if (null != dupes && dupes.Count > 0)
86 | // dupes.ForEach(c => _Changes.Remove(c));
87 |
88 | var dupe = _Changes.FirstOrDefault(c => null != c.ChangeKey && c.ChangeKey.Equals(change.ChangeKey));
89 | if (null != dupe)
90 | {
91 | dupe.MergeWith(change);
92 | // System.Diagnostics.Debug.WriteLine("AddChange: MERGED");
93 | }
94 | else
95 | {
96 | _Changes.Add(change);
97 | }
98 | }
99 | else
100 | {
101 | _Changes.Add(change);
102 | }
103 | }
104 |
105 | ///
106 | /// Undo all Changes in this ChangeSet.
107 | ///
108 | internal void Undo()
109 | {
110 | foreach (var change in _Changes.Reverse())
111 | change.Undo();
112 |
113 | _Undone = true;
114 | }
115 |
116 | ///
117 | /// Redo all Changes in this ChangeSet.
118 | ///
119 | internal void Redo()
120 | {
121 | foreach (var change in _Changes)
122 | change.Redo();
123 |
124 | _Undone = false;
125 | }
126 |
127 |
128 |
129 | }
130 |
131 | }
132 |
--------------------------------------------------------------------------------
/src/MonitoredUndo/Changes/Change.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Text;
7 |
8 | namespace MonitoredUndo
9 | {
10 |
11 | ///
12 | /// Represents an individual change, with the commands to undo / redo the change as needed.
13 | ///
14 | public abstract class Change
15 | {
16 |
17 |
18 | private object _Target;
19 | private bool _Undone = false;
20 | private object _ChangeKey;
21 |
22 |
23 |
24 |
25 | ///
26 | /// Create a new change item.
27 | ///
28 | /// The object that this change affects.
29 | /// An object, that will be used to detect changes that affect the same "field".
30 | /// This object should implement or override object.Equals() and return true if the changes are for the same field.
31 | /// This is used when the undo UndoRoot has started a batch, or when the UndoRoot.ConsolidateChangesForSameInstance is true.
32 | /// A string will work, but should be sufficiently unique within the scope of changes that affect this Target instance.
33 | /// Another good option is to use the Tuple<> class to uniquely identify the change. The Tuple could contain
34 | /// the object, and a string representing the property name. For a collection change, you might include the
35 | /// instance, the property name, and the item added/removed from the collection.
36 | ///
37 | protected Change(object target, object changeKey)
38 | {
39 | _Target = target; // new WeakReference(target);
40 | _ChangeKey = changeKey;
41 | }
42 |
43 |
44 |
45 |
46 | ///
47 | /// A reference to the object that this change is for.
48 | ///
49 | public object Target { get { return _Target; } }
50 |
51 | ///
52 | /// The change "key" that uniquely identifies this instance. (see commends on the constructor.)
53 | ///
54 | public object ChangeKey { get { return _ChangeKey; } }
55 |
56 | ///
57 | /// Has this change been undone.
58 | ///
59 | public bool Undone { get { return _Undone; } }
60 |
61 |
62 |
63 |
64 | ///
65 | /// When consolidating events, we want to keep the original (first) "Undo"
66 | /// but use the most recent Redo. This will pull the Redo from the
67 | /// specified Change and apply it to this instance.
68 | ///
69 | public abstract void MergeWith(Change latestChange);
70 |
71 |
72 |
73 |
74 | ///
75 | /// Apply the undo logic from this instance, and raise the ISupportsUndoNotification.UndoHappened event.
76 | ///
77 | internal void Undo()
78 | {
79 | PerformUndo();
80 |
81 | _Undone = true;
82 |
83 | var notify = Target as ISupportUndoNotification;
84 | if (null != notify)
85 | notify.UndoHappened(this);
86 | }
87 |
88 | ///
89 | /// Overridden in derived classes to contain the actual Undo logic.
90 | ///
91 | protected abstract void PerformUndo();
92 |
93 | ///
94 | /// Apply the redo logic from this instance, and raise the ISupportsUndoNotification.RedoHappened event.
95 | ///
96 | internal void Redo()
97 | {
98 | PerformRedo();
99 |
100 | _Undone = false;
101 |
102 | var notify = Target as ISupportUndoNotification;
103 | if (null != notify)
104 | notify.RedoHappened(this);
105 | }
106 |
107 | ///
108 | /// Overridden in derived classes to contain the actual Redo logic.
109 | ///
110 | protected abstract void PerformRedo();
111 |
112 |
113 |
114 | }
115 |
116 | }
117 |
--------------------------------------------------------------------------------
/src/MonitoredUndo/Changes/CollectionAddChange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Text;
7 |
8 | namespace MonitoredUndo
9 | {
10 |
11 | [DebuggerDisplay("{DebuggerDisplay,nq}")]
12 | public class CollectionAddChange : CollectionAddRemoveChangeBase
13 | {
14 |
15 |
16 |
17 |
18 |
19 | public CollectionAddChange(object target, string propertyName, IList collection, int index, object element)
20 | : base(target, propertyName, collection, index, element) { }
21 |
22 |
23 |
24 |
25 | protected override void PerformUndo()
26 | {
27 | Collection.Remove(Element);
28 | }
29 |
30 | protected override void PerformRedo()
31 | {
32 | Collection.Insert(_RedoIndex, _RedoElement);
33 | }
34 |
35 |
36 | }
37 |
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/MonitoredUndo/Changes/CollectionAddRemoveChangeBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections;
3 | using System.Collections.Generic;
4 | using System.Diagnostics;
5 | using System.Linq;
6 | using System.Text;
7 |
8 | namespace MonitoredUndo
9 | {
10 |
11 | public abstract class CollectionAddRemoveChangeBase : CollectionChange
12 | {
13 |
14 |
15 | protected object _RedoElement;
16 | protected int _RedoIndex;
17 |
18 |
19 |
20 |
21 |
22 | public CollectionAddRemoveChangeBase(object target, string propertyName, IList collection, int index, object element)
23 | : base(target, propertyName, collection,
24 | new ChangeKey