├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE.md ├── .gitignore ├── EditingSystem ├── CodeSnippets │ └── prope.snippet ├── Jewelry.EditingSystem.Tests │ ├── GlobalSuppressions.cs │ ├── Jewelry.EditingSystem.Tests.csproj │ ├── TestModels │ │ ├── CollectionItem.cs │ │ ├── DirectBasicTestModel.cs │ │ ├── DirectFlagTestModel.cs │ │ ├── EditableBasicTestModel.cs │ │ ├── EditableFlagTestModel.cs │ │ ├── IBasicTestModel.cs │ │ ├── IFlagTestModel.cs │ │ ├── TestModelCreator.cs │ │ ├── TestModelKinds.cs │ │ └── TestModelKindsTestData.cs │ └── Tests │ │ ├── BatchEditingTests.cs │ │ ├── CollectionItemTests.cs │ │ ├── CollectionPropertyTests.cs │ │ ├── FlagPropertyTests.cs │ │ ├── HistoryTests.cs │ │ ├── PauseEditingTests.cs │ │ ├── SetEditablePropertyTests.cs │ │ ├── SinglePropertyTests.cs │ │ └── UndoRedoCountTests.cs ├── Jewelry.EditingSystem.sln ├── Jewelry.EditingSystem.sln.DotSettings └── Jewelry.EditingSystem │ ├── CollectionChangedWeakEventManager.cs │ ├── EditableModelBase.cs │ ├── EditablePropertyCommon.cs │ ├── History.cs │ ├── ICollectionItem.cs │ ├── Jewelry.EditingSystem.csproj │ ├── ListExtensions.cs │ └── NotifyPropertyChangedExtensionsForDirectMode.cs ├── LICENSE └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [YoshihiroIto]# Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | **Warning: I will close the bug issue without the minimal project and the reproduce ways.** 3 | 4 | ## Problems summary 5 | 6 | 7 | ## Expected 8 | 9 | 10 | ## Environment Information 11 | 12 | * Target framework version: 13 | 14 | * OS: 15 | 16 | 17 | ## Provide the minimal project 18 | 19 | http://***.***.com/*** 20 | 21 | 22 | ## The reproduce ways 23 | 24 | 1. foo 25 | 2. bar 26 | 3. baz 27 | 28 | 29 | ## Screenshot or Videocapture 30 | 31 | http://***.***.com/***.gif 32 | 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /EditingSystem/CodeSnippets/prope.snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | prope 6 | prope 7 | Code snippet for EditableModelBase's property 8 | Yoshihiro Ito (yo.i.jewelry.bab@gmail.com) 9 |
10 | 11 | 12 | 13 | type 14 | Property type 15 | int 16 | 17 | 18 | name 19 | Property name 20 | MyProperty 21 | 22 | 23 | _$name$; 29 | set => SetEditableProperty(v => _$name$, _$name$, value); 30 | } 31 | 32 | #endregion 33 | 34 | $end$]]> 35 | 36 |
37 |
-------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 |  2 | // This file is used by Code Analysis to maintain SuppressMessage 3 | // attributes that are applied to this project. 4 | // Project-level suppressions either have no target or are given 5 | // a specific target and scoped to a namespace, type, member, etc. 6 | 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Assertions", "xUnit2013:Do not use equality check to check for collection size.", Justification = "<保留中>", Scope = "member", Target = "~M:EditingSJewelry.EditingSystem.TestsionPropertyTests.Add")] 8 | -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Jewelry.EditingSystem.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | latest 5 | Jewelry.EditingSystem.Tests 6 | enable 7 | net7.0 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/CollectionItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jewelry.EditingSystem.Tests 4 | { 5 | public sealed class CollectionItem : ICollectionItem 6 | { 7 | public int CollectionChangedAddCount { get; private set; } 8 | public int CollectionChangedRemoveCount { get; private set; } 9 | public int CollectionChangedMoveCount { get; private set; } 10 | 11 | public void Changed(in CollectionItemChangedInfo info) 12 | { 13 | switch (info.Type) 14 | { 15 | case CollectionItemChangedType.Add: 16 | ++ CollectionChangedAddCount; 17 | break; 18 | 19 | case CollectionItemChangedType.Remove: 20 | ++ CollectionChangedRemoveCount; 21 | break; 22 | 23 | case CollectionItemChangedType.Move: 24 | ++ CollectionChangedMoveCount; 25 | break; 26 | 27 | default: 28 | throw new ArgumentOutOfRangeException(nameof(info.Type), info.Type, null); 29 | } 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/DirectBasicTestModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Jewelry.EditingSystem.Tests.TestModels; 7 | 8 | public sealed class DirectBasicTestModel : IBasicTestModel 9 | { 10 | private readonly History _history; 11 | 12 | public DirectBasicTestModel(History history) 13 | { 14 | _history = history; 15 | } 16 | 17 | public int ChangingCount { get; private set; } 18 | 19 | #region IntValue 20 | 21 | private int _IntValue; 22 | 23 | public int IntValue 24 | { 25 | get => _IntValue; 26 | set 27 | { 28 | if (this.SetEditableProperty(_history, v => SetField(ref _IntValue, v), _IntValue, value)) 29 | ++ChangingCount; 30 | } 31 | } 32 | 33 | #endregion 34 | 35 | #region StringValue 36 | 37 | private string _StringValue = ""; 38 | 39 | public string StringValue 40 | { 41 | get => _StringValue; 42 | set 43 | { 44 | if (this.SetEditableProperty(_history, v => SetField(ref _StringValue, v), _StringValue, value)) 45 | ++ChangingCount; 46 | } 47 | } 48 | 49 | #endregion 50 | 51 | #region IntCollection 52 | 53 | private ObservableCollection _IntCollection = new(); 54 | 55 | public ObservableCollection IntCollection 56 | { 57 | get => _IntCollection; 58 | set => this.SetEditableProperty(_history, v => SetField(ref _IntCollection, v), _IntCollection, value); 59 | } 60 | 61 | #endregion 62 | 63 | #region Collection 64 | 65 | private ObservableCollection _Collection = new(); 66 | 67 | public ObservableCollection Collection 68 | { 69 | get => _Collection; 70 | set => this.SetEditableProperty(_history, v => _Collection = v, _Collection, value); 71 | } 72 | 73 | #endregion 74 | 75 | public event PropertyChangedEventHandler? PropertyChanged; 76 | 77 | private void OnPropertyChanged([CallerMemberName] string? propertyName = null) 78 | { 79 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 80 | } 81 | 82 | // ReSharper disable once UnusedMethodReturnValue.Local 83 | private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) 84 | { 85 | if (EqualityComparer.Default.Equals(field, value)) return false; 86 | field = value; 87 | OnPropertyChanged(propertyName); 88 | return true; 89 | } 90 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/DirectFlagTestModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Collections.ObjectModel; 3 | using System.ComponentModel; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Jewelry.EditingSystem.Tests.TestModels; 7 | 8 | public sealed class DirectFlagTestModel : IFlagTestModel 9 | { 10 | private readonly History _history; 11 | 12 | public DirectFlagTestModel(History history) 13 | { 14 | _history = history; 15 | } 16 | 17 | public int ChangingCount { get; private set; } 18 | 19 | public bool IsA 20 | { 21 | get => (_flags & FlagIsA) != default; 22 | set 23 | { 24 | if (this.SetEditableFlagProperty(_history, v => _flags = v, _flags, FlagIsA, value)) 25 | ++ChangingCount; 26 | } 27 | } 28 | 29 | public bool IsB 30 | { 31 | get => (_flags & FlagIsB) != default; 32 | set 33 | { 34 | if (this.SetEditableFlagProperty(_history, v => _flags = v, _flags, FlagIsB, value)) 35 | ++ChangingCount; 36 | } 37 | } 38 | 39 | public bool IsC 40 | { 41 | get => (_flags & FlagIsC) != default; 42 | set 43 | { 44 | if (this.SetEditableFlagProperty(_history, v => _flags = v, _flags, FlagIsC, value)) 45 | ++ChangingCount; 46 | } 47 | } 48 | 49 | private byte _flags; 50 | private const byte FlagIsA = 1 << 0; 51 | private const byte FlagIsB = 1 << 1; 52 | private const byte FlagIsC = 1 << 2; 53 | 54 | public event PropertyChangedEventHandler? PropertyChanged; 55 | 56 | private void OnPropertyChanged([CallerMemberName] string? propertyName = null) 57 | { 58 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 59 | } 60 | 61 | // ReSharper disable once UnusedMethodReturnValue.Local 62 | private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) 63 | { 64 | if (EqualityComparer.Default.Equals(field, value)) return false; 65 | field = value; 66 | OnPropertyChanged(propertyName); 67 | return true; 68 | } 69 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/EditableBasicTestModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace Jewelry.EditingSystem.Tests.TestModels; 4 | 5 | public sealed class EditableBasicTestModel : EditableModelBase, IBasicTestModel 6 | { 7 | public EditableBasicTestModel(History history) : base(history) 8 | { 9 | } 10 | 11 | public int ChangingCount { get; private set; } 12 | 13 | #region IntValue 14 | 15 | private int _IntValue; 16 | 17 | public int IntValue 18 | { 19 | get => _IntValue; 20 | set 21 | { 22 | if (SetEditableProperty(v => _IntValue = v, _IntValue, value)) 23 | ++ChangingCount; 24 | } 25 | } 26 | 27 | #endregion 28 | 29 | #region StringValue 30 | 31 | private string _StringValue = ""; 32 | 33 | public string StringValue 34 | { 35 | get => _StringValue; 36 | set 37 | { 38 | if (SetEditableProperty(v => _StringValue = v, _StringValue, value)) 39 | ++ChangingCount; 40 | } 41 | } 42 | 43 | #endregion 44 | 45 | #region IntCollection 46 | 47 | private ObservableCollection _IntCollection = new(); 48 | 49 | public ObservableCollection IntCollection 50 | { 51 | get => _IntCollection; 52 | set => SetEditableProperty(v => _IntCollection = v, _IntCollection, value); 53 | } 54 | 55 | #endregion 56 | 57 | #region Collection 58 | 59 | private ObservableCollection _Collection = new(); 60 | 61 | public ObservableCollection Collection 62 | { 63 | get => _Collection; 64 | set => SetEditableProperty(v => _Collection = v, _Collection, value); 65 | } 66 | 67 | #endregion 68 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/EditableFlagTestModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace Jewelry.EditingSystem.Tests.TestModels; 4 | 5 | public sealed class EditableFlagTestModel : EditableModelBase, IFlagTestModel 6 | { 7 | public EditableFlagTestModel(History history) : base(history) 8 | { 9 | } 10 | 11 | public int ChangingCount { get; private set; } 12 | 13 | public bool IsA 14 | { 15 | get => (_flags & FlagIsA) != default; 16 | set 17 | { 18 | if (SetEditableFlagProperty(v => _flags = v, _flags, FlagIsA, value)) 19 | ++ChangingCount; 20 | } 21 | } 22 | 23 | public bool IsB 24 | { 25 | get => (_flags & FlagIsB) != default; 26 | set 27 | { 28 | if (SetEditableFlagProperty(v => _flags = v, _flags, FlagIsB, value)) 29 | ++ChangingCount; 30 | } 31 | } 32 | 33 | public bool IsC 34 | { 35 | get => (_flags & FlagIsC) != default; 36 | set 37 | { 38 | if (SetEditableFlagProperty(v => _flags = v, _flags, FlagIsC, value)) 39 | ++ChangingCount; 40 | } 41 | } 42 | 43 | private byte _flags; 44 | private const byte FlagIsA = 1 << 0; 45 | private const byte FlagIsB = 1 << 1; 46 | private const byte FlagIsC = 1 << 2; 47 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/IBasicTestModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.ComponentModel; 3 | 4 | namespace Jewelry.EditingSystem.Tests.TestModels; 5 | 6 | public interface IBasicTestModel : INotifyPropertyChanged 7 | { 8 | int ChangingCount { get; } 9 | 10 | int IntValue { get; set; } 11 | string StringValue { get; set; } 12 | 13 | ObservableCollection IntCollection { get; set; } 14 | 15 | ObservableCollection Collection { get; set; } 16 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/IFlagTestModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.ComponentModel; 3 | 4 | namespace Jewelry.EditingSystem.Tests.TestModels; 5 | 6 | public interface IFlagTestModel : INotifyPropertyChanged 7 | { 8 | int ChangingCount { get; } 9 | 10 | bool IsA { get; set; } 11 | bool IsB { get; set; } 12 | bool IsC { get; set; } 13 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/TestModelCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Jewelry.EditingSystem.Tests.TestModels; 4 | 5 | public static class TestModelCreator 6 | { 7 | public static IBasicTestModel CreateBasicTestModel(TestModelKinds kind, History history) 8 | { 9 | return kind switch 10 | { 11 | TestModelKinds.EditableModel => new EditableBasicTestModel(history), 12 | TestModelKinds.Direct => new DirectBasicTestModel(history), 13 | _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) 14 | }; 15 | } 16 | 17 | public static IFlagTestModel CreateFlagTestModel(TestModelKinds kind, History history) 18 | { 19 | return kind switch 20 | { 21 | TestModelKinds.EditableModel => new EditableFlagTestModel(history), 22 | TestModelKinds.Direct => new DirectFlagTestModel(history), 23 | _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, null) 24 | }; 25 | } 26 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/TestModelKinds.cs: -------------------------------------------------------------------------------- 1 | namespace Jewelry.EditingSystem.Tests.TestModels; 2 | 3 | public enum TestModelKinds 4 | { 5 | EditableModel, 6 | Direct 7 | } 8 | -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/TestModels/TestModelKindsTestData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Jewelry.EditingSystem.Tests.TestModels; 7 | 8 | public sealed class TestModelKindsTestData : IEnumerable 9 | { 10 | public IEnumerator GetEnumerator() => 11 | Enum.GetValues() 12 | .Select(x => new object[] { x }) 13 | .GetEnumerator(); 14 | 15 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 16 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/BatchEditingTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using System; 3 | using Xunit; 4 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 5 | 6 | namespace Jewelry.EditingSystem.Tests; 7 | 8 | public sealed class BatchEditingTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(TestModelKindsTestData))] 12 | public void Basic(TestModelKinds testModelKind) 13 | { 14 | using var history = new History(); 15 | var model = CreateBasicTestModel(testModelKind, history); 16 | 17 | Assert.False(history.CanUndo); 18 | Assert.False(history.CanRedo); 19 | 20 | model.IntValue = 999; 21 | model.StringValue = "XYZ"; 22 | 23 | history.BeginBatch(); 24 | { 25 | model.IntValue = 10; 26 | model.IntValue = 11; 27 | model.IntValue = 12; 28 | 29 | model.StringValue = "A"; 30 | model.StringValue = "B"; 31 | model.StringValue = "C"; 32 | } 33 | history.EndBatch(); 34 | 35 | history.Undo(); 36 | 37 | Assert.Equal(999, model.IntValue); 38 | Assert.Equal("XYZ", model.StringValue); 39 | 40 | history.Redo(); 41 | Assert.Equal(12, model.IntValue); 42 | Assert.Equal("C", model.StringValue); 43 | } 44 | 45 | [Fact] 46 | public void Empty() 47 | { 48 | using var history = new History(); 49 | 50 | Assert.False(history.CanUndo); 51 | Assert.False(history.CanRedo); 52 | Assert.Equal(0, history.UndoCount); 53 | Assert.Equal(0, history.RedoCount); 54 | 55 | history.BeginBatch(); 56 | { 57 | } 58 | history.EndBatch(); 59 | 60 | Assert.False(history.CanUndo); 61 | Assert.False(history.CanRedo); 62 | Assert.Equal(0, history.UndoCount); 63 | Assert.Equal(0, history.RedoCount); 64 | } 65 | 66 | [Theory] 67 | [ClassData(typeof(TestModelKindsTestData))] 68 | public void NestingBatch(TestModelKinds testModelKind) 69 | { 70 | using var history = new History(); 71 | var model = CreateBasicTestModel(testModelKind, history); 72 | 73 | Assert.False(history.CanUndo); 74 | Assert.False(history.CanRedo); 75 | 76 | model.IntValue = 999; 77 | model.StringValue = "XYZ"; 78 | 79 | history.BeginBatch(); 80 | { 81 | model.IntValue = 10; 82 | 83 | history.BeginBatch(); 84 | { 85 | model.IntValue = 11; 86 | 87 | history.BeginBatch(); 88 | { 89 | model.IntValue = 12; 90 | model.StringValue = "A"; 91 | } 92 | history.EndBatch(); 93 | 94 | model.StringValue = "B"; 95 | } 96 | history.EndBatch(); 97 | 98 | model.StringValue = "C"; 99 | } 100 | history.EndBatch(); 101 | 102 | history.Undo(); 103 | 104 | Assert.Equal(999, model.IntValue); 105 | Assert.Equal("XYZ", model.StringValue); 106 | 107 | history.Redo(); 108 | Assert.Equal(12, model.IntValue); 109 | Assert.Equal("C", model.StringValue); 110 | } 111 | 112 | [Theory] 113 | [ClassData(typeof(TestModelKindsTestData))] 114 | public void Cannot_call_undo_during_batch_recording(TestModelKinds testModelKind) 115 | { 116 | using var history = new History(); 117 | var model = CreateBasicTestModel(testModelKind, history); 118 | 119 | Assert.False(history.CanUndo); 120 | Assert.False(history.CanRedo); 121 | 122 | model.IntValue = 999; 123 | model.StringValue = "XYZ"; 124 | 125 | history.BeginBatch(); 126 | 127 | Assert.Throws(() => 128 | history.Undo() 129 | ); 130 | } 131 | 132 | [Theory] 133 | [ClassData(typeof(TestModelKindsTestData))] 134 | public void Cannot_call_redo_during_batch_recording(TestModelKinds testModelKind) 135 | { 136 | using var history = new History(); 137 | var model = CreateBasicTestModel(testModelKind, history); 138 | 139 | Assert.False(history.CanUndo); 140 | Assert.False(history.CanRedo); 141 | 142 | model.IntValue = 999; 143 | model.StringValue = "XYZ"; 144 | 145 | history.BeginBatch(); 146 | 147 | Assert.Throws(() => 148 | history.Redo() 149 | ); 150 | } 151 | 152 | [Theory] 153 | [ClassData(typeof(TestModelKindsTestData))] 154 | public void Batch_recording_has_not_begun(TestModelKinds testModelKind) 155 | { 156 | using var history = new History(); 157 | var model = CreateBasicTestModel(testModelKind, history); 158 | 159 | Assert.False(history.CanUndo); 160 | Assert.False(history.CanRedo); 161 | 162 | model.IntValue = 999; 163 | model.StringValue = "XYZ"; 164 | 165 | Assert.Throws(() => 166 | history.EndBatch() 167 | ); 168 | } 169 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/CollectionItemTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using System.Collections.ObjectModel; 3 | using Xunit; 4 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 5 | 6 | // ReSharper disable UseObjectOrCollectionInitializer 7 | 8 | namespace Jewelry.EditingSystem.Tests; 9 | 10 | public sealed class CollectionItemTests 11 | { 12 | [Theory] 13 | [ClassData(typeof(TestModelKindsTestData))] 14 | public void Add(TestModelKinds testModelKind) 15 | { 16 | using var history = new History(); 17 | var model = CreateBasicTestModel(testModelKind, history); 18 | 19 | model.Collection = new ObservableCollection(); 20 | 21 | var item0 = new CollectionItem(); 22 | var item1 = new CollectionItem(); 23 | var item2 = new CollectionItem(); 24 | 25 | model.Collection.Add(item0); 26 | model.Collection.Add(item1); 27 | model.Collection.Add(item2); 28 | 29 | Assert.Equal(1, item0.CollectionChangedAddCount); 30 | Assert.Equal(1, item1.CollectionChangedAddCount); 31 | Assert.Equal(1, item2.CollectionChangedAddCount); 32 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 33 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 34 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 35 | 36 | history.Undo(); 37 | Assert.Equal(1, item0.CollectionChangedAddCount); 38 | Assert.Equal(1, item1.CollectionChangedAddCount); 39 | Assert.Equal(1, item2.CollectionChangedAddCount); 40 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 41 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 42 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 43 | 44 | history.Undo(); 45 | Assert.Equal(1, item0.CollectionChangedAddCount); 46 | Assert.Equal(1, item1.CollectionChangedAddCount); 47 | Assert.Equal(1, item2.CollectionChangedAddCount); 48 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 49 | Assert.Equal(1, item1.CollectionChangedRemoveCount); 50 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 51 | 52 | history.Undo(); 53 | Assert.Equal(1, item0.CollectionChangedAddCount); 54 | Assert.Equal(1, item1.CollectionChangedAddCount); 55 | Assert.Equal(1, item2.CollectionChangedAddCount); 56 | Assert.Equal(1, item0.CollectionChangedRemoveCount); 57 | Assert.Equal(1, item1.CollectionChangedRemoveCount); 58 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 59 | 60 | history.Redo(); 61 | Assert.Equal(2, item0.CollectionChangedAddCount); 62 | Assert.Equal(1, item1.CollectionChangedAddCount); 63 | Assert.Equal(1, item2.CollectionChangedAddCount); 64 | Assert.Equal(1, item0.CollectionChangedRemoveCount); 65 | Assert.Equal(1, item1.CollectionChangedRemoveCount); 66 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 67 | 68 | history.Redo(); 69 | Assert.Equal(2, item0.CollectionChangedAddCount); 70 | Assert.Equal(2, item1.CollectionChangedAddCount); 71 | Assert.Equal(1, item2.CollectionChangedAddCount); 72 | Assert.Equal(1, item0.CollectionChangedRemoveCount); 73 | Assert.Equal(1, item1.CollectionChangedRemoveCount); 74 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 75 | 76 | history.Redo(); 77 | Assert.Equal(2, item0.CollectionChangedAddCount); 78 | Assert.Equal(2, item1.CollectionChangedAddCount); 79 | Assert.Equal(2, item2.CollectionChangedAddCount); 80 | Assert.Equal(1, item0.CollectionChangedRemoveCount); 81 | Assert.Equal(1, item1.CollectionChangedRemoveCount); 82 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 83 | } 84 | 85 | [Theory] 86 | [ClassData(typeof(TestModelKindsTestData))] 87 | public void Move_Ascending(TestModelKinds testModelKind) 88 | { 89 | using var history = new History(); 90 | var model = CreateBasicTestModel(testModelKind, history); 91 | 92 | model.Collection = new ObservableCollection(); 93 | 94 | var item0 = new CollectionItem(); 95 | var item1 = new CollectionItem(); 96 | var item2 = new CollectionItem(); 97 | var item3 = new CollectionItem(); 98 | 99 | model.Collection.Add(item0); 100 | model.Collection.Add(item1); 101 | model.Collection.Add(item2); 102 | model.Collection.Add(item3); 103 | 104 | Assert.Equal(1, item0.CollectionChangedAddCount); 105 | Assert.Equal(1, item1.CollectionChangedAddCount); 106 | Assert.Equal(1, item2.CollectionChangedAddCount); 107 | Assert.Equal(1, item3.CollectionChangedAddCount); 108 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 109 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 110 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 111 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 112 | Assert.Equal(0, item0.CollectionChangedMoveCount); 113 | Assert.Equal(0, item1.CollectionChangedMoveCount); 114 | Assert.Equal(0, item2.CollectionChangedMoveCount); 115 | Assert.Equal(0, item3.CollectionChangedMoveCount); 116 | 117 | model.Collection.Move(0, 3); 118 | 119 | Assert.Equal(1, item0.CollectionChangedAddCount); 120 | Assert.Equal(1, item1.CollectionChangedAddCount); 121 | Assert.Equal(1, item2.CollectionChangedAddCount); 122 | Assert.Equal(1, item3.CollectionChangedAddCount); 123 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 124 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 125 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 126 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 127 | Assert.Equal(1, item0.CollectionChangedMoveCount); 128 | Assert.Equal(0, item1.CollectionChangedMoveCount); 129 | Assert.Equal(0, item2.CollectionChangedMoveCount); 130 | Assert.Equal(0, item3.CollectionChangedMoveCount); 131 | 132 | history.Undo(); 133 | Assert.Equal(1, item0.CollectionChangedAddCount); 134 | Assert.Equal(1, item1.CollectionChangedAddCount); 135 | Assert.Equal(1, item2.CollectionChangedAddCount); 136 | Assert.Equal(1, item3.CollectionChangedAddCount); 137 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 138 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 139 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 140 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 141 | Assert.Equal(2, item0.CollectionChangedMoveCount); 142 | Assert.Equal(0, item1.CollectionChangedMoveCount); 143 | Assert.Equal(0, item2.CollectionChangedMoveCount); 144 | Assert.Equal(0, item3.CollectionChangedMoveCount); 145 | 146 | history.Redo(); 147 | Assert.Equal(1, item0.CollectionChangedAddCount); 148 | Assert.Equal(1, item1.CollectionChangedAddCount); 149 | Assert.Equal(1, item2.CollectionChangedAddCount); 150 | Assert.Equal(1, item3.CollectionChangedAddCount); 151 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 152 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 153 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 154 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 155 | Assert.Equal(3, item0.CollectionChangedMoveCount); 156 | Assert.Equal(0, item1.CollectionChangedMoveCount); 157 | Assert.Equal(0, item2.CollectionChangedMoveCount); 158 | Assert.Equal(0, item3.CollectionChangedMoveCount); 159 | } 160 | 161 | [Theory] 162 | [ClassData(typeof(TestModelKindsTestData))] 163 | public void Move_Descending(TestModelKinds testModelKind) 164 | { 165 | using var history = new History(); 166 | var model = CreateBasicTestModel(testModelKind, history); 167 | 168 | model.Collection = new ObservableCollection(); 169 | 170 | var item0 = new CollectionItem(); 171 | var item1 = new CollectionItem(); 172 | var item2 = new CollectionItem(); 173 | var item3 = new CollectionItem(); 174 | 175 | model.Collection.Add(item0); 176 | model.Collection.Add(item1); 177 | model.Collection.Add(item2); 178 | model.Collection.Add(item3); 179 | 180 | Assert.Equal(1, item0.CollectionChangedAddCount); 181 | Assert.Equal(1, item1.CollectionChangedAddCount); 182 | Assert.Equal(1, item2.CollectionChangedAddCount); 183 | Assert.Equal(1, item3.CollectionChangedAddCount); 184 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 185 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 186 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 187 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 188 | Assert.Equal(0, item0.CollectionChangedMoveCount); 189 | Assert.Equal(0, item1.CollectionChangedMoveCount); 190 | Assert.Equal(0, item2.CollectionChangedMoveCount); 191 | Assert.Equal(0, item3.CollectionChangedMoveCount); 192 | 193 | model.Collection.Move(3, 0); 194 | 195 | Assert.Equal(1, item0.CollectionChangedAddCount); 196 | Assert.Equal(1, item1.CollectionChangedAddCount); 197 | Assert.Equal(1, item2.CollectionChangedAddCount); 198 | Assert.Equal(1, item3.CollectionChangedAddCount); 199 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 200 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 201 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 202 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 203 | Assert.Equal(0, item0.CollectionChangedMoveCount); 204 | Assert.Equal(0, item1.CollectionChangedMoveCount); 205 | Assert.Equal(0, item2.CollectionChangedMoveCount); 206 | Assert.Equal(1, item3.CollectionChangedMoveCount); 207 | 208 | history.Undo(); 209 | Assert.Equal(1, item0.CollectionChangedAddCount); 210 | Assert.Equal(1, item1.CollectionChangedAddCount); 211 | Assert.Equal(1, item2.CollectionChangedAddCount); 212 | Assert.Equal(1, item3.CollectionChangedAddCount); 213 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 214 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 215 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 216 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 217 | Assert.Equal(0, item0.CollectionChangedMoveCount); 218 | Assert.Equal(0, item1.CollectionChangedMoveCount); 219 | Assert.Equal(0, item2.CollectionChangedMoveCount); 220 | Assert.Equal(2, item3.CollectionChangedMoveCount); 221 | 222 | history.Redo(); 223 | Assert.Equal(1, item0.CollectionChangedAddCount); 224 | Assert.Equal(1, item1.CollectionChangedAddCount); 225 | Assert.Equal(1, item2.CollectionChangedAddCount); 226 | Assert.Equal(1, item3.CollectionChangedAddCount); 227 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 228 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 229 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 230 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 231 | Assert.Equal(0, item0.CollectionChangedMoveCount); 232 | Assert.Equal(0, item1.CollectionChangedMoveCount); 233 | Assert.Equal(0, item2.CollectionChangedMoveCount); 234 | Assert.Equal(3, item3.CollectionChangedMoveCount); 235 | } 236 | 237 | [Theory] 238 | [ClassData(typeof(TestModelKindsTestData))] 239 | public void Remove(TestModelKinds testModelKind) 240 | { 241 | using var history = new History(); 242 | var model = CreateBasicTestModel(testModelKind, history); 243 | 244 | model.Collection = new ObservableCollection(); 245 | 246 | var item0 = new CollectionItem(); 247 | var item1 = new CollectionItem(); 248 | var item2 = new CollectionItem(); 249 | var item3 = new CollectionItem(); 250 | 251 | model.Collection.Add(item0); 252 | model.Collection.Add(item1); 253 | model.Collection.Add(item2); 254 | model.Collection.Add(item3); 255 | 256 | model.Collection.Remove(item3); 257 | 258 | Assert.Equal(1, item0.CollectionChangedAddCount); 259 | Assert.Equal(1, item1.CollectionChangedAddCount); 260 | Assert.Equal(1, item2.CollectionChangedAddCount); 261 | Assert.Equal(1, item3.CollectionChangedAddCount); 262 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 263 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 264 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 265 | Assert.Equal(1, item3.CollectionChangedRemoveCount); 266 | Assert.Equal(0, item0.CollectionChangedMoveCount); 267 | Assert.Equal(0, item1.CollectionChangedMoveCount); 268 | Assert.Equal(0, item2.CollectionChangedMoveCount); 269 | Assert.Equal(0, item3.CollectionChangedMoveCount); 270 | 271 | history.Undo(); 272 | Assert.Equal(1, item0.CollectionChangedAddCount); 273 | Assert.Equal(1, item1.CollectionChangedAddCount); 274 | Assert.Equal(1, item2.CollectionChangedAddCount); 275 | Assert.Equal(2, item3.CollectionChangedAddCount); 276 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 277 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 278 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 279 | Assert.Equal(1, item3.CollectionChangedRemoveCount); 280 | Assert.Equal(0, item0.CollectionChangedMoveCount); 281 | Assert.Equal(0, item1.CollectionChangedMoveCount); 282 | Assert.Equal(0, item2.CollectionChangedMoveCount); 283 | Assert.Equal(0, item3.CollectionChangedMoveCount); 284 | 285 | history.Redo(); 286 | Assert.Equal(1, item0.CollectionChangedAddCount); 287 | Assert.Equal(1, item1.CollectionChangedAddCount); 288 | Assert.Equal(1, item2.CollectionChangedAddCount); 289 | Assert.Equal(2, item3.CollectionChangedAddCount); 290 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 291 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 292 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 293 | Assert.Equal(2, item3.CollectionChangedRemoveCount); 294 | Assert.Equal(0, item0.CollectionChangedMoveCount); 295 | Assert.Equal(0, item1.CollectionChangedMoveCount); 296 | Assert.Equal(0, item2.CollectionChangedMoveCount); 297 | Assert.Equal(0, item3.CollectionChangedMoveCount); 298 | } 299 | 300 | [Theory] 301 | [ClassData(typeof(TestModelKindsTestData))] 302 | public void RemoveAt(TestModelKinds testModelKind) 303 | { 304 | using var history = new History(); 305 | var model = CreateBasicTestModel(testModelKind, history); 306 | 307 | model.Collection = new ObservableCollection(); 308 | 309 | var item0 = new CollectionItem(); 310 | var item1 = new CollectionItem(); 311 | var item2 = new CollectionItem(); 312 | var item3 = new CollectionItem(); 313 | 314 | model.Collection.Add(item0); 315 | model.Collection.Add(item1); 316 | model.Collection.Add(item2); 317 | model.Collection.Add(item3); 318 | 319 | model.Collection.RemoveAt(3); 320 | 321 | Assert.Equal(1, item0.CollectionChangedAddCount); 322 | Assert.Equal(1, item1.CollectionChangedAddCount); 323 | Assert.Equal(1, item2.CollectionChangedAddCount); 324 | Assert.Equal(1, item3.CollectionChangedAddCount); 325 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 326 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 327 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 328 | Assert.Equal(1, item3.CollectionChangedRemoveCount); 329 | Assert.Equal(0, item0.CollectionChangedMoveCount); 330 | Assert.Equal(0, item1.CollectionChangedMoveCount); 331 | Assert.Equal(0, item2.CollectionChangedMoveCount); 332 | Assert.Equal(0, item3.CollectionChangedMoveCount); 333 | 334 | history.Undo(); 335 | Assert.Equal(1, item0.CollectionChangedAddCount); 336 | Assert.Equal(1, item1.CollectionChangedAddCount); 337 | Assert.Equal(1, item2.CollectionChangedAddCount); 338 | Assert.Equal(2, item3.CollectionChangedAddCount); 339 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 340 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 341 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 342 | Assert.Equal(1, item3.CollectionChangedRemoveCount); 343 | Assert.Equal(0, item0.CollectionChangedMoveCount); 344 | Assert.Equal(0, item1.CollectionChangedMoveCount); 345 | Assert.Equal(0, item2.CollectionChangedMoveCount); 346 | Assert.Equal(0, item3.CollectionChangedMoveCount); 347 | 348 | history.Redo(); 349 | Assert.Equal(1, item0.CollectionChangedAddCount); 350 | Assert.Equal(1, item1.CollectionChangedAddCount); 351 | Assert.Equal(1, item2.CollectionChangedAddCount); 352 | Assert.Equal(2, item3.CollectionChangedAddCount); 353 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 354 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 355 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 356 | Assert.Equal(2, item3.CollectionChangedRemoveCount); 357 | Assert.Equal(0, item0.CollectionChangedMoveCount); 358 | Assert.Equal(0, item1.CollectionChangedMoveCount); 359 | Assert.Equal(0, item2.CollectionChangedMoveCount); 360 | Assert.Equal(0, item3.CollectionChangedMoveCount); 361 | } 362 | 363 | [Theory] 364 | [ClassData(typeof(TestModelKindsTestData))] 365 | public void Insert(TestModelKinds testModelKind) 366 | { 367 | using var history = new History(); 368 | var model = CreateBasicTestModel(testModelKind, history); 369 | 370 | model.Collection = new ObservableCollection(); 371 | 372 | var item0 = new CollectionItem(); 373 | var item1 = new CollectionItem(); 374 | var item2 = new CollectionItem(); 375 | var item3 = new CollectionItem(); 376 | 377 | model.Collection.Add(item0); 378 | model.Collection.Add(item1); 379 | model.Collection.Add(item2); 380 | model.Collection.Add(item3); 381 | 382 | Assert.Equal(1, item0.CollectionChangedAddCount); 383 | Assert.Equal(1, item1.CollectionChangedAddCount); 384 | Assert.Equal(1, item2.CollectionChangedAddCount); 385 | Assert.Equal(1, item3.CollectionChangedAddCount); 386 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 387 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 388 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 389 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 390 | Assert.Equal(0, item0.CollectionChangedMoveCount); 391 | Assert.Equal(0, item1.CollectionChangedMoveCount); 392 | Assert.Equal(0, item2.CollectionChangedMoveCount); 393 | Assert.Equal(0, item3.CollectionChangedMoveCount); 394 | 395 | var itemX = new CollectionItem(); 396 | Assert.Equal(0, itemX.CollectionChangedAddCount); 397 | Assert.Equal(0, itemX.CollectionChangedRemoveCount); 398 | Assert.Equal(0, itemX.CollectionChangedMoveCount); 399 | 400 | model.Collection.Insert(2, itemX); 401 | 402 | Assert.Equal(1, item0.CollectionChangedAddCount); 403 | Assert.Equal(1, item1.CollectionChangedAddCount); 404 | Assert.Equal(1, itemX.CollectionChangedAddCount); 405 | Assert.Equal(1, item2.CollectionChangedAddCount); 406 | Assert.Equal(1, item3.CollectionChangedAddCount); 407 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 408 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 409 | Assert.Equal(0, itemX.CollectionChangedRemoveCount); 410 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 411 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 412 | Assert.Equal(0, item0.CollectionChangedMoveCount); 413 | Assert.Equal(0, itemX.CollectionChangedMoveCount); 414 | Assert.Equal(0, item1.CollectionChangedMoveCount); 415 | Assert.Equal(0, item2.CollectionChangedMoveCount); 416 | Assert.Equal(0, item3.CollectionChangedMoveCount); 417 | 418 | history.Undo(); 419 | Assert.Equal(1, item0.CollectionChangedAddCount); 420 | Assert.Equal(1, item1.CollectionChangedAddCount); 421 | Assert.Equal(1, itemX.CollectionChangedAddCount); 422 | Assert.Equal(1, item2.CollectionChangedAddCount); 423 | Assert.Equal(1, item3.CollectionChangedAddCount); 424 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 425 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 426 | Assert.Equal(1, itemX.CollectionChangedRemoveCount); 427 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 428 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 429 | Assert.Equal(0, item0.CollectionChangedMoveCount); 430 | Assert.Equal(0, itemX.CollectionChangedMoveCount); 431 | Assert.Equal(0, item1.CollectionChangedMoveCount); 432 | Assert.Equal(0, item2.CollectionChangedMoveCount); 433 | Assert.Equal(0, item3.CollectionChangedMoveCount); 434 | 435 | history.Redo(); 436 | Assert.Equal(1, item0.CollectionChangedAddCount); 437 | Assert.Equal(1, item1.CollectionChangedAddCount); 438 | Assert.Equal(2, itemX.CollectionChangedAddCount); 439 | Assert.Equal(1, item2.CollectionChangedAddCount); 440 | Assert.Equal(1, item3.CollectionChangedAddCount); 441 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 442 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 443 | Assert.Equal(1, itemX.CollectionChangedRemoveCount); 444 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 445 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 446 | Assert.Equal(0, item0.CollectionChangedMoveCount); 447 | Assert.Equal(0, itemX.CollectionChangedMoveCount); 448 | Assert.Equal(0, item1.CollectionChangedMoveCount); 449 | Assert.Equal(0, item2.CollectionChangedMoveCount); 450 | Assert.Equal(0, item3.CollectionChangedMoveCount); 451 | } 452 | 453 | [Theory] 454 | [ClassData(typeof(TestModelKindsTestData))] 455 | public void ClearEx(TestModelKinds testModelKind) 456 | { 457 | using var history = new History(); 458 | var model = CreateBasicTestModel(testModelKind, history); 459 | 460 | model.Collection = new ObservableCollection(); 461 | 462 | var item0 = new CollectionItem(); 463 | var item1 = new CollectionItem(); 464 | var item2 = new CollectionItem(); 465 | var item3 = new CollectionItem(); 466 | 467 | model.Collection.Add(item0); 468 | model.Collection.Add(item1); 469 | model.Collection.Add(item2); 470 | model.Collection.Add(item3); 471 | 472 | Assert.Equal(1, item0.CollectionChangedAddCount); 473 | Assert.Equal(1, item1.CollectionChangedAddCount); 474 | Assert.Equal(1, item2.CollectionChangedAddCount); 475 | Assert.Equal(1, item3.CollectionChangedAddCount); 476 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 477 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 478 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 479 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 480 | Assert.Equal(0, item0.CollectionChangedMoveCount); 481 | Assert.Equal(0, item1.CollectionChangedMoveCount); 482 | Assert.Equal(0, item2.CollectionChangedMoveCount); 483 | Assert.Equal(0, item3.CollectionChangedMoveCount); 484 | 485 | model.Collection.ClearEx(history); 486 | 487 | Assert.Equal(1, item0.CollectionChangedAddCount); 488 | Assert.Equal(1, item1.CollectionChangedAddCount); 489 | Assert.Equal(1, item2.CollectionChangedAddCount); 490 | Assert.Equal(1, item3.CollectionChangedAddCount); 491 | Assert.Equal(1, item0.CollectionChangedRemoveCount); 492 | Assert.Equal(1, item1.CollectionChangedRemoveCount); 493 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 494 | Assert.Equal(1, item3.CollectionChangedRemoveCount); 495 | Assert.Equal(0, item0.CollectionChangedMoveCount); 496 | Assert.Equal(0, item1.CollectionChangedMoveCount); 497 | Assert.Equal(0, item2.CollectionChangedMoveCount); 498 | Assert.Equal(0, item3.CollectionChangedMoveCount); 499 | 500 | history.Undo(); 501 | Assert.Equal(2, item0.CollectionChangedAddCount); 502 | Assert.Equal(2, item1.CollectionChangedAddCount); 503 | Assert.Equal(2, item2.CollectionChangedAddCount); 504 | Assert.Equal(2, item3.CollectionChangedAddCount); 505 | Assert.Equal(1, item0.CollectionChangedRemoveCount); 506 | Assert.Equal(1, item1.CollectionChangedRemoveCount); 507 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 508 | Assert.Equal(1, item3.CollectionChangedRemoveCount); 509 | Assert.Equal(0, item0.CollectionChangedMoveCount); 510 | Assert.Equal(0, item1.CollectionChangedMoveCount); 511 | Assert.Equal(0, item2.CollectionChangedMoveCount); 512 | Assert.Equal(0, item3.CollectionChangedMoveCount); 513 | 514 | history.Redo(); 515 | Assert.Equal(2, item0.CollectionChangedAddCount); 516 | Assert.Equal(2, item1.CollectionChangedAddCount); 517 | Assert.Equal(2, item2.CollectionChangedAddCount); 518 | Assert.Equal(2, item3.CollectionChangedAddCount); 519 | Assert.Equal(2, item0.CollectionChangedRemoveCount); 520 | Assert.Equal(2, item1.CollectionChangedRemoveCount); 521 | Assert.Equal(2, item2.CollectionChangedRemoveCount); 522 | Assert.Equal(2, item3.CollectionChangedRemoveCount); 523 | Assert.Equal(0, item0.CollectionChangedMoveCount); 524 | Assert.Equal(0, item1.CollectionChangedMoveCount); 525 | Assert.Equal(0, item2.CollectionChangedMoveCount); 526 | Assert.Equal(0, item3.CollectionChangedMoveCount); 527 | } 528 | 529 | [Theory] 530 | [ClassData(typeof(TestModelKindsTestData))] 531 | public void Replace(TestModelKinds testModelKind) 532 | { 533 | using var history = new History(); 534 | var model = CreateBasicTestModel(testModelKind, history); 535 | 536 | model.Collection = new ObservableCollection(); 537 | 538 | var item0 = new CollectionItem(); 539 | var item1 = new CollectionItem(); 540 | var item2 = new CollectionItem(); 541 | var item3 = new CollectionItem(); 542 | 543 | model.Collection.Add(item0); 544 | model.Collection.Add(item1); 545 | model.Collection.Add(item2); 546 | model.Collection.Add(item3); 547 | 548 | Assert.Equal(1, item0.CollectionChangedAddCount); 549 | Assert.Equal(1, item1.CollectionChangedAddCount); 550 | Assert.Equal(1, item2.CollectionChangedAddCount); 551 | Assert.Equal(1, item3.CollectionChangedAddCount); 552 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 553 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 554 | Assert.Equal(0, item2.CollectionChangedRemoveCount); 555 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 556 | Assert.Equal(0, item0.CollectionChangedMoveCount); 557 | Assert.Equal(0, item1.CollectionChangedMoveCount); 558 | Assert.Equal(0, item2.CollectionChangedMoveCount); 559 | Assert.Equal(0, item3.CollectionChangedMoveCount); 560 | 561 | var itemX = new CollectionItem(); 562 | 563 | model.Collection[2] = itemX; 564 | 565 | Assert.Equal(1, item0.CollectionChangedAddCount); 566 | Assert.Equal(1, item1.CollectionChangedAddCount); 567 | Assert.Equal(1, item2.CollectionChangedAddCount); 568 | Assert.Equal(1, item3.CollectionChangedAddCount); 569 | Assert.Equal(1, itemX.CollectionChangedAddCount); 570 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 571 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 572 | Assert.Equal(1, item2.CollectionChangedRemoveCount); 573 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 574 | Assert.Equal(0, itemX.CollectionChangedRemoveCount); 575 | Assert.Equal(0, item0.CollectionChangedMoveCount); 576 | Assert.Equal(0, item1.CollectionChangedMoveCount); 577 | Assert.Equal(0, item2.CollectionChangedMoveCount); 578 | Assert.Equal(0, item3.CollectionChangedMoveCount); 579 | Assert.Equal(0, itemX.CollectionChangedMoveCount); 580 | 581 | history.Undo(); 582 | Assert.Equal(1, item0.CollectionChangedAddCount); 583 | Assert.Equal(1, item1.CollectionChangedAddCount); 584 | Assert.Equal(1, item2.CollectionChangedAddCount); 585 | Assert.Equal(1, item3.CollectionChangedAddCount); 586 | Assert.Equal(2, itemX.CollectionChangedAddCount); 587 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 588 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 589 | Assert.Equal(2, item2.CollectionChangedRemoveCount); 590 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 591 | Assert.Equal(0, itemX.CollectionChangedRemoveCount); 592 | Assert.Equal(0, item0.CollectionChangedMoveCount); 593 | Assert.Equal(0, item1.CollectionChangedMoveCount); 594 | Assert.Equal(0, item2.CollectionChangedMoveCount); 595 | Assert.Equal(0, item3.CollectionChangedMoveCount); 596 | Assert.Equal(0, itemX.CollectionChangedMoveCount); 597 | 598 | history.Redo(); 599 | Assert.Equal(1, item0.CollectionChangedAddCount); 600 | Assert.Equal(1, item1.CollectionChangedAddCount); 601 | Assert.Equal(1, item2.CollectionChangedAddCount); 602 | Assert.Equal(1, item3.CollectionChangedAddCount); 603 | Assert.Equal(3, itemX.CollectionChangedAddCount); 604 | Assert.Equal(0, item0.CollectionChangedRemoveCount); 605 | Assert.Equal(0, item1.CollectionChangedRemoveCount); 606 | Assert.Equal(3, item2.CollectionChangedRemoveCount); 607 | Assert.Equal(0, item3.CollectionChangedRemoveCount); 608 | Assert.Equal(0, itemX.CollectionChangedRemoveCount); 609 | Assert.Equal(0, item0.CollectionChangedMoveCount); 610 | Assert.Equal(0, item1.CollectionChangedMoveCount); 611 | Assert.Equal(0, item2.CollectionChangedMoveCount); 612 | Assert.Equal(0, item3.CollectionChangedMoveCount); 613 | Assert.Equal(0, itemX.CollectionChangedMoveCount); 614 | } 615 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/CollectionPropertyTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using System; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using Xunit; 6 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 7 | 8 | namespace Jewelry.EditingSystem.Tests; 9 | 10 | public sealed class CollectionPropertyTests 11 | { 12 | [Theory] 13 | [ClassData(typeof(TestModelKindsTestData))] 14 | public void Add(TestModelKinds testModelKind) 15 | { 16 | using var history = new History(); 17 | var model = CreateBasicTestModel(testModelKind, history); 18 | 19 | model.IntCollection = new ObservableCollection(); 20 | 21 | model.IntCollection.Add(1); 22 | Assert.Single(model.IntCollection); 23 | 24 | model.IntCollection.Add(2); 25 | Assert.Equal(2, model.IntCollection.Count); 26 | 27 | model.IntCollection.Add(3); 28 | Assert.Equal(3, model.IntCollection.Count); 29 | 30 | history.Undo(); 31 | Assert.Equal(2, model.IntCollection.Count); 32 | Assert.True(model.IntCollection.SequenceEqual(new[] {1, 2})); 33 | 34 | history.Undo(); 35 | Assert.Single(model.IntCollection); 36 | Assert.True(model.IntCollection.SequenceEqual(new[] {1})); 37 | 38 | history.Redo(); 39 | Assert.Equal(2, model.IntCollection.Count); 40 | Assert.True(model.IntCollection.SequenceEqual(new[] {1, 2})); 41 | 42 | history.Redo(); 43 | Assert.Equal(3, model.IntCollection.Count); 44 | Assert.True(model.IntCollection.SequenceEqual(new[] {1, 2, 3})); 45 | } 46 | 47 | [Theory] 48 | [ClassData(typeof(TestModelKindsTestData))] 49 | public void Move_Ascending(TestModelKinds testModelKind) 50 | { 51 | using var history = new History(); 52 | var model = CreateBasicTestModel(testModelKind, history); 53 | 54 | model.IntCollection = new ObservableCollection(); 55 | 56 | model.IntCollection.Add(0); 57 | model.IntCollection.Add(1); 58 | model.IntCollection.Add(2); 59 | model.IntCollection.Add(3); 60 | 61 | Assert.True(model.IntCollection.SequenceEqual(new[] {0, 1, 2, 3})); 62 | 63 | model.IntCollection.Move(0, 3); 64 | Assert.True(model.IntCollection.SequenceEqual(new[] {1, 2, 3, 0})); 65 | 66 | history.Undo(); 67 | Assert.True(model.IntCollection.SequenceEqual(new[] {0, 1, 2, 3})); 68 | 69 | history.Redo(); 70 | Assert.True(model.IntCollection.SequenceEqual(new[] {1, 2, 3, 0})); 71 | } 72 | 73 | [Theory] 74 | [ClassData(typeof(TestModelKindsTestData))] 75 | public void Move_Descending(TestModelKinds testModelKind) 76 | { 77 | using var history = new History(); 78 | var model = CreateBasicTestModel(testModelKind, history); 79 | 80 | model.IntCollection = new ObservableCollection(); 81 | 82 | model.IntCollection.Add(0); 83 | model.IntCollection.Add(1); 84 | model.IntCollection.Add(2); 85 | model.IntCollection.Add(3); 86 | 87 | Assert.True(model.IntCollection.SequenceEqual(new[] {0, 1, 2, 3})); 88 | 89 | model.IntCollection.Move(3, 0); 90 | Assert.True(model.IntCollection.SequenceEqual(new[] {3, 0, 1, 2})); 91 | 92 | history.Undo(); 93 | Assert.True(model.IntCollection.SequenceEqual(new[] {0, 1, 2, 3})); 94 | 95 | history.Redo(); 96 | Assert.True(model.IntCollection.SequenceEqual(new[] {3, 0, 1, 2})); 97 | } 98 | 99 | [Theory] 100 | [ClassData(typeof(TestModelKindsTestData))] 101 | public void Remove(TestModelKinds testModelKind) 102 | { 103 | using var history = new History(); 104 | var model = CreateBasicTestModel(testModelKind, history); 105 | 106 | model.IntCollection = new ObservableCollection(); 107 | 108 | model.IntCollection.Add(100); 109 | model.IntCollection.Add(101); 110 | model.IntCollection.Add(102); 111 | model.IntCollection.Add(103); 112 | 113 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 114 | 115 | model.IntCollection.Remove(103); 116 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 117 | 118 | history.Undo(); 119 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 120 | 121 | history.Redo(); 122 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 123 | } 124 | 125 | [Theory] 126 | [ClassData(typeof(TestModelKindsTestData))] 127 | public void RemoveAt(TestModelKinds testModelKind) 128 | { 129 | using var history = new History(); 130 | var model = CreateBasicTestModel(testModelKind, history); 131 | 132 | model.IntCollection = new ObservableCollection(); 133 | 134 | model.IntCollection.Add(100); 135 | model.IntCollection.Add(101); 136 | model.IntCollection.Add(102); 137 | model.IntCollection.Add(103); 138 | 139 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 140 | 141 | model.IntCollection.RemoveAt(3); 142 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 143 | 144 | history.Undo(); 145 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 146 | 147 | history.Redo(); 148 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 149 | } 150 | 151 | [Theory] 152 | [ClassData(typeof(TestModelKindsTestData))] 153 | public void Insert(TestModelKinds testModelKind) 154 | { 155 | using var history = new History(); 156 | var model = CreateBasicTestModel(testModelKind, history); 157 | 158 | model.IntCollection = new ObservableCollection(); 159 | 160 | model.IntCollection.Add(100); 161 | model.IntCollection.Add(101); 162 | model.IntCollection.Add(102); 163 | model.IntCollection.Add(103); 164 | 165 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 166 | 167 | model.IntCollection.Insert(2, 999); 168 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 999, 102, 103})); 169 | 170 | history.Undo(); 171 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 172 | 173 | history.Redo(); 174 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 999, 102, 103})); 175 | } 176 | 177 | [Theory] 178 | [ClassData(typeof(TestModelKindsTestData))] 179 | public void Clear(TestModelKinds testModelKind) 180 | { 181 | using var history = new History(); 182 | var model = CreateBasicTestModel(testModelKind, history); 183 | 184 | model.IntCollection = new ObservableCollection(); 185 | 186 | model.IntCollection.Add(100); 187 | model.IntCollection.Add(101); 188 | model.IntCollection.Add(102); 189 | model.IntCollection.Add(103); 190 | 191 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 192 | 193 | Assert.Throws(() => 194 | model.IntCollection.Clear() 195 | ); 196 | } 197 | 198 | [Theory] 199 | [ClassData(typeof(TestModelKindsTestData))] 200 | public void ClearEx(TestModelKinds testModelKind) 201 | { 202 | using var history = new History(); 203 | var model = CreateBasicTestModel(testModelKind, history); 204 | 205 | model.IntCollection = new ObservableCollection(); 206 | 207 | model.IntCollection.Add(100); 208 | model.IntCollection.Add(101); 209 | model.IntCollection.Add(102); 210 | model.IntCollection.Add(103); 211 | 212 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 213 | 214 | model.IntCollection.ClearEx(history); 215 | 216 | Assert.True(model.IntCollection.SequenceEqual(new int[] { })); 217 | 218 | history.Undo(); 219 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 220 | 221 | history.Redo(); 222 | Assert.True(model.IntCollection.SequenceEqual(new int[] { })); 223 | } 224 | 225 | [Theory] 226 | [ClassData(typeof(TestModelKindsTestData))] 227 | public void Replace(TestModelKinds testModelKind) 228 | { 229 | using var history = new History(); 230 | var model = CreateBasicTestModel(testModelKind, history); 231 | 232 | model.IntCollection = new ObservableCollection(); 233 | 234 | model.IntCollection.Add(100); 235 | model.IntCollection.Add(101); 236 | model.IntCollection.Add(102); 237 | model.IntCollection.Add(103); 238 | 239 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 240 | 241 | model.IntCollection[2] = 999; 242 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 999, 103})); 243 | 244 | history.Undo(); 245 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 246 | 247 | history.Redo(); 248 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 999, 103})); 249 | } 250 | 251 | [Theory] 252 | [ClassData(typeof(TestModelKindsTestData))] 253 | public void CollectionChanged_on_Undo_Redo(TestModelKinds testModelKind) 254 | { 255 | using var history = new History(); 256 | var model = CreateBasicTestModel(testModelKind, history); 257 | 258 | model.IntCollection = new ObservableCollection(); 259 | 260 | var count = 0; 261 | // ReSharper disable once AccessToModifiedClosure 262 | model.IntCollection.CollectionChanged += (_, __) => ++count; 263 | 264 | model.IntCollection.Add(100); 265 | model.IntCollection.Add(101); 266 | model.IntCollection.Add(102); 267 | model.IntCollection.Add(103); 268 | 269 | count = 0; 270 | 271 | var oldCount = count; 272 | history.Undo(); 273 | Assert.NotEqual(oldCount, count); 274 | 275 | oldCount = count; 276 | history.Redo(); 277 | Assert.NotEqual(oldCount, count); 278 | 279 | 280 | 281 | model.IntCollection.Move(0, 3); 282 | 283 | oldCount = count; 284 | history.Undo(); 285 | Assert.NotEqual(oldCount, count); 286 | 287 | oldCount = count; 288 | history.Redo(); 289 | Assert.NotEqual(oldCount, count); 290 | 291 | 292 | 293 | model.IntCollection.Remove(100); 294 | 295 | oldCount = count; 296 | history.Undo(); 297 | Assert.NotEqual(oldCount, count); 298 | 299 | oldCount = count; 300 | history.Redo(); 301 | Assert.NotEqual(oldCount, count); 302 | 303 | 304 | 305 | model.IntCollection[2] = 999; 306 | 307 | oldCount = count; 308 | history.Undo(); 309 | Assert.NotEqual(oldCount, count); 310 | 311 | oldCount = count; 312 | history.Redo(); 313 | Assert.NotEqual(oldCount, count); 314 | } 315 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/FlagPropertyTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using Xunit; 3 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 4 | 5 | namespace Jewelry.EditingSystem.Tests; 6 | 7 | public sealed class FlagPropertyTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(TestModelKindsTestData))] 11 | public void BasicByte(TestModelKinds testModelKind) 12 | { 13 | using var history = new History(); 14 | var model = CreateFlagTestModel(testModelKind, history); 15 | 16 | Assert.False(model.IsA); 17 | Assert.False(model.IsB); 18 | Assert.False(model.IsC); 19 | Assert.False(history.CanUndo); 20 | Assert.False(history.CanRedo); 21 | 22 | model.IsA = true; 23 | Assert.True(model.IsA); 24 | Assert.False(model.IsB); 25 | Assert.False(model.IsC); 26 | 27 | model.IsB = true; 28 | Assert.True(model.IsA); 29 | Assert.True(model.IsB); 30 | Assert.False(model.IsC); 31 | 32 | model.IsC = true; 33 | Assert.True(model.IsA); 34 | Assert.True(model.IsB); 35 | Assert.True(model.IsC); 36 | 37 | history.Undo(); 38 | Assert.True(model.IsA); 39 | Assert.True(model.IsB); 40 | Assert.False(model.IsC); 41 | 42 | history.Undo(); 43 | Assert.True(model.IsA); 44 | Assert.False(model.IsB); 45 | Assert.False(model.IsC); 46 | 47 | history.Undo(); 48 | Assert.False(model.IsA); 49 | Assert.False(model.IsB); 50 | Assert.False(model.IsC); 51 | 52 | history.Redo(); 53 | Assert.True(model.IsA); 54 | Assert.False(model.IsB); 55 | Assert.False(model.IsC); 56 | 57 | history.Redo(); 58 | Assert.True(model.IsA); 59 | Assert.True(model.IsB); 60 | Assert.False(model.IsC); 61 | 62 | history.Redo(); 63 | Assert.True(model.IsA); 64 | Assert.True(model.IsB); 65 | Assert.True(model.IsC); 66 | } 67 | 68 | [Theory] 69 | [ClassData(typeof(TestModelKindsTestData))] 70 | public void BasicUint(TestModelKinds testModelKind) 71 | { 72 | using var history = new History(); 73 | var model = CreateFlagTestModel(testModelKind, history); 74 | 75 | Assert.False(model.IsA); 76 | Assert.False(model.IsB); 77 | Assert.False(model.IsC); 78 | Assert.False(history.CanUndo); 79 | Assert.False(history.CanRedo); 80 | 81 | model.IsA = true; 82 | Assert.True(model.IsA); 83 | Assert.False(model.IsB); 84 | Assert.False(model.IsC); 85 | 86 | model.IsB = true; 87 | Assert.True(model.IsA); 88 | Assert.True(model.IsB); 89 | Assert.False(model.IsC); 90 | 91 | model.IsC = true; 92 | Assert.True(model.IsA); 93 | Assert.True(model.IsB); 94 | Assert.True(model.IsC); 95 | 96 | history.Undo(); 97 | Assert.True(model.IsA); 98 | Assert.True(model.IsB); 99 | Assert.False(model.IsC); 100 | 101 | history.Undo(); 102 | Assert.True(model.IsA); 103 | Assert.False(model.IsB); 104 | Assert.False(model.IsC); 105 | 106 | history.Undo(); 107 | Assert.False(model.IsA); 108 | Assert.False(model.IsB); 109 | Assert.False(model.IsC); 110 | 111 | history.Redo(); 112 | Assert.True(model.IsA); 113 | Assert.False(model.IsB); 114 | Assert.False(model.IsC); 115 | 116 | history.Redo(); 117 | Assert.True(model.IsA); 118 | Assert.True(model.IsB); 119 | Assert.False(model.IsC); 120 | 121 | history.Redo(); 122 | Assert.True(model.IsA); 123 | Assert.True(model.IsB); 124 | Assert.True(model.IsC); 125 | } 126 | 127 | [Theory] 128 | [ClassData(typeof(TestModelKindsTestData))] 129 | public void BasicUlong(TestModelKinds testModelKind) 130 | { 131 | using var history = new History(); 132 | var model = CreateFlagTestModel(testModelKind, history); 133 | 134 | Assert.False(model.IsA); 135 | Assert.False(model.IsB); 136 | Assert.False(model.IsC); 137 | Assert.False(history.CanUndo); 138 | Assert.False(history.CanRedo); 139 | 140 | model.IsA = true; 141 | Assert.True(model.IsA); 142 | Assert.False(model.IsB); 143 | Assert.False(model.IsC); 144 | 145 | model.IsB = true; 146 | Assert.True(model.IsA); 147 | Assert.True(model.IsB); 148 | Assert.False(model.IsC); 149 | 150 | model.IsC = true; 151 | Assert.True(model.IsA); 152 | Assert.True(model.IsB); 153 | Assert.True(model.IsC); 154 | 155 | history.Undo(); 156 | Assert.True(model.IsA); 157 | Assert.True(model.IsB); 158 | Assert.False(model.IsC); 159 | 160 | history.Undo(); 161 | Assert.True(model.IsA); 162 | Assert.False(model.IsB); 163 | Assert.False(model.IsC); 164 | 165 | history.Undo(); 166 | Assert.False(model.IsA); 167 | Assert.False(model.IsB); 168 | Assert.False(model.IsC); 169 | 170 | history.Redo(); 171 | Assert.True(model.IsA); 172 | Assert.False(model.IsB); 173 | Assert.False(model.IsC); 174 | 175 | history.Redo(); 176 | Assert.True(model.IsA); 177 | Assert.True(model.IsB); 178 | Assert.False(model.IsC); 179 | 180 | history.Redo(); 181 | Assert.True(model.IsA); 182 | Assert.True(model.IsB); 183 | Assert.True(model.IsC); 184 | } 185 | 186 | [Theory] 187 | [ClassData(typeof(TestModelKindsTestData))] 188 | public void ChangingCount(TestModelKinds testModelKind) 189 | { 190 | using var history = new History(); 191 | var model = CreateFlagTestModel(testModelKind, history); 192 | 193 | model.IsA = true; 194 | Assert.Equal(1, model.ChangingCount); 195 | 196 | model.IsB = true; 197 | Assert.Equal(2, model.ChangingCount); 198 | 199 | model.IsB = true; 200 | Assert.Equal(2, model.ChangingCount); 201 | 202 | model.IsC = true; 203 | Assert.Equal(3, model.ChangingCount); 204 | } 205 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/HistoryTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Jewelry.EditingSystem.Tests; 4 | 5 | public sealed class HistoryTests 6 | { 7 | [Fact] 8 | public void Undoable_if_CanUndo_is_false() 9 | { 10 | using var history = new History(); 11 | 12 | Assert.False(history.CanUndo); 13 | history.Undo(); 14 | } 15 | 16 | [Fact] 17 | public void Redoable_if_CanRedo_is_false() 18 | { 19 | using var history = new History(); 20 | 21 | Assert.False(history.CanRedo); 22 | history.Redo(); 23 | } 24 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/PauseEditingTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using System; 3 | using Xunit; 4 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 5 | 6 | namespace Jewelry.EditingSystem.Tests; 7 | 8 | public sealed class PauseEditingTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(TestModelKindsTestData))] 12 | public void Basic(TestModelKinds testModelKind) 13 | { 14 | using var history = new History(); 15 | var model = CreateBasicTestModel(testModelKind, history); 16 | 17 | history.BeginPause(); 18 | { 19 | model.IntValue = 10; 20 | model.IntValue = 11; 21 | model.IntValue = 12; 22 | 23 | model.StringValue = "A"; 24 | model.StringValue = "B"; 25 | model.StringValue = "C"; 26 | } 27 | history.EndPause(); 28 | 29 | Assert.False(history.CanUndo); 30 | Assert.False(history.CanRedo); 31 | } 32 | 33 | [Theory] 34 | [ClassData(typeof(TestModelKindsTestData))] 35 | public void NestingPause(TestModelKinds testModelKind) 36 | { 37 | using var history = new History(); 38 | var model = CreateBasicTestModel(testModelKind, history); 39 | 40 | Assert.False(history.CanUndo); 41 | Assert.False(history.CanRedo); 42 | 43 | history.BeginPause(); 44 | { 45 | model.IntValue = 10; 46 | 47 | history.BeginPause(); 48 | { 49 | model.IntValue = 11; 50 | 51 | history.BeginPause(); 52 | { 53 | model.IntValue = 12; 54 | model.StringValue = "A"; 55 | } 56 | history.EndPause(); 57 | 58 | model.StringValue = "B"; 59 | } 60 | history.EndPause(); 61 | 62 | model.StringValue = "C"; 63 | } 64 | history.EndPause(); 65 | 66 | Assert.False(history.CanUndo); 67 | Assert.False(history.CanRedo); 68 | } 69 | 70 | [Theory] 71 | [ClassData(typeof(TestModelKindsTestData))] 72 | public void Cannot_call_undo_during_pause(TestModelKinds testModelKind) 73 | { 74 | using var history = new History(); 75 | var model = CreateBasicTestModel(testModelKind, history); 76 | 77 | Assert.False(history.CanUndo); 78 | Assert.False(history.CanRedo); 79 | 80 | model.IntValue = 999; 81 | model.StringValue = "XYZ"; 82 | 83 | history.BeginPause(); 84 | 85 | Assert.Throws(() => 86 | history.Undo() 87 | ); 88 | } 89 | 90 | [Theory] 91 | [ClassData(typeof(TestModelKindsTestData))] 92 | public void Cannot_call_redo_during_pause(TestModelKinds testModelKind) 93 | { 94 | using var history = new History(); 95 | var model = CreateBasicTestModel(testModelKind, history); 96 | 97 | Assert.False(history.CanUndo); 98 | Assert.False(history.CanRedo); 99 | 100 | model.IntValue = 999; 101 | model.StringValue = "XYZ"; 102 | 103 | history.BeginPause(); 104 | 105 | Assert.Throws(() => 106 | history.Redo() 107 | ); 108 | } 109 | 110 | [Theory] 111 | [ClassData(typeof(TestModelKindsTestData))] 112 | public void Pause_has_not_begun(TestModelKinds testModelKind) 113 | { 114 | using var history = new History(); 115 | var model = CreateBasicTestModel(testModelKind, history); 116 | 117 | Assert.False(history.CanUndo); 118 | Assert.False(history.CanRedo); 119 | 120 | model.IntValue = 999; 121 | model.StringValue = "XYZ"; 122 | 123 | Assert.Throws(() => 124 | history.EndPause() 125 | ); 126 | } 127 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/SetEditablePropertyTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using Xunit; 3 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 4 | 5 | namespace Jewelry.EditingSystem.Tests; 6 | 7 | public sealed class SetEditablePropertyTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(TestModelKindsTestData))] 11 | public void Basic(TestModelKinds testModelKind) 12 | { 13 | using var history = new History(); 14 | var model = CreateBasicTestModel(testModelKind, history); 15 | 16 | model.IntValue = 123; 17 | Assert.Equal(1, model.ChangingCount); 18 | 19 | model.IntValue = 456; 20 | Assert.Equal(2, model.ChangingCount); 21 | 22 | model.IntValue = 456; 23 | Assert.Equal(2, model.ChangingCount); 24 | 25 | model.IntValue = 123; 26 | Assert.Equal(3, model.ChangingCount); 27 | } 28 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/SinglePropertyTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using System.ComponentModel; 3 | using Xunit; 4 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 5 | 6 | namespace Jewelry.EditingSystem.Tests; 7 | 8 | public sealed class SinglePropertyTests 9 | { 10 | [Theory] 11 | [ClassData(typeof(TestModelKindsTestData))] 12 | public void Basic(TestModelKinds testModelKind) 13 | { 14 | using var history = new History(); 15 | var model = CreateBasicTestModel(testModelKind, history); 16 | 17 | Assert.Equal(0, model.IntValue); 18 | Assert.False(history.CanUndo); 19 | Assert.False(history.CanRedo); 20 | 21 | //------------------------------------------------ 22 | model.IntValue = 123; 23 | Assert.Equal(123, model.IntValue); 24 | Assert.True(history.CanUndo); 25 | Assert.False(history.CanRedo); 26 | 27 | model.IntValue = 456; 28 | Assert.Equal(456, model.IntValue); 29 | Assert.True(history.CanUndo); 30 | Assert.False(history.CanRedo); 31 | 32 | model.IntValue = 789; 33 | Assert.Equal(789, model.IntValue); 34 | Assert.True(history.CanUndo); 35 | Assert.False(history.CanRedo); 36 | 37 | history.Undo(); 38 | Assert.Equal(456, model.IntValue); 39 | Assert.True(history.CanUndo); 40 | Assert.True(history.CanRedo); 41 | 42 | history.Undo(); 43 | Assert.Equal(123, model.IntValue); 44 | Assert.True(history.CanUndo); 45 | Assert.True(history.CanRedo); 46 | 47 | history.Undo(); 48 | Assert.Equal(0, model.IntValue); 49 | Assert.False(history.CanUndo); 50 | Assert.True(history.CanRedo); 51 | 52 | //------------------------------------------------ 53 | history.Redo(); 54 | Assert.Equal(123, model.IntValue); 55 | Assert.True(history.CanUndo); 56 | Assert.True(history.CanRedo); 57 | 58 | history.Redo(); 59 | Assert.Equal(456, model.IntValue); 60 | Assert.True(history.CanUndo); 61 | Assert.True(history.CanRedo); 62 | 63 | history.Redo(); 64 | Assert.Equal(789, model.IntValue); 65 | Assert.True(history.CanUndo); 66 | Assert.False(history.CanRedo); 67 | 68 | //------------------------------------------------ 69 | history.Undo(); 70 | history.Undo(); 71 | history.Undo(); 72 | 73 | Assert.False(history.CanUndo); 74 | Assert.True(history.CanRedo); 75 | 76 | 77 | model.IntValue = 111; 78 | Assert.True(history.CanUndo); 79 | Assert.False(history.CanRedo); 80 | } 81 | 82 | [Theory] 83 | [ClassData(typeof(TestModelKindsTestData))] 84 | public void PropertyChanged(TestModelKinds testModelKind) 85 | { 86 | using var history = new History(); 87 | var model = CreateBasicTestModel(testModelKind, history); 88 | 89 | var count = 0; 90 | 91 | model.PropertyChanged += (_, e) => 92 | { 93 | if (e.PropertyName == nameof(model.IntValue)) 94 | ++count; 95 | }; 96 | 97 | Assert.Equal(0, count); 98 | 99 | model.IntValue = 123; 100 | Assert.Equal(1, count); 101 | 102 | model.IntValue = 456; 103 | Assert.Equal(2, count); 104 | 105 | history.Undo(); 106 | Assert.Equal(3, count); 107 | 108 | history.Redo(); 109 | Assert.Equal(4, count); 110 | } 111 | 112 | [Theory] 113 | [ClassData(typeof(TestModelKindsTestData))] 114 | public void PropertyChanged_CanUndo_CanRedo_CanClear(TestModelKinds testModelKind) 115 | { 116 | using var history = new History(); 117 | var model = CreateBasicTestModel(testModelKind, history); 118 | 119 | var canUndoCount = 0; 120 | var canRedoCount = 0; 121 | var canClearCount = 0; 122 | 123 | void HistoryOnPropertyChanged(object? sender, PropertyChangedEventArgs e) 124 | { 125 | if (e.PropertyName == "CanUndo") ++canUndoCount; 126 | if (e.PropertyName == "CanRedo") ++canRedoCount; 127 | if (e.PropertyName == "CanClear") ++canClearCount; 128 | } 129 | 130 | history.PropertyChanged += HistoryOnPropertyChanged; 131 | 132 | model.IntValue = 123; 133 | Assert.Equal(1, canUndoCount); 134 | Assert.Equal(0, canRedoCount); 135 | Assert.Equal(1, canClearCount); 136 | 137 | model.IntValue = 456; 138 | Assert.Equal(1, canUndoCount); 139 | Assert.Equal(0, canRedoCount); 140 | Assert.Equal(1, canClearCount); 141 | 142 | history.Undo(); 143 | Assert.Equal(1, canUndoCount); 144 | Assert.Equal(1, canRedoCount); 145 | Assert.Equal(1, canClearCount); 146 | 147 | history.Undo(); 148 | Assert.Equal(2, canUndoCount); 149 | Assert.Equal(1, canRedoCount); 150 | Assert.Equal(1, canClearCount); 151 | } 152 | 153 | [Theory] 154 | [ClassData(typeof(TestModelKindsTestData))] 155 | public void Clear(TestModelKinds testModelKind) 156 | { 157 | using var history = new History(); 158 | var model = CreateBasicTestModel(testModelKind, history); 159 | 160 | Assert.Equal(0, model.IntValue); 161 | Assert.False(history.CanUndo); 162 | Assert.False(history.CanRedo); 163 | Assert.False(history.CanClear); 164 | 165 | //------------------------------------------------ 166 | model.IntValue = 123; 167 | Assert.Equal(123, model.IntValue); 168 | Assert.True(history.CanUndo); 169 | Assert.False(history.CanRedo); 170 | Assert.True(history.CanClear); 171 | 172 | history.Clear(); 173 | Assert.False(history.CanUndo); 174 | Assert.False(history.CanRedo); 175 | Assert.False(history.CanClear); 176 | 177 | //------------------------------------------------ 178 | model.IntValue = 456; 179 | Assert.Equal(456, model.IntValue); 180 | Assert.True(history.CanUndo); 181 | Assert.False(history.CanRedo); 182 | Assert.True(history.CanClear); 183 | 184 | history.Undo(); 185 | Assert.False(history.CanUndo); 186 | Assert.True(history.CanRedo); 187 | Assert.True(history.CanClear); 188 | 189 | history.Clear(); 190 | Assert.False(history.CanUndo); 191 | Assert.False(history.CanRedo); 192 | Assert.False(history.CanClear); 193 | } 194 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.Tests/Tests/UndoRedoCountTests.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.Tests.TestModels; 2 | using Xunit; 3 | using static Jewelry.EditingSystem.Tests.TestModels.TestModelCreator; 4 | 5 | namespace Jewelry.EditingSystem.Tests; 6 | 7 | public sealed class UndoRedoCountTests 8 | { 9 | [Theory] 10 | [ClassData(typeof(TestModelKindsTestData))] 11 | public void Basic(TestModelKinds testModelKind) 12 | { 13 | using var history = new History(); 14 | var model = CreateBasicTestModel(testModelKind, history); 15 | 16 | Assert.Equal(0, history.UndoCount); 17 | Assert.Equal(0, history.RedoCount); 18 | 19 | model.IntValue = 123; 20 | Assert.Equal(1, history.UndoCount); 21 | Assert.Equal(0, history.RedoCount); 22 | 23 | model.IntValue = 456; 24 | Assert.Equal(2, history.UndoCount); 25 | Assert.Equal(0, history.RedoCount); 26 | 27 | history.Undo(); 28 | Assert.Equal(1, history.UndoCount); 29 | Assert.Equal(1, history.RedoCount); 30 | 31 | history.Clear(); 32 | Assert.Equal(0, history.UndoCount); 33 | Assert.Equal(0, history.RedoCount); 34 | } 35 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28307.271 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jewelry.EditingSystem", "Jewelry.EditingSystem\Jewelry.EditingSystem.csproj", "{8C937123-0B42-4A7B-997E-57BE485B92BE}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jewelry.EditingSystem.Tests", "Jewelry.EditingSystem.Tests\Jewelry.EditingSystem.Tests.csproj", "{1B174DA4-669F-4047-B131-CFDC4A3EDDE0}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {8C937123-0B42-4A7B-997E-57BE485B92BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {8C937123-0B42-4A7B-997E-57BE485B92BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {8C937123-0B42-4A7B-997E-57BE485B92BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {8C937123-0B42-4A7B-997E-57BE485B92BE}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {1B174DA4-669F-4047-B131-CFDC4A3EDDE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {1B174DA4-669F-4047-B131-CFDC4A3EDDE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {1B174DA4-669F-4047-B131-CFDC4A3EDDE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {1B174DA4-669F-4047-B131-CFDC4A3EDDE0}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {D66D4775-ECAF-43AA-8277-BB469FA5E5AC} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | 9999 3 | <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> 4 | <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb"><ExtraRule Prefix="_" Suffix="" Style="AaBb" /></Policy> 5 | <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> 6 | OnSave 7 | True 8 | LIVE_MONITOR 9 | LIVE_MONITOR 10 | LIVE_MONITOR 11 | DO_NOTHING 12 | LIVE_MONITOR 13 | LIVE_MONITOR 14 | LIVE_MONITOR 15 | LIVE_MONITOR 16 | LIVE_MONITOR 17 | LIVE_MONITOR 18 | LIVE_MONITOR 19 | LIVE_MONITOR 20 | DO_NOTHING 21 | LIVE_MONITOR 22 | True 23 | True 24 | True 25 | True 26 | True 27 | True 28 | True 29 | True 30 | True -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/CollectionChangedWeakEventManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Buffers; 3 | using System.Collections.Generic; 4 | using System.Collections.Specialized; 5 | 6 | namespace Jewelry.EditingSystem.WeakEvent; 7 | 8 | internal sealed class CollectionChangedWeakEventManager : IDisposable 9 | { 10 | private readonly Dictionary _listeners = new(); 11 | 12 | public void AddWeakEventListener(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler) 13 | { 14 | _listeners.Add(new CollectionChangedWeakEventListener(source, handler), handler); 15 | } 16 | 17 | public void RemoveWeakEventListener(INotifyCollectionChanged source) 18 | { 19 | var toRemoveListeners = ArrayPool.Shared.Rent(_listeners.Count); 20 | 21 | try 22 | { 23 | var count = 0; 24 | 25 | foreach (var listener in _listeners.Keys) 26 | { 27 | if (listener.IsAlive == false) 28 | toRemoveListeners[count++] = listener; 29 | 30 | else if (listener.Source == source) 31 | { 32 | listener.Dispose(); 33 | toRemoveListeners[count++] = listener; 34 | } 35 | } 36 | 37 | for (var i = 0; i != count; ++i) 38 | _listeners.Remove(toRemoveListeners[i]); 39 | } 40 | finally 41 | { 42 | ArrayPool.Shared.Return(toRemoveListeners); 43 | } 44 | } 45 | 46 | public void Dispose() 47 | { 48 | foreach (var listener in _listeners.Keys) 49 | { 50 | if (listener.IsAlive == false) 51 | continue; 52 | 53 | listener.Dispose(); 54 | } 55 | 56 | _listeners.Clear(); 57 | } 58 | 59 | private sealed class CollectionChangedWeakEventListener : IDisposable 60 | { 61 | public bool IsAlive => _handler.TryGetTarget(out _) && _source.TryGetTarget(out _); 62 | public object? Source => _source.TryGetTarget(out var source) ? source : default; 63 | 64 | private readonly WeakReference _source; 65 | private readonly WeakReference _handler; 66 | 67 | public CollectionChangedWeakEventListener(INotifyCollectionChanged source, NotifyCollectionChangedEventHandler handler) 68 | { 69 | _source = new WeakReference(source); 70 | _handler = new WeakReference(handler); 71 | 72 | source.CollectionChanged += HandleEvent; 73 | } 74 | 75 | private void HandleEvent(object? sender, NotifyCollectionChangedEventArgs e) 76 | { 77 | if (_handler.TryGetTarget(out var handler)) 78 | handler(sender, e); 79 | else 80 | Dispose(); 81 | } 82 | 83 | public void Dispose() 84 | { 85 | if (_source.TryGetTarget(out var source)) 86 | source.CollectionChanged -= HandleEvent; 87 | } 88 | } 89 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/EditableModelBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.ComponentModel; 5 | using System.Numerics; 6 | using System.Runtime.CompilerServices; 7 | 8 | namespace Jewelry.EditingSystem; 9 | 10 | public class EditableModelBase : INotifyPropertyChanged 11 | { 12 | public event PropertyChangedEventHandler? PropertyChanged; 13 | 14 | protected EditableModelBase(History history) 15 | { 16 | _history = history; 17 | } 18 | 19 | protected bool SetEditableProperty(Action setValue, T oldValue, T newValue, [CallerMemberName] string propertyName = "") 20 | { 21 | void SetValueWithRaisePropertyChanged(T v) 22 | { 23 | setValue(v); 24 | RaisePropertyChanged(propertyName); 25 | } 26 | 27 | return EditablePropertyCommon.SetEditableProperty(_history, SetValueWithRaisePropertyChanged, oldValue, newValue); 28 | } 29 | 30 | protected bool SetEditableFlagProperty(Action setValue, T oldFlags, T newFlags, bool value, [CallerMemberName] string propertyName = "") 31 | where T : IBitwiseOperators, IEqualityOperators, IUnsignedNumber 32 | { 33 | void SetValueWithRaisePropertyChanged(T v) 34 | { 35 | setValue(v); 36 | RaisePropertyChanged(propertyName); 37 | } 38 | 39 | return EditablePropertyCommon.SetEditableFlagProperty(_history, SetValueWithRaisePropertyChanged, oldFlags, newFlags, value); 40 | } 41 | 42 | protected bool SetPropertyWithoutHistory(ref T storage, T value, [CallerMemberName] string propertyName = "") 43 | { 44 | if (EqualityComparer.Default.Equals(storage, value)) 45 | return false; 46 | 47 | storage = value; 48 | 49 | RaisePropertyChanged(propertyName); 50 | 51 | return true; 52 | } 53 | 54 | protected bool SetFlagPropertyWithoutHistory(ref T storage, T flag, bool value, [CallerMemberName] string propertyName = "") 55 | where T : struct, IBitwiseOperators, IEqualityOperators, IUnsignedNumber 56 | { 57 | if (value) 58 | { 59 | if ((storage & flag) != default) 60 | return false; 61 | 62 | storage |= flag; 63 | } 64 | else 65 | { 66 | if ((storage & flag) == default) 67 | return false; 68 | 69 | storage &= ~flag; 70 | } 71 | 72 | RaisePropertyChanged(propertyName); 73 | 74 | return true; 75 | } 76 | 77 | protected void RaisePropertyChanged([CallerMemberName] string propertyName = "") 78 | { 79 | if (PropertyChanged is null) 80 | return; 81 | 82 | var pc = PropChanged.GetOrAdd(propertyName, name => new PropertyChangedEventArgs(name)); 83 | 84 | PropertyChanged.Invoke(this, pc); 85 | } 86 | 87 | private readonly History _history; 88 | private static readonly ConcurrentDictionary PropChanged = new(); 89 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/EditablePropertyCommon.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Specialized; 4 | using System.Numerics; 5 | 6 | namespace Jewelry.EditingSystem; 7 | 8 | internal static class EditablePropertyCommon 9 | { 10 | internal static bool SetEditableProperty(History history, Action setValue, T oldValue, T newValue) 11 | { 12 | if (EqualityComparer.Default.Equals(oldValue, newValue)) 13 | return false; 14 | 15 | 16 | history.Push(() => setValue(oldValue), () => setValue(newValue)); 17 | 18 | if (oldValue is INotifyCollectionChanged oldNotifyCollectionChanged) 19 | history._collectionChangedWeakEventManager.RemoveWeakEventListener(oldNotifyCollectionChanged); 20 | 21 | if (newValue is INotifyCollectionChanged newNotifyCollectionChanged) 22 | history._collectionChangedWeakEventManager.AddWeakEventListener(newNotifyCollectionChanged, history.OnCollectionPropertyCollectionChanged); 23 | 24 | setValue(newValue); 25 | return true; 26 | } 27 | 28 | internal static bool SetEditableFlagProperty(History history, Action setValue, T oldFlags, T newFlags, bool value) 29 | where T : IBitwiseOperators, IEqualityOperators, IUnsignedNumber 30 | { 31 | var newValue = oldFlags; 32 | 33 | if (value) 34 | { 35 | if ((oldFlags & newFlags) != default) 36 | return false; 37 | 38 | newValue |= newFlags; 39 | } 40 | else 41 | { 42 | if ((oldFlags & newFlags) == default) 43 | return false; 44 | 45 | newValue &= ~newFlags; 46 | } 47 | 48 | history.Push(() => setValue(oldFlags), () => setValue(newValue)); 49 | 50 | setValue(newValue); 51 | return true; 52 | } 53 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/History.cs: -------------------------------------------------------------------------------- 1 | using Jewelry.EditingSystem.WeakEvent; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Collections.Specialized; 6 | using System.ComponentModel; 7 | using System.Diagnostics; 8 | 9 | namespace Jewelry.EditingSystem; 10 | 11 | public class History : INotifyPropertyChanged, IDisposable 12 | { 13 | public bool CanUndo => _undoStack.Count > 0; 14 | public bool CanRedo => _redoStack.Count > 0; 15 | public bool CanClear => CanUndo || CanRedo; 16 | public int UndoCount => _undoStack.Count; 17 | public int RedoCount => _redoStack.Count; 18 | public int PauseDepth { get; private set; } 19 | public int BatchDepth { get; private set; } 20 | public bool IsInUndoing { get; private set; } 21 | public bool IsInPaused => PauseDepth > 0; 22 | public bool IsInBatch => BatchDepth > 0; 23 | 24 | public event PropertyChangedEventHandler? PropertyChanged; 25 | 26 | internal readonly CollectionChangedWeakEventManager _collectionChangedWeakEventManager = new(); 27 | 28 | public void Dispose() 29 | { 30 | _collectionChangedWeakEventManager.Dispose(); 31 | } 32 | 33 | public void BeginPause() 34 | { 35 | ++PauseDepth; 36 | } 37 | 38 | public void EndPause() 39 | { 40 | if (PauseDepth == 0) 41 | throw new InvalidOperationException("Pause is not begun."); 42 | 43 | --PauseDepth; 44 | } 45 | 46 | public void BeginBatch() 47 | { 48 | ++BatchDepth; 49 | 50 | if (BatchDepth == 1) 51 | BeginBatchInternal(); 52 | } 53 | 54 | public void EndBatch() 55 | { 56 | if (BatchDepth == 0) 57 | throw new InvalidOperationException("Batch recording has not begun."); 58 | 59 | --BatchDepth; 60 | 61 | if (BatchDepth == 0) 62 | EndBatchInternal(); 63 | } 64 | 65 | public void Undo() 66 | { 67 | if (IsInBatch) 68 | throw new InvalidOperationException("Can't call Undo() during batch recording."); 69 | 70 | if (IsInPaused) 71 | throw new InvalidOperationException("Can't call Undo() during in paused."); 72 | 73 | if (CanUndo == false) 74 | return; 75 | 76 | var currentFlags = CanUndoRedoClear; 77 | var currentUndoRedoCount = UndoRedoCount; 78 | var currentDepth = PauseBatchDepth; 79 | 80 | var action = _undoStack.Pop(); 81 | 82 | try 83 | { 84 | IsInUndoing = true; 85 | action.Undo(); 86 | } 87 | finally 88 | { 89 | IsInUndoing = false; 90 | } 91 | 92 | _redoStack.Push(action); 93 | 94 | InvokePropertyChanged(currentFlags, currentUndoRedoCount, currentDepth); 95 | } 96 | 97 | public void Redo() 98 | { 99 | if (IsInBatch) 100 | throw new InvalidOperationException("Can't call Redo() during batch recording."); 101 | 102 | if (IsInPaused) 103 | throw new InvalidOperationException("Can't call Redo() during in paused."); 104 | 105 | if (CanRedo == false) 106 | return; 107 | 108 | var currentFlags = CanUndoRedoClear; 109 | var currentUndoRedoCount = UndoRedoCount; 110 | var currentDepth = PauseBatchDepth; 111 | 112 | var action = _redoStack.Pop(); 113 | 114 | try 115 | { 116 | IsInUndoing = true; 117 | action.Redo(); 118 | } 119 | finally 120 | { 121 | IsInUndoing = false; 122 | } 123 | 124 | _undoStack.Push(action); 125 | 126 | InvokePropertyChanged(currentFlags, currentUndoRedoCount, currentDepth); 127 | } 128 | 129 | public void Push(Action undo, Action redo) 130 | { 131 | if (IsInPaused) 132 | return; 133 | 134 | if (IsInBatch) 135 | { 136 | _ = _batchHistory ?? throw new NullReferenceException(); 137 | 138 | _batchHistory.Push(undo, redo); 139 | return; 140 | } 141 | 142 | var currentFlags = CanUndoRedoClear; 143 | var currentUndoRedoCount = UndoRedoCount; 144 | var currentDepth = PauseBatchDepth; 145 | 146 | _undoStack.Push(new HistoryAction(undo, redo)); 147 | 148 | if (_redoStack.Count > 0) 149 | _redoStack.Clear(); 150 | 151 | InvokePropertyChanged(currentFlags, currentUndoRedoCount, currentDepth); 152 | } 153 | 154 | public void Clear() 155 | { 156 | var currentFlags = CanUndoRedoClear; 157 | var currentUndoRedoCount = UndoRedoCount; 158 | var currentDepth = PauseBatchDepth; 159 | 160 | _undoStack.Clear(); 161 | _redoStack.Clear(); 162 | 163 | InvokePropertyChanged(currentFlags, currentUndoRedoCount, currentDepth); 164 | } 165 | 166 | internal void OnCollectionPropertyCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) 167 | { 168 | if (IsInUndoing) 169 | return; 170 | 171 | var list = sender as IList ?? throw new NullReferenceException(); 172 | 173 | switch (e.Action) 174 | { 175 | case NotifyCollectionChangedAction.Add: 176 | { 177 | void DoRedo() 178 | { 179 | var addItems = e.NewItems ?? throw new NullReferenceException(); 180 | var addCount = addItems.Count; 181 | var addIndex = e.NewStartingIndex; 182 | 183 | // ICollectionItem 184 | for (var i = 0; i != addCount; ++i) 185 | { 186 | list.Insert(addIndex + i, addItems[i]); 187 | 188 | if (addItems[i] is ICollectionItem collItem) 189 | collItem.Changed(CollectionItemChangedInfo.Add); 190 | } 191 | } 192 | 193 | void DoUndo() 194 | { 195 | var addItems = e.NewItems ?? throw new NullReferenceException(); 196 | var addCount = addItems.Count; 197 | var addIndex = e.NewStartingIndex; 198 | 199 | // ICollectionItem 200 | for (var i = 0; i != addCount; ++i) 201 | { 202 | list.RemoveAt(addIndex + i); 203 | 204 | if (addItems[i] is ICollectionItem collItem) 205 | collItem.Changed(CollectionItemChangedInfo.Remove); 206 | } 207 | } 208 | 209 | // ICollectionItem 210 | { 211 | var addItems = e.NewItems ?? throw new NullReferenceException(); 212 | var addCount = addItems.Count; 213 | 214 | for (var i = 0; i != addCount; ++i) 215 | { 216 | if (addItems[i] is ICollectionItem collItem) 217 | collItem.Changed(CollectionItemChangedInfo.Add); 218 | } 219 | } 220 | 221 | Push(DoUndo, DoRedo); 222 | break; 223 | } 224 | 225 | case NotifyCollectionChangedAction.Move: 226 | { 227 | _ = e.OldItems ?? throw new NullReferenceException(); 228 | _ = e.NewItems ?? throw new NullReferenceException(); 229 | 230 | if (e.OldItems.Count != 1) 231 | throw new NotImplementedException(); 232 | 233 | if (e.NewItems.Count != 1) 234 | throw new NotImplementedException(); 235 | 236 | void DoRedo() 237 | { 238 | var src = e.OldStartingIndex; 239 | var dst = e.NewStartingIndex; 240 | 241 | var item = list[src]; 242 | list.RemoveAt(src); 243 | 244 | list.Insert(dst, item); 245 | 246 | // ICollectionItem 247 | { 248 | if (item is ICollectionItem collItem) 249 | collItem.Changed(CollectionItemChangedInfo.Move); 250 | } 251 | } 252 | 253 | void DoUndo() 254 | { 255 | var src = e.NewStartingIndex; 256 | var dst = e.OldStartingIndex; 257 | 258 | var item = list[src]; 259 | list.RemoveAt(src); 260 | 261 | list.Insert(dst, item); 262 | 263 | // ICollectionItem 264 | if (item is ICollectionItem collItem) 265 | collItem.Changed(CollectionItemChangedInfo.Move); 266 | } 267 | 268 | // ICollectionItem 269 | { 270 | if (e.OldItems[0] is ICollectionItem collItem) 271 | collItem.Changed(CollectionItemChangedInfo.Move); 272 | } 273 | 274 | Push(DoUndo, DoRedo); 275 | break; 276 | } 277 | 278 | case NotifyCollectionChangedAction.Remove: 279 | { 280 | _ = e.OldItems ?? throw new NullReferenceException(); 281 | 282 | if (e.OldItems.Count != 1) 283 | throw new NotImplementedException(); 284 | 285 | if (e.NewItems is not null) 286 | throw new NotImplementedException(); 287 | 288 | var item = e.OldItems[0]; 289 | 290 | void DoRedo() 291 | { 292 | item = list[e.OldStartingIndex]; 293 | list.RemoveAt(e.OldStartingIndex); 294 | 295 | // ICollectionItem 296 | { 297 | if (item is ICollectionItem collItem) 298 | collItem.Changed(CollectionItemChangedInfo.Remove); 299 | } 300 | } 301 | 302 | void DoUndo() 303 | { 304 | list.Insert(e.OldStartingIndex, item); 305 | 306 | // ICollectionItem 307 | { 308 | if (item is ICollectionItem collItem) 309 | collItem.Changed(CollectionItemChangedInfo.Add); 310 | } 311 | } 312 | 313 | // ICollectionItem 314 | { 315 | if (e.OldItems[0] is ICollectionItem collItem) 316 | collItem.Changed(CollectionItemChangedInfo.Remove); 317 | } 318 | 319 | Push(DoUndo, DoRedo); 320 | break; 321 | } 322 | 323 | case NotifyCollectionChangedAction.Replace: 324 | { 325 | _ = e.OldItems ?? throw new NullReferenceException(); 326 | _ = e.NewItems ?? throw new NullReferenceException(); 327 | 328 | if (e.OldItems.Count != 1) 329 | throw new NotImplementedException(); 330 | 331 | if (e.NewItems.Count != 1) 332 | throw new NotImplementedException(); 333 | 334 | if (e.NewStartingIndex != e.OldStartingIndex) 335 | throw new NotImplementedException(); 336 | 337 | void DoRedo() 338 | { 339 | var index = e.OldStartingIndex; 340 | var oldItem = list[index]; 341 | list[index] = e.NewItems[0]; 342 | 343 | // ICollectionItem 344 | { 345 | if (oldItem is ICollectionItem oldCollItem) 346 | oldCollItem.Changed(CollectionItemChangedInfo.Remove); 347 | 348 | if (list[index] is ICollectionItem collItem) 349 | collItem.Changed(CollectionItemChangedInfo.Add); 350 | } 351 | } 352 | 353 | void DoUndo() 354 | { 355 | var index = e.OldStartingIndex; 356 | var oldItem = list[index]; 357 | list[index] = e.OldItems[0]; 358 | 359 | // ICollectionItem 360 | { 361 | if (oldItem is ICollectionItem oldCollItem) 362 | oldCollItem.Changed(CollectionItemChangedInfo.Add); 363 | 364 | if (list[index] is ICollectionItem collItem) 365 | collItem.Changed(CollectionItemChangedInfo.Remove); 366 | } 367 | } 368 | 369 | // ICollectionItem 370 | { 371 | if (e.OldItems[0] is ICollectionItem oldCollItem) 372 | oldCollItem.Changed(CollectionItemChangedInfo.Remove); 373 | 374 | if (e.NewItems[0] is ICollectionItem newCollItem) 375 | newCollItem.Changed(CollectionItemChangedInfo.Add); 376 | } 377 | 378 | Push(DoUndo, DoRedo); 379 | break; 380 | } 381 | 382 | case NotifyCollectionChangedAction.Reset: 383 | { 384 | if (IsInPaused) 385 | break; 386 | 387 | throw new NotSupportedException("Clear() is not support. Use ClearEx()"); 388 | } 389 | 390 | default: 391 | throw new ArgumentOutOfRangeException(); 392 | } 393 | } 394 | 395 | private void InvokePropertyChanged((bool CanUndo, bool CanRedo, bool CanClear) flags, (int UndoCount, int RedoCount) undoRedoCount, (int PauseDepth, int BatchDepth) depthCount) 396 | { 397 | if (PropertyChanged is null) 398 | return; 399 | 400 | if (flags.CanUndo != CanUndo) 401 | PropertyChanged.Invoke(this, CanUndoArgs); 402 | 403 | if (flags.CanRedo != CanRedo) 404 | PropertyChanged.Invoke(this, CanRedoArgs); 405 | 406 | if (flags.CanClear != CanClear) 407 | PropertyChanged.Invoke(this, CanClearArgs); 408 | 409 | if (undoRedoCount.UndoCount != UndoCount) 410 | PropertyChanged.Invoke(this, UndoCountArgs); 411 | 412 | if (undoRedoCount.RedoCount != RedoCount) 413 | PropertyChanged.Invoke(this, RedoCountArgs); 414 | 415 | if (depthCount.PauseDepth != PauseDepth) 416 | PropertyChanged.Invoke(this, PauseDepthArgs); 417 | 418 | if (depthCount.BatchDepth != BatchDepth) 419 | PropertyChanged.Invoke(this, BatchDepthArgs); 420 | } 421 | 422 | private void BeginBatchInternal() 423 | { 424 | Debug.Assert(_batchHistory is null); 425 | 426 | _batchHistory = new BatchHistory(); 427 | } 428 | 429 | private void EndBatchInternal() 430 | { 431 | Debug.Assert(_batchHistory is not null); 432 | 433 | if (_batchHistory.UndoRedoCount != (UndoCount: 0, RedoCount: 0)) 434 | Push(_batchHistory.UndoAll, _batchHistory.RedoAll); 435 | 436 | _batchHistory.Dispose(); 437 | _batchHistory = null; 438 | } 439 | 440 | private (int UndoCount, int RedoCount) UndoRedoCount => (UndoCount, RedoCount); 441 | private (bool CanUndo, bool CanRedo, bool CanClear) CanUndoRedoClear => (CanUndo, CanRedo, CanClear); 442 | private (int PauseDepth, int BatchDepth) PauseBatchDepth => (PauseDepth, BatchDepth); 443 | 444 | private BatchHistory? _batchHistory; 445 | 446 | private readonly Stack _undoStack = new(); 447 | private readonly Stack _redoStack = new(); 448 | 449 | private static readonly PropertyChangedEventArgs CanUndoArgs = new(nameof(CanUndo)); 450 | private static readonly PropertyChangedEventArgs CanRedoArgs = new(nameof(CanRedo)); 451 | private static readonly PropertyChangedEventArgs CanClearArgs = new(nameof(CanClear)); 452 | private static readonly PropertyChangedEventArgs UndoCountArgs = new(nameof(UndoCount)); 453 | private static readonly PropertyChangedEventArgs RedoCountArgs = new(nameof(RedoCount)); 454 | private static readonly PropertyChangedEventArgs PauseDepthArgs = new(nameof(PauseDepth)); 455 | private static readonly PropertyChangedEventArgs BatchDepthArgs = new(nameof(BatchDepth)); 456 | 457 | private sealed class BatchHistory : History 458 | { 459 | public void UndoAll() 460 | { 461 | while (CanUndo) 462 | Undo(); 463 | } 464 | 465 | public void RedoAll() 466 | { 467 | while (CanRedo) 468 | Redo(); 469 | } 470 | } 471 | 472 | private record struct HistoryAction(Action Undo, Action Redo); 473 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/ICollectionItem.cs: -------------------------------------------------------------------------------- 1 | namespace Jewelry.EditingSystem; 2 | 3 | public interface ICollectionItem 4 | { 5 | void Changed(in CollectionItemChangedInfo info); 6 | } 7 | 8 | public readonly struct CollectionItemChangedInfo 9 | { 10 | public readonly CollectionItemChangedType Type; 11 | 12 | private CollectionItemChangedInfo(in CollectionItemChangedType type) 13 | { 14 | Type = type; 15 | } 16 | 17 | public static readonly CollectionItemChangedInfo Add = new(CollectionItemChangedType.Add); 18 | public static readonly CollectionItemChangedInfo Remove = new(CollectionItemChangedType.Remove); 19 | public static readonly CollectionItemChangedInfo Move = new(CollectionItemChangedType.Move); 20 | } 21 | 22 | public enum CollectionItemChangedType 23 | { 24 | Add, 25 | Remove, 26 | Move 27 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/Jewelry.EditingSystem.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | enable 4 | latest 5 | Yoshihiro Ito 6 | Easy to use undo/redo system for .NET Standard. 7 | Copyright (c) 2018-2023 copyright Yoshihiro Ito (yo.i.jewelry.bab@gmail.com) 8 | https://github.com/YoshihiroIto/EditingSystem 9 | https://github.com/YoshihiroIto/EditingSystem 10 | undo editing 11 | true 12 | Jewelry.EditingSystem 13 | 2.1.1 14 | EditingSystem 15 | 16 | net7.0;net8.0 17 | 18 | 19 | -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/ListExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Jewelry.EditingSystem; 4 | 5 | public static class ListExtensions 6 | { 7 | public static void ClearEx(this IList self, History history) 8 | { 9 | try 10 | { 11 | history.BeginBatch(); 12 | 13 | while (self.Count != 0) 14 | self.RemoveAt(self.Count - 1); 15 | } 16 | finally 17 | { 18 | history.EndBatch(); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /EditingSystem/Jewelry.EditingSystem/NotifyPropertyChangedExtensionsForDirectMode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Numerics; 4 | using System.Runtime.CompilerServices; 5 | 6 | namespace Jewelry.EditingSystem; 7 | 8 | public static class NotifyPropertyChangedExtensionsForDirectMode 9 | { 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static bool SetEditableProperty( 12 | this INotifyPropertyChanged _, 13 | History history, 14 | Action setValue, T oldValue, T newValue) 15 | { 16 | return EditablePropertyCommon.SetEditableProperty(history, setValue, oldValue, newValue); 17 | } 18 | 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | public static bool SetEditableFlagProperty( 21 | this INotifyPropertyChanged _, 22 | History history, 23 | Action setValue, T oldFlags, T newFlags, bool value) 24 | where T : IBitwiseOperators, IEqualityOperators, IUnsignedNumber 25 | { 26 | return EditablePropertyCommon.SetEditableFlagProperty(history, setValue, oldFlags, newFlags, value); 27 | } 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Yoshihiro Ito 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EditingSystem 2 | [![Biaui NuGet package](https://img.shields.io/nuget/v/Jewelry.EditingSystem)](https://www.nuget.org/packages/Jewelry.EditingSystem) [![Build status](https://ci.appveyor.com/api/projects/status/x42th0lpkuldqhg8?svg=true)](https://ci.appveyor.com/project/YoshihiroIto/editingsystem) [![MIT License](http://img.shields.io/badge/license-MIT-lightgray)](LICENSE) 3 | 4 | Easy to use undo/redo system for .NET 5 | 6 | 7 | ## Install 8 | ``` 9 | PM> Install-Package Jewelry.EditingSystem 10 | ``` 11 | 12 | 13 | ## Example 14 | 15 | ```cs 16 | using Jewelry.EditingSystem; 17 | 18 | public class TestModel : EditableModelBase 19 | { 20 | public TestModel(History history) : base(history) 21 | { 22 | } 23 | 24 | #region IntValue 25 | 26 | private int _IntValue; 27 | 28 | public int IntValue 29 | { 30 | get => _IntValue; 31 | set => SetEditableProperty(v => _IntValue = v, _IntValue, value); 32 | } 33 | 34 | #endregion 35 | 36 | 37 | #region IntCollection 38 | 39 | private ObservableCollection _IntCollection = new(); 40 | 41 | public ObservableCollection IntCollection 42 | { 43 | get => _IntCollection; 44 | set => SetEditableProperty(v => _IntCollection = v, _IntCollection, value); 45 | } 46 | 47 | #endregion 48 | } 49 | 50 | public void Basic() 51 | { 52 | using var history = new History(); 53 | var model = new TestModel(history); 54 | 55 | 56 | 57 | model.IntValue = 123; 58 | model.IntValue = 456; 59 | model.IntValue = 789; 60 | 61 | 62 | 63 | history.Undo(); 64 | Assert.Equal(456, model.IntValue); 65 | 66 | history.Undo(); 67 | Assert.Equal(123, model.IntValue); 68 | 69 | history.Undo(); 70 | Assert.Equal(0, model.IntValue); 71 | 72 | 73 | 74 | history.Redo(); 75 | Assert.Equal(123, model.IntValue); 76 | 77 | history.Redo(); 78 | Assert.Equal(456, model.IntValue); 79 | 80 | history.Redo(); 81 | Assert.Equal(789, model.IntValue); 82 | } 83 | 84 | public void Collection() 85 | { 86 | using var history = new History(); 87 | var model = new TestModel(history); 88 | 89 | model.IntCollection = new ObservableCollection(); 90 | 91 | 92 | 93 | model.IntCollection.Add(100); 94 | model.IntCollection.Add(101); 95 | model.IntCollection.Add(102); 96 | model.IntCollection.Add(103); 97 | 98 | 99 | 100 | model.IntCollection.RemoveAt(3); 101 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 102 | 103 | 104 | 105 | history.Undo(); 106 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 107 | 108 | 109 | 110 | history.Redo(); 111 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 112 | } 113 | 114 | ``` 115 | 116 | ### Support plane INotifyPropertyChanged object 117 | 118 | Can be implemented in any way for INotifyPropertyChanged objects. 119 | 120 | ```cs 121 | public sealed class TestModel : INotifyPropertyChanged 122 | { 123 | private readonly History _history; 124 | 125 | public TestModel(History history) 126 | { 127 | _history = history; 128 | } 129 | 130 | #region IntValue 131 | 132 | private int _IntValue; 133 | 134 | public int IntValue 135 | { 136 | get => _IntValue; 137 | set => this.SetEditableProperty(_history, v => SetField(ref _IntValue, v), _IntValue, value); 138 | } 139 | 140 | #endregion 141 | 142 | #region IntCollection 143 | 144 | private ObservableCollection _IntCollection = new(); 145 | 146 | public ObservableCollection IntCollection 147 | { 148 | get => _IntCollection; 149 | set => this.SetEditableProperty(_history, v => SetField(ref _IntCollection, v), _IntCollection, value); 150 | } 151 | 152 | #endregion 153 | 154 | 155 | public event PropertyChangedEventHandler? PropertyChanged; 156 | 157 | private void OnPropertyChanged([CallerMemberName] string? propertyName = null) 158 | { 159 | PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 160 | } 161 | 162 | private bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) 163 | { 164 | if (EqualityComparer.Default.Equals(field, value)) return false; 165 | field = value; 166 | OnPropertyChanged(propertyName); 167 | return true; 168 | } 169 | } 170 | 171 | public void Basic() 172 | { 173 | using var history = new History(); 174 | var model = new TestModel(history); 175 | 176 | 177 | 178 | model.IntValue = 123; 179 | model.IntValue = 456; 180 | model.IntValue = 789; 181 | 182 | 183 | 184 | history.Undo(); 185 | Assert.Equal(456, model.IntValue); 186 | 187 | history.Undo(); 188 | Assert.Equal(123, model.IntValue); 189 | 190 | history.Undo(); 191 | Assert.Equal(0, model.IntValue); 192 | 193 | 194 | 195 | history.Redo(); 196 | Assert.Equal(123, model.IntValue); 197 | 198 | history.Redo(); 199 | Assert.Equal(456, model.IntValue); 200 | 201 | history.Redo(); 202 | Assert.Equal(789, model.IntValue); 203 | } 204 | 205 | public void Collection() 206 | { 207 | using var history = new History(); 208 | var model = new TestModel(history); 209 | 210 | model.IntCollection = new ObservableCollection(); 211 | 212 | 213 | 214 | model.IntCollection.Add(100); 215 | model.IntCollection.Add(101); 216 | model.IntCollection.Add(102); 217 | model.IntCollection.Add(103); 218 | 219 | 220 | 221 | model.IntCollection.RemoveAt(3); 222 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 223 | 224 | 225 | 226 | history.Undo(); 227 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102, 103})); 228 | 229 | 230 | 231 | history.Redo(); 232 | Assert.True(model.IntCollection.SequenceEqual(new[] {100, 101, 102})); 233 | } 234 | ``` 235 | 236 | 237 | ## Author 238 | 239 | Yoshihiro Ito 240 | Twitter: [https://twitter.com/yoiyoi322](https://twitter.com/yoiyoi322) 241 | Email: yo.i.jewelry.bab@gmail.com 242 | 243 | 244 | ## License 245 | 246 | MIT 247 | 248 | 249 | --------------------------------------------------------------------------------