├── .gitignore ├── LICENSE ├── ObservableCollectionEx.sln ├── ObservableCollectionEx ├── ObservableCollectionEx.cs └── ObservableCollectionEx.csproj ├── README.md ├── UnitTests ├── ObservableCollectionExTests.cs ├── ObservableCollectionTests.cs └── UnitTests.csproj └── appveyor.yml /.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 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Eugene Sadovoi 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 | -------------------------------------------------------------------------------- /ObservableCollectionEx.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservableCollectionEx", "ObservableCollectionEx\ObservableCollectionEx.csproj", "{8743156C-E04F-4B10-80CC-BD219444A3FF}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B19B7683-A367-42B4-8C2D-111604EEA3A0}" 9 | ProjectSection(SolutionItems) = preProject 10 | appveyor.yml = appveyor.yml 11 | LICENSE = LICENSE 12 | EndProjectSection 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests", "UnitTests\UnitTests.csproj", "{9937131D-6C22-42AA-9A5A-82D9C8AA5C19}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {8743156C-E04F-4B10-80CC-BD219444A3FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {8743156C-E04F-4B10-80CC-BD219444A3FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {8743156C-E04F-4B10-80CC-BD219444A3FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {8743156C-E04F-4B10-80CC-BD219444A3FF}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {9937131D-6C22-42AA-9A5A-82D9C8AA5C19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {9937131D-6C22-42AA-9A5A-82D9C8AA5C19}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {9937131D-6C22-42AA-9A5A-82D9C8AA5C19}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {9937131D-6C22-42AA-9A5A-82D9C8AA5C19}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {1000DFD5-F14B-45DA-99B0-094E183B0DF7} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /ObservableCollectionEx/ObservableCollectionEx.cs: -------------------------------------------------------------------------------- 1 | //---------------------------------------------------------------------------- 2 | // 3 | // Description: Implementation of an Collection implementing INotifyCollectionChanged 4 | // to notify listeners of dynamic changes of the list. In addition these 5 | // notifications can be postponed or disabled. 6 | // 7 | // See spec at http://avalon/connecteddata/Specs/Collection%20Interfaces.mht 8 | // 9 | // Author: Eugene Sadovoi 10 | // 11 | // History: 12 | // 09/1/2011 : [....] - created 13 | // 14 | //--------------------------------------------------------------------------- 15 | 16 | #if !SILVERLIGHT && !NETSTANDARD1_0 && !NETSTANDARD1_4 && !NETSTANDARD1_6 && !NETSTANDARD2_0 17 | #define FRAMEWORK 18 | #endif 19 | 20 | using System.Collections.Generic; 21 | using System.Collections.Specialized; 22 | using System.ComponentModel; 23 | using System.Diagnostics; 24 | using System.Reflection; 25 | 26 | namespace System.Collections.ObjectModel 27 | { 28 | /// 29 | /// Observable collection with ability to delay or suspend CollectionChanged notifications 30 | /// 31 | /// 32 | #if FRAMEWORK 33 | [Serializable] 34 | #endif 35 | public class ObservableCollectionEx : Collection, INotifyCollectionChanged, INotifyPropertyChanged, 36 | IDisposable 37 | { 38 | //----------------------------------------------------- 39 | // Constants 40 | //----------------------------------------------------- 41 | 42 | #region Constants 43 | 44 | private const string _countString = "Count"; 45 | 46 | // This must agree with Binding.IndexerName. It is declared separately 47 | // here so as to avoid a dependency on PresentationFramework.dll. 48 | private const string _indexerName = "Item[]"; 49 | 50 | /// 51 | /// Empty delegate used to initialize event if it is empty 52 | /// 53 | #if FRAMEWORK 54 | [field: NonSerialized] 55 | #endif 56 | private static readonly NotifyCollectionChangedEventHandler _emptyDelegate = delegate { }; 57 | 58 | #endregion 59 | 60 | //----------------------------------------------------- 61 | // Private Fields 62 | //----------------------------------------------------- 63 | 64 | #region Private Fields 65 | 66 | /// 67 | /// 68 | /// 69 | #if FRAMEWORK 70 | [field: NonSerialized] 71 | #endif 72 | private ReentryMonitor _monitor = new ReentryMonitor(); 73 | 74 | /// 75 | /// Placeholder for all data related to delayed 76 | /// notifications. 77 | /// 78 | #if FRAMEWORK 79 | [field: NonSerialized] 80 | #endif 81 | private NotificationInfo _notifyInfo; 82 | 83 | /// 84 | /// Indicates if modification of container allowed during change notification. 85 | /// 86 | #if FRAMEWORK 87 | [field: NonSerialized] 88 | #endif 89 | private bool _disableReentry; 90 | 91 | #if FRAMEWORK 92 | [field: NonSerialized] 93 | #endif 94 | Action FireCountAndIndexerChanged = delegate { }; 95 | 96 | #if FRAMEWORK 97 | [field: NonSerialized] 98 | #endif 99 | Action FireIndexerChanged = delegate { }; 100 | 101 | #endregion Private Fields 102 | 103 | //----------------------------------------------------- 104 | // Protected Fields 105 | //----------------------------------------------------- 106 | 107 | #region Protected Fields 108 | 109 | /// 110 | /// PropertyChanged event . 111 | /// 112 | #if FRAMEWORK 113 | [field: NonSerialized] 114 | #endif 115 | protected virtual event PropertyChangedEventHandler PropertyChanged; 116 | 117 | /// 118 | /// Occurs when the collection changes, either by adding or removing an item. 119 | /// 120 | /// See 121 | #if FRAMEWORK 122 | [field: NonSerialized] 123 | #endif 124 | protected virtual event NotifyCollectionChangedEventHandler CollectionChanged = _emptyDelegate; 125 | 126 | #endregion Protected Fields 127 | 128 | //----------------------------------------------------- 129 | // Constructors 130 | //----------------------------------------------------- 131 | 132 | #region Constructors 133 | 134 | /// 135 | /// Initializes a new instance of ObservableCollectionEx that is empty and has default initial capacity. 136 | /// 137 | public ObservableCollectionEx() 138 | : base() 139 | { 140 | } 141 | 142 | /// 143 | /// Initializes a new instance of the ObservableCollectionEx class 144 | /// that contains elements copied from the specified list 145 | /// 146 | /// The list whose elements are copied to the new list. 147 | /// 148 | /// The elements are copied onto the ObservableCollectionEx in the 149 | /// same order they are read by the enumerator of the list. 150 | /// 151 | /// list is a null reference 152 | public ObservableCollectionEx(List list) 153 | : base((list != null) ? new List(list.Count) : list) 154 | { 155 | foreach (T item in list) 156 | { 157 | Items.Add(item); 158 | } 159 | } 160 | 161 | /// 162 | /// Initializes a new instance of the ObservableCollection class that contains 163 | /// elements copied from the specified collection and has sufficient capacity 164 | /// to accommodate the number of elements copied. 165 | /// 166 | /// The collection whose elements are copied to the new list. 167 | /// 168 | /// The elements are copied onto the ObservableCollection in the 169 | /// same order they are read by the enumerator of the collection. 170 | /// 171 | /// collection is a null reference 172 | public ObservableCollectionEx(IEnumerable collection) 173 | { 174 | if (collection == null) 175 | throw new ArgumentNullException("collection"); 176 | 177 | using (IEnumerator enumerator = collection.GetEnumerator()) 178 | { 179 | while (enumerator.MoveNext()) 180 | { 181 | Items.Add(enumerator.Current); 182 | } 183 | } 184 | } 185 | 186 | /// 187 | /// Constructor that configures the container to delay or disable notifications. 188 | /// 189 | /// Reference to an original collection whos events are being postponed 190 | /// Specifies if notifications needs to be delayed or disabled 191 | public ObservableCollectionEx(ObservableCollectionEx parent, bool notify) 192 | : base(parent.Items) 193 | { 194 | _notifyInfo = new NotificationInfo(); 195 | _notifyInfo.RootCollection = parent; 196 | 197 | if (notify) 198 | { 199 | CollectionChanged = _notifyInfo.Initialize(); 200 | } 201 | } 202 | 203 | /// 204 | /// Distructor 205 | /// 206 | ~ObservableCollectionEx() 207 | { 208 | Dispose(false); 209 | } 210 | 211 | #endregion Constructors 212 | 213 | //------------------------------------------------------ 214 | // Public Events 215 | //------------------------------------------------------ 216 | 217 | #region Public Events 218 | 219 | #region INotifyPropertyChanged implementation 220 | 221 | /// 222 | /// PropertyChanged event (per ). 223 | /// 224 | event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged 225 | { 226 | add 227 | { 228 | if (null == _notifyInfo) 229 | { 230 | if (null == PropertyChanged) 231 | { 232 | FireCountAndIndexerChanged = delegate 233 | { 234 | OnPropertyChanged(new PropertyChangedEventArgs(_countString)); 235 | OnPropertyChanged(new PropertyChangedEventArgs(_indexerName)); 236 | }; 237 | FireIndexerChanged = delegate 238 | { 239 | OnPropertyChanged(new PropertyChangedEventArgs(_indexerName)); 240 | }; 241 | } 242 | 243 | PropertyChanged += value; 244 | } 245 | else 246 | _notifyInfo.RootCollection.PropertyChanged += value; 247 | } 248 | 249 | remove 250 | { 251 | if (null == _notifyInfo) 252 | { 253 | PropertyChanged -= value; 254 | 255 | if (null == PropertyChanged) 256 | { 257 | FireCountAndIndexerChanged = delegate { }; 258 | FireIndexerChanged = delegate { }; 259 | } 260 | } 261 | else 262 | _notifyInfo.RootCollection.PropertyChanged -= value; 263 | } 264 | } 265 | 266 | #endregion INotifyPropertyChanged implementation 267 | 268 | #region INotifyCollectionChanged implementation 269 | 270 | /// 271 | /// Occurs when the collection changes, either by adding or removing an item. 272 | /// 273 | /// 274 | /// see 275 | /// 276 | event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged 277 | { 278 | add 279 | { 280 | if (null == _notifyInfo) 281 | { 282 | // Remove ballast delegate if necessary 283 | if (1 == CollectionChanged.GetInvocationList().Length) 284 | CollectionChanged -= _emptyDelegate; 285 | 286 | CollectionChanged += value; 287 | _disableReentry = CollectionChanged.GetInvocationList().Length > 1; 288 | } 289 | else 290 | _notifyInfo.RootCollection.CollectionChanged += value; 291 | } 292 | 293 | remove 294 | { 295 | if (null == _notifyInfo) 296 | { 297 | CollectionChanged -= value; 298 | 299 | if ((null == CollectionChanged) || (0 == CollectionChanged.GetInvocationList().Length)) 300 | CollectionChanged += _emptyDelegate; 301 | 302 | _disableReentry = CollectionChanged.GetInvocationList().Length > 1; 303 | } 304 | else 305 | _notifyInfo.RootCollection.CollectionChanged -= value; 306 | } 307 | } 308 | 309 | #endregion INotifyCollectionChanged implementation 310 | 311 | #endregion Public Events 312 | 313 | //------------------------------------------------------ 314 | // Public Methods 315 | //----------------------------------------------------- 316 | 317 | #region Public Methods 318 | 319 | #if !SILVERLIGHT 320 | /// 321 | /// Move item at oldIndex to newIndex. 322 | /// 323 | public void Move(int oldIndex, int newIndex) 324 | { 325 | MoveItem(oldIndex, newIndex); 326 | } 327 | #endif 328 | 329 | /// 330 | /// Returns an instance of ObservableCollectionEx 331 | /// class which manipulates original collection but suppresses notifications 332 | /// untill this instance has been released and Dispose() method has been called. 333 | /// To supress notifications it is recommended to use this instance inside 334 | /// using() statement: 335 | /// 336 | /// using (var iSuppressed = collection.DelayNotifications()) 337 | /// { 338 | /// iSuppressed.Add(x); 339 | /// iSuppressed.Add(y); 340 | /// iSuppressed.Add(z); 341 | /// } 342 | /// 343 | /// Each delayed interface is bound to only one type of operation such as Add, Remove, etc. 344 | /// Different types of operation on the same delayed interface are not allowed. In order to 345 | /// do other type of opertaion you can allocate another wrapper by calling .DelayNotifications() on 346 | /// either original object or any delayed instances. 347 | /// 348 | /// ObservableCollectionEx 349 | public ObservableCollectionEx DelayNotifications() 350 | { 351 | return new ObservableCollectionEx((null == _notifyInfo) ? this : _notifyInfo.RootCollection, true); 352 | } 353 | 354 | /// 355 | /// Returns a wrapper instance of an ObservableCollectionEx class. 356 | /// Calling methods of this instance will modify original collection 357 | /// but will not generate any notifications. 358 | /// 359 | /// ObservableCollectionEx 360 | public ObservableCollectionEx DisableNotifications() 361 | { 362 | return new ObservableCollectionEx((null == _notifyInfo) ? this : _notifyInfo.RootCollection, false); 363 | } 364 | 365 | 366 | #endregion Public Methods 367 | 368 | //----------------------------------------------------- 369 | // Protected Methods 370 | //----------------------------------------------------- 371 | 372 | #region Protected Methods 373 | 374 | /// 375 | /// Called by base class Collection<T> when the list is being cleared; 376 | /// raises a CollectionChanged event to any listeners. 377 | /// 378 | protected override void ClearItems() 379 | { 380 | CheckReentrancy(); 381 | 382 | base.ClearItems(); 383 | 384 | FireCountAndIndexerChanged(); 385 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); 386 | } 387 | 388 | /// 389 | /// Called by base class Collection<T> when an item is removed from list; 390 | /// raises a CollectionChanged event to any listeners. 391 | /// 392 | protected override void RemoveItem(int index) 393 | { 394 | CheckReentrancy(); 395 | T removedItem = this[index]; 396 | 397 | base.RemoveItem(index); 398 | 399 | FireCountAndIndexerChanged(); 400 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removedItem, index)); 401 | } 402 | 403 | /// 404 | /// Called by base class Collection<T> when an item is added to list; 405 | /// raises a CollectionChanged event to any listeners. 406 | /// 407 | protected override void InsertItem(int index, T item) 408 | { 409 | CheckReentrancy(); 410 | 411 | base.InsertItem(index, item); 412 | 413 | FireCountAndIndexerChanged(); 414 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index)); 415 | } 416 | 417 | /// 418 | /// Called by base class Collection<T> when an item is set in list; 419 | /// raises a CollectionChanged event to any listeners. 420 | /// 421 | protected override void SetItem(int index, T item) 422 | { 423 | CheckReentrancy(); 424 | 425 | T originalItem = this[index]; 426 | base.SetItem(index, item); 427 | 428 | FireIndexerChanged(); 429 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, originalItem, item, index)); 430 | } 431 | 432 | #if !SILVERLIGHT 433 | /// 434 | /// Called by base class ObservableCollection<T> when an item is to be moved within the list; 435 | /// raises a CollectionChanged event to any listeners. 436 | /// 437 | protected virtual void MoveItem(int oldIndex, int newIndex) 438 | { 439 | CheckReentrancy(); 440 | 441 | T removedItem = this[oldIndex]; 442 | base.RemoveItem(oldIndex); 443 | base.InsertItem(newIndex, removedItem); 444 | 445 | FireIndexerChanged(); 446 | OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Move, removedItem, newIndex, oldIndex)); 447 | } 448 | #endif 449 | 450 | /// 451 | /// Raises a PropertyChanged event (per ). 452 | /// 453 | protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) 454 | { 455 | PropertyChanged(this, e); 456 | } 457 | 458 | /// 459 | /// Raise CollectionChanged event to any listeners. 460 | /// Properties/methods modifying this ObservableCollection will raise 461 | /// a collection changed event through this virtual method. 462 | /// 463 | /// 464 | /// When overriding this method, either call its base implementation 465 | /// or call to guard against reentrant collection changes. 466 | /// 467 | protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) 468 | { 469 | using (BlockReentrancy()) 470 | { 471 | CollectionChanged(this, e); 472 | } 473 | } 474 | 475 | /// 476 | /// Disallow reentrant attempts to change this collection. E.g. a event handler 477 | /// of the CollectionChanged event is not allowed to make changes to this collection. 478 | /// 479 | /// 480 | /// typical usage is to wrap e.g. a OnCollectionChanged call with a using() scope: 481 | /// 482 | /// using (BlockReentrancy()) 483 | /// { 484 | /// CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, item, index)); 485 | /// } 486 | /// 487 | /// 488 | protected IDisposable BlockReentrancy() 489 | { 490 | return _monitor.Enter(); 491 | } 492 | 493 | /// Check and assert for reentrant attempts to change this collection. 494 | /// raised when changing the collection 495 | /// while another collection change is still being notified to other listeners 496 | protected void CheckReentrancy() 497 | { 498 | // we can allow changes if there's only one listener - the problem 499 | // only arises if reentrant changes make the original event args 500 | // invalid for later listeners. This keeps existing code working 501 | // (e.g. Selector.SelectedItems). 502 | if ((_disableReentry) && (_monitor.IsNotifying)) 503 | { 504 | throw new InvalidOperationException("ObservableCollectionEx Reentrancy Not Allowed"); 505 | } 506 | } 507 | 508 | #endregion Protected Methods 509 | 510 | //----------------------------------------------------- 511 | // IDisposable 512 | //------------------------------------------------------ 513 | 514 | #region IDisposable 515 | 516 | /// 517 | /// Called by the application code to fire all delayed notifications. 518 | /// 519 | public void Dispose() 520 | { 521 | Dispose(true); 522 | GC.SuppressFinalize(this); 523 | } 524 | 525 | /// 526 | /// Fires notification with all accumulated events 527 | /// 528 | /// True is called by App code. False if called from GC. 529 | protected virtual void Dispose(bool reason) 530 | { 531 | // Fire delayed notifications 532 | if (null != _notifyInfo) 533 | { 534 | if (_notifyInfo.HasEventArgs) 535 | { 536 | if (null != _notifyInfo.RootCollection.PropertyChanged) 537 | { 538 | if (_notifyInfo.IsCountChanged) 539 | _notifyInfo.RootCollection.OnPropertyChanged(new PropertyChangedEventArgs(_countString)); 540 | 541 | _notifyInfo.RootCollection.OnPropertyChanged(new PropertyChangedEventArgs(_indexerName)); 542 | } 543 | 544 | using (_notifyInfo.RootCollection.BlockReentrancy()) 545 | { 546 | NotifyCollectionChangedEventArgs args = _notifyInfo.EventArgs; 547 | 548 | foreach (Delegate delegateItem in _notifyInfo.RootCollection.CollectionChanged.GetInvocationList()) 549 | { 550 | #if FRAMEWORK 551 | try 552 | { 553 | #endif 554 | delegateItem.DynamicInvoke(new object[] { _notifyInfo.RootCollection, args }); 555 | #if FRAMEWORK 556 | } 557 | catch (TargetInvocationException e) 558 | { 559 | if ((e.InnerException is NotSupportedException) && (delegateItem.Target is ICollectionView)) 560 | { 561 | (delegateItem.Target as ICollectionView).Refresh(); 562 | } 563 | else 564 | throw; 565 | } 566 | #endif 567 | } 568 | } 569 | 570 | // Reset and reuse if necessary 571 | CollectionChanged = _notifyInfo.Initialize(); 572 | } 573 | } 574 | } 575 | 576 | #endregion 577 | 578 | //----------------------------------------------------- 579 | // Private Types 580 | //------------------------------------------------------ 581 | 582 | #region Private Types 583 | 584 | #if FRAMEWORK 585 | [Serializable()] 586 | #endif 587 | private class ReentryMonitor : IDisposable 588 | { 589 | #region Fields 590 | 591 | int _referenceCount; 592 | 593 | #endregion 594 | 595 | #region Methods 596 | 597 | public IDisposable Enter() 598 | { 599 | ++_referenceCount; 600 | 601 | return this; 602 | } 603 | 604 | public void Dispose() 605 | { 606 | --_referenceCount; 607 | } 608 | 609 | public bool IsNotifying { get { return _referenceCount != 0; } } 610 | 611 | #endregion 612 | } 613 | 614 | private class NotificationInfo 615 | { 616 | #region Fields 617 | 618 | private Nullable _action; 619 | 620 | private IList _newItems; 621 | 622 | private IList _oldItems; 623 | 624 | private int _newIndex; 625 | 626 | private int _oldIndex; 627 | 628 | #endregion 629 | 630 | #region Methods 631 | 632 | public NotifyCollectionChangedEventHandler Initialize() 633 | { 634 | _action = null; 635 | _newItems = null; 636 | _oldItems = null; 637 | 638 | return (sender, args) => 639 | { 640 | ObservableCollectionEx wrapper = sender as ObservableCollectionEx; 641 | Debug.Assert(null != wrapper, "Calling object must be ObservableCollectionEx"); 642 | Debug.Assert(null != wrapper._notifyInfo, "Calling object must be Delayed wrapper."); 643 | 644 | // Setup 645 | _action = args.Action; 646 | 647 | switch (_action) 648 | { 649 | case NotifyCollectionChangedAction.Add: 650 | _newItems = new List(); 651 | IsCountChanged = true; 652 | wrapper.CollectionChanged = (s, e) => 653 | { 654 | AssertActionType(e); 655 | foreach (T item in e.NewItems) 656 | _newItems.Add(item); 657 | }; 658 | wrapper.CollectionChanged(sender, args); 659 | break; 660 | 661 | case NotifyCollectionChangedAction.Remove: 662 | _oldItems = new List(); 663 | IsCountChanged = true; 664 | wrapper.CollectionChanged = (s, e) => 665 | { 666 | AssertActionType(e); 667 | foreach (T item in e.OldItems) 668 | _oldItems.Add(item); 669 | }; 670 | wrapper.CollectionChanged(sender, args); 671 | break; 672 | 673 | case NotifyCollectionChangedAction.Replace: 674 | _newItems = new List(); 675 | _oldItems = new List(); 676 | wrapper.CollectionChanged = (s, e) => 677 | { 678 | AssertActionType(e); 679 | foreach (T item in e.NewItems) 680 | _newItems.Add(item); 681 | 682 | foreach (T item in e.OldItems) 683 | _oldItems.Add(item); 684 | }; 685 | wrapper.CollectionChanged(sender, args); 686 | break; 687 | #if !SILVERLIGHT 688 | case NotifyCollectionChangedAction.Move: 689 | _newIndex = args.NewStartingIndex; 690 | _newItems = args.NewItems; 691 | _oldIndex = args.OldStartingIndex; 692 | _oldItems = args.OldItems; 693 | wrapper.CollectionChanged = (s, e) => 694 | { 695 | throw new InvalidOperationException( 696 | "Due to design of NotifyCollectionChangedEventArgs combination of multiple Move operations is not possible"); 697 | }; 698 | break; 699 | #endif 700 | case NotifyCollectionChangedAction.Reset: 701 | IsCountChanged = true; 702 | wrapper.CollectionChanged = (s, e) => { AssertActionType(e); }; 703 | break; 704 | } 705 | }; 706 | } 707 | 708 | #endregion 709 | 710 | #region Properties 711 | 712 | public ObservableCollectionEx RootCollection { get; set; } 713 | 714 | public bool IsCountChanged { get; private set; } 715 | 716 | public NotifyCollectionChangedEventArgs EventArgs 717 | { 718 | get 719 | { 720 | switch (_action) 721 | { 722 | case NotifyCollectionChangedAction.Reset: 723 | return new NotifyCollectionChangedEventArgs(_action.Value); 724 | 725 | case NotifyCollectionChangedAction.Add: 726 | #if !SILVERLIGHT 727 | return new NotifyCollectionChangedEventArgs(_action.Value, _newItems); 728 | #else 729 | return new NotifyCollectionChangedEventArgs(_action.Value, _newItems, _newIndex); 730 | #endif 731 | 732 | case NotifyCollectionChangedAction.Remove: 733 | #if SILVERLIGHT 734 | return new NotifyCollectionChangedEventArgs(_action.Value, _oldItems, _oldIndex); 735 | #else 736 | return new NotifyCollectionChangedEventArgs(_action.Value, _oldItems); 737 | 738 | case NotifyCollectionChangedAction.Move: 739 | return new NotifyCollectionChangedEventArgs(_action.Value, _oldItems[0], _newIndex, _oldIndex); 740 | #endif 741 | case NotifyCollectionChangedAction.Replace: 742 | #if !SILVERLIGHT 743 | return new NotifyCollectionChangedEventArgs(_action.Value, _newItems, _oldItems); 744 | #else 745 | return new NotifyCollectionChangedEventArgs(_action.Value, _newItems, _newIndex); 746 | #endif 747 | } 748 | 749 | return null; 750 | } 751 | } 752 | 753 | public bool HasEventArgs 754 | { 755 | get { return _action.HasValue; } 756 | } 757 | 758 | #endregion 759 | 760 | #region Private Helper Methods 761 | 762 | private void AssertActionType(NotifyCollectionChangedEventArgs e) 763 | { 764 | if (e.Action != _action) 765 | { 766 | throw new InvalidOperationException( 767 | string.Format("Attempting to perform {0} during {1}. Mixed actions on the same delayed interface are not allowed.", 768 | e.Action, _action)); 769 | } 770 | } 771 | 772 | #endregion 773 | } 774 | 775 | #endregion Private Types 776 | } 777 | } 778 | 779 | -------------------------------------------------------------------------------- /ObservableCollectionEx/ObservableCollectionEx.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard1.0;netstandard2.0 4 | True 5 | 6 | 7 | netstandard1.0;netstandard2.0 8 | False 9 | 10 | 11 | 12 | Copyright © Eugene Sadovoi 2011 13 | https://github.com/ENikS/ObservableCollectionEx/blob/master/LICENSE 14 | https://github.com/ENikS/ObservableCollectionEx 15 | https://github.com/ENikS/ObservableCollectionEx 16 | ObservableCollection Collection delay disable notification INotifyCollectionChanged CollectionChanged 17 | ObservableCollectionEx is designed to disable or delay notifications. The ObservanleCollectionEx is a direct replacement for ObservableCollection and could be used without any code modifications. 18 | 1.4.1.0 19 | 1.4.1.0 20 | 1.4.1 21 | Eugene Sadovoi 22 | ENikS 23 | Enhanced ObservableCollection 24 | 25 | true 26 | 27 | 28 | true 29 | 30 | 31 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build status](https://ci.appveyor.com/api/projects/status/c3skqh03vl3nmb9a?svg=true)](https://ci.appveyor.com/project/ENikS/observablecollectionex) 2 | 3 | **Introduction** 4 | 5 | MSDN describes 6 | [ObservableCollection](http://msdn.microsoft.com/en-us/library/ms668604.aspx) 7 | as a dynamic data collection which provides notifications when items get 8 | added, removed, or when the whole list is refreshed. 9 | 10 | [ObservableCollection](http://msdn.microsoft.com/en-us/library/ms668604.aspx) 11 | is fully bindable. It implements both 12 | [INotifyPropertyChanged](http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.aspx) 13 | and 14 | [INotifyCollectionChanged](http://msdn.microsoft.com/en-us/library/system.collections.specialized.inotifycollectionchanged.aspx), 15 | so whenever the collection is changed, appropriate notification events 16 | are fired off immediately and bound objects are notified and updated. 17 | 18 | This scenario works in most cases but sometimes it would be beneficial 19 | to postpone notifications until later or temporarily disable them all 20 | together. For example, until a batch update is finished. This 21 | notification delay could increase performance as well as eliminate 22 | screen flicker of updated visuals. Unfortunately, the default 23 | implementation of 24 | [ObservableCollection](http://msdn.microsoft.com/en-us/library/ms668604.aspx) 25 | does not provide this functionality. 26 | 27 | ObservableCollectionEx is designed to provide this missing 28 | functionality. ObservableCollectionEx is designed as a direct 29 | replacement for 30 | [ObservableCollection](http://msdn.microsoft.com/en-us/library/ms668604.aspx), 31 | is completely code compatible with it, and also provides a way to delay 32 | or disable notifications. 33 | 34 | **Background** 35 | 36 | In order to postpone notifications, we have to temporarily reroute them 37 | to a holding place and fire them all once delay is no longer required. 38 | At the same time, we need to continue to provide normal behavior and 39 | notifications for other consumers of the collection which do not require 40 | delay. 41 | 42 | This could be achieved if we have multiple objects acting like a shell 43 | and manipulating the same collection. One instance will contain the 44 | element’s container and be a host for all of the notification events 45 | which consumers will be subscribed to, and other instances of the shell 46 | will handle disabled and delayed events. These extra shells reference 47 | the same container but instead of firing events which consumer handlers 48 | attached to, they will call its own handlers which either collect these 49 | events or discard them. 50 | 51 | The ObservableCollection implementation is based on a Collection which 52 | implements functionality, and ObservableCollection implements 53 | notifications. The Collection class is implemented as a shell around the 54 | IList interface. It contains a reference to a container which exposes 55 | the IList interface and manipulates this container through it. One of 56 | the constructors of the Collection class takes List as a parameter and 57 | allows this list to be a container for that Collection. This creates a 58 | way to have multiple Collection instances to manipulate the same 59 | container, which perfectly serves our purpose. 60 | 61 | Unfortunately, this ability is lost in the ObservableCollection 62 | implementation. Instead of assigning IList to be a container for the 63 | instance, it creates a copy of that List and uses that copy to store 64 | elements. This limitation prevents us from inheriting from the 65 | ObservableCollection class. 66 | 67 | ObservableCollectionEx is based on a Collection class as well, and 68 | implements exactly the same methods and properties that 69 | ObservableCollection does. 70 | 71 | In addition to these members, ObservableCollectionEx exposes two methods 72 | to create disabled or delayed notification shells around the container. 73 | Methods of the shell created by DisableNotifications() produce no 74 | notifications on either INotifyPropertyChanged or 75 | INotifyCollectionChanged. 76 | 77 | Calls to the methods of the shell created by DelayNorifications() 78 | produce no notifications until this instance goes out of scope or 79 | IDisposable.Dispose() has been called on it. 80 | 81 | **How it works** 82 | 83 | Except for a few performance tricks, ObservableCollectionEx behaves 84 | exactly as the ObservableCollection class. It uses Collection to perform 85 | its operations, notifies consumers via INotifyPropertyChanged and 86 | INotifyCollectionChanged, and creates a copy of the List if you pass it 87 | to a constructor. 88 | 89 | The differences starts when the DelayNotifications() or 90 | DisableNotifications() methods are called. This method creates a new 91 | instance of the ObservableCollectionEx object and passes its constructor 92 | a reference to the original ObservableCollectionEx object, and the 93 | Boolean parameter that specifies if notifications are disabled or 94 | postponed. This new instance will have the same interface as the 95 | original, the same element’s container, but none of the consumer 96 | handlers attached to the CollectionChanged event. So when methods of 97 | this instance are called and events are fired, none of these are going 98 | anywhere but to temporary storage. 99 | 100 | Once updates are done, and either this instance goes out of scope or 101 | Dispose() has been called, all of the collected events are combined into 102 | one and fired on CollectionChanged and PropertyChanged of the original 103 | object notifying all of the consumers about changes. 104 | 105 | **Using the code** 106 | 107 | The easiest way to include this class into your project is by installing 108 | the [Nuget](http://www.nuget.org/) package available at this 109 | [link](http://www.nuget.org/List/Packages/ObservableCollectionEx). 110 | 111 | ObservableCollectionEx should be used exactly as ObservableCollection. 112 | It could be instantiated and used in place of ObservableCollection, or 113 | it could be derived from it. No special treatment is required. 114 | 115 | In order to postpone notifications, it is recommended to use the using() 116 | directive: 117 | 118 | ![http://www.codeproject.com/images/minus.gif](media/image1.gif){width="9.375e-2in" 119 | height="9.375e-2in"}Collapse | [Copy 120 | Code](http://www.codeproject.com/KB/collections/ObservableCollectionEx.aspx) 121 | 122 | ObservableCollectionEx<T> target = new 123 | ObservableCollectionEx<T>(); 124 | 125 | using (ObservableCollectionEx<T> iDelayed = 126 | target.DelayNotifications()) 127 | 128 | { 129 | 130 | iDelayed.Add(item0); 131 | 132 | iDelayed.Add(item0); 133 | 134 | iDelayed.Add(item0); 135 | 136 | } 137 | 138 | Due to the design of notification arguments, it is not possible to 139 | combine different operations together. For example, it is not possible 140 | to Add and Remove elements on the same delayed instance unless Dispose() 141 | has been called in between these calls. Calling Dispose() will fire 142 | previously collected events and reinitialize operation. 143 | 144 | ![http://www.codeproject.com/images/minus.gif](media/image1.gif){width="9.375e-2in" 145 | height="9.375e-2in"}Collapse | [Copy 146 | Code](http://www.codeproject.com/KB/collections/ObservableCollectionEx.aspx) 147 | 148 | ObservableCollectionEx<T> target = new 149 | ObservableCollectionEx<T>(); 150 | 151 | using (ObservableCollectionEx<T> iDelayed = 152 | target.DelayNotifications()) 153 | 154 | { 155 | 156 | iDelayed.Add(item0); 157 | 158 | iDelayed.Add(item0); 159 | 160 | } 161 | 162 | using (ObservableCollectionEx<T> iDelayed = 163 | target.DelayNotifications()) 164 | 165 | { 166 | 167 | iDelayed.Remove(item0); 168 | 169 | iDelayed.Remove(item0); 170 | 171 | } 172 | 173 | using (ObservableCollectionEx<T> iDelayed = 174 | target.DelayNotifications()) 175 | 176 | { 177 | 178 | iDelayed.Add(item0); 179 | 180 | iDelayed.Add(item0); 181 | 182 | iDelayed.Dispose(); 183 | 184 | iDelayed.Remove(item0); 185 | 186 | iDelayed.Remove(item0); 187 | 188 | } 189 | 190 | **Performance** 191 | 192 | In general, both ObservableCollection and ObservableCollectionEx provide 193 | comparable performance. Testing has been done using an array of 10,000 194 | unique objects. Both ObservableCollection and ObservableCollectionEx 195 | where initialized with this array to pre allocate storage so it is not 196 | affecting timing results. Application has been run about dozen times to 197 | let JIT to optimize the executable before the test results were 198 | collected. 199 | 200 | The test consisted of 10,000 Add, Replace, and Remove operations. Timing 201 | has been collected using the Stopwatch class and presented in 202 | milliseconds. 203 | 204 | ![ObservableCollectionEx.png](media/image2.png){width="6.25in" 205 | height="5.104166666666667in"} 206 | 207 | The value on the left represents the number of milliseconds it took to 208 | complete the test (Add, Replace, and Remove). The value on the bottom 209 | specifies the number of notification subscribers (handlers added to the 210 | CollectionChanged event). 211 | 212 | As you can see from the graph, the performance of the interface with 213 | disabled notifications does not depend on the subscribers. Due to 214 | several performance enhancements, ObservableCollectionEx performs 215 | slightly better than ObservableCollection regardless of the number of 216 | subscribers but it obviously loses the Disabled interface once there is 217 | more than one subscriber. 218 | 219 | The performance of ObservableCollectionEx when notifications are delayed 220 | is different compared to the results described above. Since notification 221 | is called only once, it saves some time but it requires some extra 222 | processing to unwind saved notifications. Time spent on notifications 223 | for ObservableCollection and ObservableCollectionEx are described by the 224 | following equitation: 225 | 226 | **ObservableCollection**: overhead = (**n** \* **a**) + (**n** \* **b**) 227 | 228 | **ObservableCollectionEx**: overhead = **a** + **c** + (**n** \* **b**) 229 | 230 | Where **a** is a constant overhead required to execute notification, 231 | **n** is number of changed elements, **b** is the cost of redrawing each 232 | individual element, and **c** the overhead required to execute delayed 233 | notification. 234 | 235 | ![DelayedPerformance.png](media/image3.png){width="6.25in" 236 | height="4.0625in"} 237 | 238 | The value on the left represents the time required to complete 239 | notifications. The value on the bottom specifies the number of changed 240 | elements. 241 | 242 | In these equations, values **a** and **c** are constants so the 243 | performance depends only on two elements: **b** – the time required to 244 | redraw each individual element, and **n** – the number of notified 245 | elements. As you know from calculus, **b** controls how steep the raise 246 | of the graph is. So when the time required to redraw each element (b) 247 | increases, these two lines meet sooner. It means it takes less changed 248 | elements to see performance benefits. 249 | -------------------------------------------------------------------------------- /UnitTests/ObservableCollectionExTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | using System.Collections.Specialized; 5 | using System.ComponentModel; 6 | 7 | namespace ObservableCollectionEx 8 | { 9 | 10 | 11 | /// 12 | ///This is a test class for LazyObservableCollectionTest and is intended 13 | ///to contain all LazyObservableCollectionTest Unit Tests 14 | /// 15 | [TestClass()] 16 | public class ObservableCollectionExTests 17 | { 18 | private Collection _firedCollectionEvents = new Collection(); 19 | 20 | private Collection _firedPropertyEvents = new Collection(); 21 | 22 | /// 23 | ///Gets or sets the test context which provides 24 | ///information about and functionality for the current test run. 25 | /// 26 | public TestContext TestContext { get; set; } 27 | 28 | #region Additional test attributes 29 | // 30 | //You can use the following additional attributes as you write your tests: 31 | // 32 | //Use ClassInitialize to run code before running the first test in the class 33 | [ClassInitialize()] 34 | public static void MyClassInitialize(TestContext testContext) 35 | { 36 | } 37 | 38 | //Use ClassCleanup to run code after all tests in a class have run 39 | [ClassCleanup()] 40 | public static void MyClassCleanup() 41 | { 42 | } 43 | 44 | //Use TestInitialize to run code before running each test 45 | [TestInitialize()] 46 | public void MyTestInitialize() 47 | { 48 | // TODO: TestContext.BeginTimer("TestTimer"); 49 | } 50 | 51 | //Use TestCleanup to run code after each test has run 52 | [TestCleanup()] 53 | public void MyTestCleanup() 54 | { 55 | // TODO: TestContext.EndTimer("TestTimer"); 56 | } 57 | 58 | #endregion 59 | 60 | #region Tests 61 | 62 | [TestMethod()] 63 | public void DelayedNotificationTest() 64 | { 65 | DelayedNotificationTestHelper(); 66 | } 67 | 68 | [TestMethod()] 69 | public void DisabledNotificationTest() 70 | { 71 | DisabledNotificationTestHelper(); 72 | } 73 | 74 | 75 | #endregion 76 | 77 | #region Test Helpers 78 | 79 | #region DelayedNotificationTest 80 | 81 | /// 82 | ///A test for GetDelayedNotifier 83 | /// 84 | public void DelayedNotificationTestHelper() where T : new() 85 | { 86 | ObservableCollectionEx target = CreateTargetHelper(); 87 | 88 | T item0 = new T(); 89 | T item1 = new T(); 90 | T item2 = new T(); 91 | T item3 = new T(); 92 | T item4 = new T(); 93 | 94 | 95 | // Testing Add 96 | this._firedCollectionEvents.Clear(); 97 | this._firedPropertyEvents.Clear(); 98 | using (ObservableCollectionEx iTarget = target.DelayNotifications()) 99 | { 100 | iTarget.Add(item0); 101 | iTarget.Add(item1); 102 | iTarget.Add(item2); 103 | iTarget.Add(item3); 104 | iTarget.Add(item4); 105 | 106 | Assert.IsTrue(5 == target.Count, "Count is incorrect"); 107 | Assert.IsTrue(0 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 108 | Assert.IsTrue(0 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 109 | } 110 | 111 | Assert.IsTrue(5 == target.Count, "Count is incorrect"); 112 | Assert.IsTrue(2 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 113 | Assert.IsTrue(1 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 114 | 115 | // Testing Replace 116 | this._firedCollectionEvents.Clear(); 117 | this._firedPropertyEvents.Clear(); 118 | using (ObservableCollectionEx iTarget = target.DelayNotifications()) 119 | { 120 | iTarget[1] = item0; 121 | iTarget[2] = item1; 122 | iTarget[3] = item2; 123 | iTarget[4] = item3; 124 | iTarget[0] = item4; 125 | 126 | using(ObservableCollectionEx iNested = iTarget.DelayNotifications()) 127 | { 128 | iNested.Add(item4); 129 | iNested.Add(item4); 130 | 131 | Assert.IsTrue(0 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 132 | Assert.IsTrue(0 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 133 | } 134 | 135 | Assert.IsTrue(7 == target.Count, "Count is incorrect"); 136 | Assert.IsTrue(2 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 137 | Assert.IsTrue(1 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 138 | } 139 | 140 | Assert.IsTrue(7 == target.Count, "Count is incorrect"); 141 | Assert.IsTrue(3 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 142 | Assert.IsTrue(2 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 143 | 144 | // Testing Remove 145 | this._firedCollectionEvents.Clear(); 146 | this._firedPropertyEvents.Clear(); 147 | using (ObservableCollectionEx iTarget = target.DelayNotifications()) 148 | { 149 | iTarget.Remove(item0); 150 | iTarget.Remove(item1); 151 | iTarget.Remove(item2); 152 | iTarget.Remove(item3); 153 | iTarget.Remove(item4); 154 | 155 | Assert.IsTrue(2 == target.Count, "Count is incorrect"); 156 | Assert.IsTrue(0 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 157 | Assert.IsTrue(0 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 158 | 159 | try 160 | { 161 | iTarget.Add(item0); 162 | Assert.Fail("Mixed operation is not handled"); 163 | } 164 | catch (Exception e) 165 | { 166 | Assert.IsInstanceOfType(e, typeof(InvalidOperationException)); 167 | } 168 | } 169 | 170 | Assert.IsTrue(3 == target.Count, "Count is incorrect"); 171 | Assert.IsTrue(2 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 172 | Assert.IsTrue(1 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 173 | 174 | this._firedCollectionEvents.Clear(); 175 | this._firedPropertyEvents.Clear(); 176 | using (ObservableCollectionEx iTarget = target.DelayNotifications()) 177 | { 178 | iTarget.Clear(); 179 | } 180 | 181 | Assert.IsTrue(0 == target.Count, "Count is incorrect"); 182 | Assert.IsTrue(2 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 183 | Assert.IsTrue(1 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 184 | 185 | } 186 | 187 | #endregion 188 | 189 | #region DisabledNotificationTest 190 | 191 | /// 192 | ///A test for DisableNotifications 193 | /// 194 | public void DisabledNotificationTestHelper() where T : new() 195 | { 196 | ObservableCollectionEx target = CreateTargetHelper(); 197 | 198 | T item0 = new T(); 199 | T item1 = new T(); 200 | T item2 = new T(); 201 | T item3 = new T(); 202 | T item4 = new T(); 203 | 204 | this._firedCollectionEvents.Clear(); 205 | this._firedPropertyEvents.Clear(); 206 | 207 | using (ObservableCollectionEx iTarget = target.DisableNotifications()) 208 | { 209 | iTarget.Add(item0); 210 | iTarget.Add(item1); 211 | iTarget.Add(item2); 212 | iTarget.Add(item3); 213 | iTarget.Add(item4); 214 | 215 | Assert.IsTrue(5 == target.Count, "Count is incorrect"); 216 | Assert.IsTrue(0 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 217 | Assert.IsTrue(0 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 218 | } 219 | 220 | Assert.IsTrue(5 == target.Count, "Count is incorrect"); 221 | Assert.IsTrue(0 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 222 | Assert.IsTrue(0 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 223 | } 224 | 225 | #endregion 226 | 227 | #region Other 228 | 229 | private ObservableCollectionEx CreateTargetHelper() 230 | { 231 | ObservableCollectionEx target = new ObservableCollectionEx(); 232 | (target as INotifyCollectionChanged).CollectionChanged += ((s, e) => this._firedCollectionEvents.Add(e)); 233 | (target as INotifyPropertyChanged).PropertyChanged += ((s, e) => this._firedPropertyEvents.Add(e)); 234 | 235 | return target; 236 | } 237 | 238 | #endregion 239 | 240 | #endregion 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /UnitTests/ObservableCollectionTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections; 6 | using System.Collections.Specialized; 7 | using System.ComponentModel; 8 | 9 | namespace ObservableCollectionEx 10 | { 11 | 12 | 13 | /// 14 | ///This is a test class for LazyObservableCollectionTest and is intended 15 | ///to contain all LazyObservableCollectionTest Unit Tests 16 | /// 17 | [TestClass()] 18 | public class ObservableCollectionTests 19 | { 20 | private Collection _firedCollectionEvents = new Collection(); 21 | 22 | private Collection _firedPropertyEvents = new Collection(); 23 | 24 | /// 25 | ///Gets or sets the test context which provides 26 | ///information about and functionality for the current test run. 27 | /// 28 | public TestContext TestContext { get; set; } 29 | 30 | #region Additional test attributes 31 | // 32 | //You can use the following additional attributes as you write your tests: 33 | // 34 | //Use ClassInitialize to run code before running the first test in the class 35 | [ClassInitialize()] 36 | public static void MyClassInitialize(TestContext testContext) 37 | { 38 | } 39 | 40 | //Use ClassCleanup to run code after all tests in a class have run 41 | [ClassCleanup()] 42 | public static void MyClassCleanup() 43 | { 44 | } 45 | 46 | //Use TestInitialize to run code before running each test 47 | [TestInitialize()] 48 | public void MyTestInitialize() 49 | { 50 | // TODO: TestContext.BeginTimer("TestTimer"); 51 | } 52 | 53 | //Use TestCleanup to run code after each test has run 54 | [TestCleanup()] 55 | public void MyTestCleanup() 56 | { 57 | // TODO: TestContext.EndTimer("TestTimer"); 58 | } 59 | 60 | #endregion 61 | 62 | #region Tests 63 | 64 | [TestMethod()] 65 | public void ConstructorTest() 66 | { 67 | ObservableCollectionExConstructorTestHelper(); 68 | } 69 | 70 | [TestMethod()] 71 | public void AddTest() 72 | { 73 | IListTAddTestHelper(); 74 | IListAddTestHelper(); 75 | } 76 | 77 | [TestMethod()] 78 | public void ClearTest() 79 | { 80 | ClearTestHelper(); 81 | } 82 | 83 | [TestMethod()] 84 | public void ContainsTest() 85 | { 86 | IListTContainsTestHelper(); 87 | IListContainsTestHelper(); 88 | } 89 | 90 | [TestMethod()] 91 | public void CopyToTest() 92 | { 93 | IListTCopyToTestHelper(); 94 | IListCopyToTestHelper(); 95 | } 96 | 97 | [TestMethod()] 98 | public void IndexOfTest() 99 | { 100 | IListTIndexOfTestHelper(); 101 | IListIndexOfTestHelper(); 102 | } 103 | 104 | [TestMethod()] 105 | public void InsertTest() 106 | { 107 | IListTInsertTestHelper(); 108 | IListInsertTestHelper(); 109 | } 110 | 111 | [TestMethod()] 112 | public void RemoveTest() 113 | { 114 | IListTRemoveTestHelper(); 115 | IListRemoveTestHelper(); 116 | } 117 | 118 | [TestMethod()] 119 | public void RemoveAtTest() 120 | { 121 | IListTRemoveAtTestHelper(); 122 | } 123 | 124 | [TestMethod()] 125 | public void ItemTest() 126 | { 127 | IListTItemTestHelper(); 128 | IListItemTestHelper(); 129 | } 130 | 131 | [TestMethod()] 132 | public void MoveTest() 133 | { 134 | MoveTestHelper(); 135 | } 136 | 137 | [TestMethod()] 138 | public void ReEntranceTest() 139 | { 140 | ReEntranceTestHelper(); 141 | } 142 | 143 | 144 | #endregion 145 | 146 | #region Test Helpers 147 | 148 | #region Constructor 149 | 150 | /// 151 | ///A test for ObservableCollectionEx Constructor 152 | /// 153 | public void ObservableCollectionExConstructorTestHelper() where T : new() 154 | { 155 | ObservableCollectionEx target; 156 | List list = new List(); 157 | 158 | list.Add(new T()); 159 | list.Add(new T()); 160 | list.Add(new T()); 161 | list.Add(new T()); 162 | list.Add(new T()); 163 | list.Add(new T()); 164 | 165 | target = new ObservableCollectionEx(); 166 | Assert.IsNotNull(target); 167 | Assert.IsInstanceOfType(target, typeof(ObservableCollectionEx)); 168 | 169 | target = new ObservableCollectionEx(list); 170 | Assert.IsNotNull(target); 171 | Assert.AreEqual(6, target.Count, "Incorrect List copy constructor"); 172 | 173 | target = new ObservableCollectionEx(list as IEnumerable); 174 | Assert.IsNotNull(target); 175 | Assert.AreEqual(6, target.Count, "Incorrect List copy constructor"); 176 | 177 | try 178 | { 179 | target = new ObservableCollectionEx((List)null); 180 | Assert.Fail("Null arguments are not allowed"); 181 | } 182 | catch (Exception e) 183 | { 184 | Assert.IsInstanceOfType(e, typeof(ArgumentNullException)); 185 | } 186 | 187 | try 188 | { 189 | target = new ObservableCollectionEx((IEnumerable)null); 190 | Assert.Fail("Null arguments are not allowed"); 191 | } 192 | catch (Exception e) 193 | { 194 | Assert.IsInstanceOfType(e, typeof(ArgumentNullException)); 195 | } 196 | } 197 | 198 | #endregion 199 | 200 | #region AddTest 201 | 202 | /// 203 | ///A test for IList.Add 204 | /// 205 | public void IListTAddTestHelper() where T : new() 206 | { 207 | IList target = CreateTargetHelper(); 208 | 209 | this._firedCollectionEvents.Clear(); 210 | this._firedPropertyEvents.Clear(); 211 | 212 | target.Add(new T()); 213 | target.Add(new T()); 214 | 215 | Assert.IsTrue(2 == target.Count, "Added count is incorrect"); 216 | Assert.IsTrue(4 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 217 | Assert.IsTrue(2 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 218 | } 219 | 220 | /// 221 | ///A test for IList.Add 222 | /// 223 | public void IListAddTestHelper() where T : new() 224 | { 225 | IList target = CreateTargetHelper(); 226 | 227 | this._firedCollectionEvents.Clear(); 228 | this._firedPropertyEvents.Clear(); 229 | 230 | // Add to the end of the collection 231 | Assert.IsTrue(0 == target.Add(new T()), "Incorrect index"); 232 | Assert.IsTrue(1 == target.Add(new T()), "Incorrect index"); 233 | 234 | // Verify fired events 235 | Assert.IsTrue(2 == target.Count, "Added count is incorrect"); 236 | Assert.IsTrue(4 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 237 | Assert.IsTrue(2 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 238 | 239 | try 240 | { 241 | target.Add(new object()); 242 | Assert.Fail("Out of range error not handled"); 243 | } 244 | catch (Exception e) 245 | { 246 | Assert.IsInstanceOfType(e, typeof(ArgumentException)); 247 | } 248 | 249 | } 250 | 251 | 252 | #endregion 253 | 254 | #region ClearTest 255 | 256 | /// 257 | ///A test for Clear 258 | /// 259 | public void ClearTestHelper() where T : new() 260 | { 261 | IList target = CreateTargetHelper(); 262 | 263 | target.Add(new T()); 264 | target.Add(new T()); 265 | 266 | // Clear event caches 267 | this._firedCollectionEvents.Clear(); 268 | this._firedPropertyEvents.Clear(); 269 | 270 | target.Clear(); 271 | 272 | Assert.IsTrue(0 == target.Count, "Count is incorrect"); 273 | Assert.IsTrue(2 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 274 | Assert.IsTrue(1 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 275 | } 276 | 277 | #endregion 278 | 279 | #region ContainsTest 280 | 281 | /// 282 | ///A test for Contains 283 | /// 284 | public void IListTContainsTestHelper() 285 | { 286 | IList target = CreateTargetHelper(); 287 | 288 | GenericParameterHelper value = new GenericParameterHelper(1); 289 | 290 | target.Add(new GenericParameterHelper(0)); 291 | target.Add(value); 292 | target.Add(new GenericParameterHelper(2)); 293 | 294 | Assert.IsFalse(target.Contains(new GenericParameterHelper(3)), "Incorrect condition, found wrong item"); 295 | Assert.IsTrue(target.Contains(value), "Incorrect condition, could not find item"); 296 | } 297 | 298 | /// 299 | ///A test for Contains 300 | /// 301 | public void IListContainsTestHelper() 302 | { 303 | IList target = CreateTargetHelper(); 304 | 305 | GenericParameterHelper value = new GenericParameterHelper(0); 306 | 307 | target.Add(new GenericParameterHelper(1)); 308 | target.Add(value); 309 | target.Add(new GenericParameterHelper(2)); 310 | 311 | Assert.IsFalse(target.Contains(new GenericParameterHelper(3)), "Incorrect condition, found wrong item"); 312 | Assert.IsFalse(target.Contains(new object()), "Incorrect condition, found wrong item"); 313 | Assert.IsTrue(target.Contains(value), "Incorrect condition, could not find item"); 314 | } 315 | 316 | #endregion 317 | 318 | #region CopyToTest 319 | 320 | /// 321 | ///A test for CopyTo 322 | /// 323 | public void IListTCopyToTestHelper() where T : new() 324 | { 325 | IList target = CreateTargetHelper(); 326 | 327 | T item1 = new T(); 328 | T item2 = new T(); 329 | T item3 = new T(); 330 | T item4 = new T(); 331 | 332 | target.Add(item1); 333 | target.Add(item2); 334 | target.Add(item3); 335 | target.Add(item4); 336 | 337 | T[] array = new T[6]; 338 | 339 | int arrayIndex = 1; 340 | target.CopyTo(array, arrayIndex); 341 | 342 | Assert.IsNull(array[0], "Array index 0 if off"); 343 | Assert.IsNull(array[5], "Array index 6 if off"); 344 | Assert.AreEqual(array[1], item1, "Array index 1 if off"); 345 | Assert.AreEqual(array[2], item2, "Array index 2 if off"); 346 | Assert.AreEqual(array[3], item3, "Array index 3 if off"); 347 | Assert.AreEqual(array[4], item4, "Array index 4 if off"); 348 | } 349 | 350 | /// 351 | ///A test for CopyTo 352 | /// 353 | public void IListCopyToTestHelper() where T : new() 354 | { 355 | IList target = CreateTargetHelper(); 356 | 357 | // Check for error conditions 358 | try 359 | { 360 | target.CopyTo(null, 0); ; 361 | Assert.Fail("Array is null not handled"); 362 | } 363 | catch (Exception e) 364 | { 365 | Assert.IsInstanceOfType(e, typeof(ArgumentNullException)); 366 | } 367 | 368 | T item1 = new T(); 369 | T item2 = new T(); 370 | T item3 = new T(); 371 | T item4 = new T(); 372 | 373 | target.Add(item1); 374 | target.Add(item2); 375 | target.Add(item3); 376 | target.Add(item4); 377 | 378 | object[] array = new object[6]; 379 | 380 | int arrayIndex = 1; 381 | target.CopyTo(array, arrayIndex); 382 | 383 | Assert.IsNull(array[0], "Array index 0 if off"); 384 | Assert.IsNull(array[5], "Array index 6 if off"); 385 | Assert.AreEqual(array[1], item1, "Array index 1 if off"); 386 | Assert.AreEqual(array[2], item2, "Array index 2 if off"); 387 | Assert.AreEqual(array[3], item3, "Array index 3 if off"); 388 | Assert.AreEqual(array[4], item4, "Array index 4 if off"); 389 | } 390 | 391 | #endregion 392 | 393 | #region IndexOfTest 394 | 395 | /// 396 | ///A test for IndexOf 397 | /// 398 | public void IListTIndexOfTestHelper() where T : new() 399 | { 400 | IList target = CreateTargetHelper(); 401 | 402 | T item0 = new T(); 403 | T item1 = new T(); 404 | T item2 = new T(); 405 | T item3 = new T(); 406 | T item4 = new T(); 407 | 408 | target.Add(item0); 409 | target.Add(item1); 410 | target.Add(item2); 411 | target.Add(item3); 412 | target.Add(item4); 413 | 414 | Assert.AreEqual(target[0], item0, "Array index 0 if off"); 415 | Assert.AreEqual(target[1], item1, "Array index 1 if off"); 416 | Assert.AreEqual(target[2], item2, "Array index 2 if off"); 417 | Assert.AreEqual(target[3], item3, "Array index 3 if off"); 418 | Assert.AreEqual(target[4], item4, "Array index 4 if off"); 419 | 420 | try 421 | { 422 | var temp = target[-1]; 423 | Assert.Fail("Out of range error not handled"); 424 | } 425 | catch (Exception e) 426 | { 427 | Assert.IsInstanceOfType(e, typeof(ArgumentOutOfRangeException)); 428 | } 429 | 430 | try 431 | { 432 | var temp = target[5]; 433 | Assert.Fail("Out of range error not handled"); 434 | } 435 | catch (Exception e) 436 | { 437 | Assert.IsInstanceOfType(e, typeof(ArgumentOutOfRangeException)); 438 | } 439 | } 440 | 441 | /// 442 | ///A test for IndexOf 443 | /// 444 | public void IListIndexOfTestHelper() where T : new() 445 | { 446 | IList target = CreateTargetHelper(); 447 | 448 | T item0 = new T(); 449 | T item1 = new T(); 450 | T item2 = new T(); 451 | T item3 = new T(); 452 | T item4 = new T(); 453 | 454 | target.Add(item0); 455 | target.Add(item1); 456 | target.Add(item2); 457 | target.Add(item3); 458 | target.Add(item4); 459 | 460 | Assert.AreEqual(target[0], item0, "Array index 0 if off"); 461 | Assert.AreEqual(target[1], item1, "Array index 1 if off"); 462 | Assert.AreEqual(target[2], item2, "Array index 2 if off"); 463 | Assert.AreEqual(target[3], item3, "Array index 3 if off"); 464 | Assert.AreEqual(target[4], item4, "Array index 4 if off"); 465 | 466 | try 467 | { 468 | var temp = target[-1]; 469 | Assert.Fail("Out of range error not handled"); 470 | } 471 | catch (Exception e) 472 | { 473 | Assert.IsInstanceOfType(e, typeof(ArgumentOutOfRangeException)); 474 | } 475 | 476 | try 477 | { 478 | var temp = target[5]; 479 | Assert.Fail("Out of range error not handled"); 480 | } 481 | catch (Exception e) 482 | { 483 | Assert.IsInstanceOfType(e, typeof(ArgumentOutOfRangeException)); 484 | } 485 | 486 | try 487 | { 488 | target[2] = target; 489 | Assert.Fail("Type missmatch not handled"); 490 | } 491 | catch (Exception e) 492 | { 493 | Assert.IsInstanceOfType(e, typeof(ArgumentException)); 494 | } 495 | 496 | } 497 | 498 | #endregion 499 | 500 | #region InsertTest 501 | 502 | /// 503 | ///A test for Insert 504 | /// 505 | public void IListTInsertTestHelper() where T : new() 506 | { 507 | IList target = CreateTargetHelper(); 508 | 509 | T item0 = new T(); 510 | T item1 = new T(); 511 | T item2 = new T(); 512 | T item3 = new T(); 513 | T item4 = new T(); 514 | 515 | this._firedCollectionEvents.Clear(); 516 | this._firedPropertyEvents.Clear(); 517 | 518 | target.Insert(0, item1); 519 | target.Insert(0, item0); 520 | target.Insert(2, item2); 521 | 522 | Assert.IsTrue(3 == target.Count, "Added count is incorrect"); 523 | Assert.IsTrue(6 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 524 | Assert.IsTrue(3 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 525 | 526 | Assert.AreEqual(target[0], item0, "Array index 0 if off"); 527 | Assert.AreEqual(target[1], item1, "Array index 1 if off"); 528 | Assert.AreEqual(target[2], item2, "Array index 2 if off"); 529 | } 530 | 531 | /// 532 | ///A test for Insert 533 | /// 534 | public void IListInsertTestHelper() where T : new() 535 | { 536 | IList target = CreateTargetHelper(); 537 | 538 | T item0 = new T(); 539 | T item1 = new T(); 540 | T item2 = new T(); 541 | T item3 = new T(); 542 | T item4 = new T(); 543 | 544 | this._firedCollectionEvents.Clear(); 545 | this._firedPropertyEvents.Clear(); 546 | 547 | target.Insert(0, item1); 548 | target.Insert(0, item0); 549 | target.Insert(2, item2); 550 | 551 | Assert.IsTrue(3 == target.Count, "Added count is incorrect"); 552 | Assert.IsTrue(6 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 553 | Assert.IsTrue(3 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 554 | 555 | Assert.AreEqual(target[0], item0, "Array index 0 if off"); 556 | Assert.AreEqual(target[1], item1, "Array index 1 if off"); 557 | Assert.AreEqual(target[2], item2, "Array index 2 if off"); 558 | 559 | try 560 | { 561 | target.Insert(0, new object()); 562 | Assert.Fail("Wrond type is not handled"); 563 | } 564 | catch (Exception e) 565 | { 566 | Assert.IsInstanceOfType(e, typeof(ArgumentException)); 567 | } 568 | } 569 | #endregion 570 | 571 | #region RemoveTest 572 | 573 | /// 574 | ///A test for Remove 575 | /// 576 | public void IListTRemoveTestHelper() 577 | { 578 | IList target = CreateTargetHelper(); 579 | 580 | GenericParameterHelper item0 = new GenericParameterHelper(0); 581 | GenericParameterHelper item1 = new GenericParameterHelper(1); 582 | GenericParameterHelper item2 = new GenericParameterHelper(2); 583 | GenericParameterHelper item3 = new GenericParameterHelper(3); 584 | GenericParameterHelper item4 = new GenericParameterHelper(4); 585 | 586 | target.Add(item0); 587 | target.Add(item1); 588 | target.Add(item2); 589 | 590 | this._firedCollectionEvents.Clear(); 591 | this._firedPropertyEvents.Clear(); 592 | 593 | Assert.IsTrue(target.Remove(item1)); 594 | Assert.IsTrue(target.Remove(item0)); 595 | Assert.IsFalse(target.Remove(item0)); 596 | Assert.IsTrue(target.Remove(item2)); 597 | 598 | Assert.IsTrue(0 == target.Count, "Added/Removed count is incorrect"); 599 | Assert.IsTrue(6 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 600 | Assert.IsTrue(3 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 601 | } 602 | 603 | /// 604 | ///A test for Remove 605 | /// 606 | public void IListRemoveTestHelper() 607 | { 608 | IList target = CreateTargetHelper(); 609 | 610 | GenericParameterHelper item0 = new GenericParameterHelper(0); 611 | GenericParameterHelper item1 = new GenericParameterHelper(1); 612 | GenericParameterHelper item2 = new GenericParameterHelper(2); 613 | GenericParameterHelper item3 = new GenericParameterHelper(3); 614 | GenericParameterHelper item4 = new GenericParameterHelper(4); 615 | GenericParameterHelper item5 = new GenericParameterHelper(5); 616 | 617 | target.Add(item0); 618 | target.Add(item1); 619 | target.Add(item2); 620 | 621 | this._firedCollectionEvents.Clear(); 622 | this._firedPropertyEvents.Clear(); 623 | 624 | target.Remove(item1); 625 | target.Remove(item0); 626 | target.Remove(item2); 627 | target.Remove(item5); 628 | target.Remove(new object()); 629 | 630 | Assert.IsTrue(0 == target.Count, "Count is incorrect"); 631 | Assert.IsTrue(6 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 632 | Assert.IsTrue(3 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 633 | } 634 | 635 | 636 | #endregion 637 | 638 | #region RemoveAtTest 639 | 640 | /// 641 | ///A test for RemoveAt 642 | /// 643 | public void IListTRemoveAtTestHelper() where T : new() 644 | { 645 | IList target = CreateTargetHelper(); 646 | 647 | T item0 = new T(); 648 | T item1 = new T(); 649 | T item2 = new T(); 650 | T item3 = new T(); 651 | T item4 = new T(); 652 | 653 | target.Add(item0); 654 | target.Add(item1); 655 | target.Add(item2); 656 | 657 | this._firedCollectionEvents.Clear(); 658 | this._firedPropertyEvents.Clear(); 659 | 660 | target.RemoveAt(2); 661 | target.RemoveAt(0); 662 | target.RemoveAt(0); 663 | 664 | Assert.IsTrue(0 == target.Count, "Added/Removed count is incorrect"); 665 | Assert.IsTrue(6 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 666 | Assert.IsTrue(3 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 667 | } 668 | 669 | #endregion 670 | 671 | #region ItemTest 672 | 673 | /// 674 | ///A test for Item 675 | /// 676 | public void IListTItemTestHelper() where T : new() 677 | { 678 | IList target = CreateTargetHelper(); 679 | 680 | T item1 = new T(); 681 | T item2 = new T(); 682 | T item3 = new T(); 683 | T item4 = new T(); 684 | 685 | target.Add(item1); 686 | target.Add(item2); 687 | target.Add(item3); 688 | target.Add(item4); 689 | 690 | Assert.AreEqual(target[0], item1, "Array index 1 if off"); 691 | Assert.AreEqual(target[1], item2, "Array index 2 if off"); 692 | Assert.AreEqual(target[2], item3, "Array index 3 if off"); 693 | Assert.AreEqual(target[3], item4, "Array index 4 if off"); 694 | 695 | target[2] = item1; 696 | Assert.AreEqual(target[2], item1, "Array index 2 if off"); 697 | } 698 | 699 | /// 700 | ///A test for Item 701 | /// 702 | public void IListItemTestHelper() where T : new() 703 | { 704 | IList target = CreateTargetHelper(); 705 | 706 | T item1 = new T(); 707 | T item2 = new T(); 708 | T item3 = new T(); 709 | T item4 = new T(); 710 | 711 | target.Add(item1); 712 | target.Add(item2); 713 | target.Add(item3); 714 | target.Add(item4); 715 | 716 | Assert.AreEqual(target[0], item1, "Array index 1 if off"); 717 | Assert.AreEqual(target[1], item2, "Array index 2 if off"); 718 | Assert.AreEqual(target[2], item3, "Array index 3 if off"); 719 | Assert.AreEqual(target[3], item4, "Array index 4 if off"); 720 | 721 | target[2] = item1; 722 | Assert.AreEqual(target[2], item1, "Array index 2 if off"); 723 | } 724 | 725 | #endregion 726 | 727 | #region MoveTest 728 | 729 | void MoveTestHelper() 730 | { 731 | ObservableCollectionEx target = CreateTargetHelper(); 732 | 733 | GenericParameterHelper item0 = new GenericParameterHelper(0); 734 | GenericParameterHelper item1 = new GenericParameterHelper(1); 735 | GenericParameterHelper item2 = new GenericParameterHelper(2); 736 | GenericParameterHelper item3 = new GenericParameterHelper(3); 737 | GenericParameterHelper item4 = new GenericParameterHelper(4); 738 | 739 | target.Add(item0); 740 | target.Add(item1); 741 | target.Add(item2); 742 | target.Add(item3); 743 | target.Add(item4); 744 | 745 | this._firedCollectionEvents.Clear(); 746 | this._firedPropertyEvents.Clear(); 747 | 748 | Assert.IsTrue(1 == target.IndexOf(item1)); 749 | target.Move(1, 3); 750 | Assert.IsTrue(3 == target.IndexOf(item1)); 751 | 752 | this._firedCollectionEvents.Clear(); 753 | this._firedPropertyEvents.Clear(); 754 | using (var iDisabled = target.DisableNotifications()) 755 | { 756 | iDisabled.Move(3, 1); 757 | iDisabled.Move(1, 3); 758 | iDisabled.Move(3, 1); 759 | iDisabled.Move(1, 3); 760 | iDisabled.Move(3, 1); 761 | } 762 | Assert.IsTrue(0 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 763 | Assert.IsTrue(0 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 764 | 765 | this._firedCollectionEvents.Clear(); 766 | this._firedPropertyEvents.Clear(); 767 | using (var iDelayed = target.DelayNotifications()) 768 | { 769 | iDelayed.Move(1, 3); 770 | try 771 | { 772 | iDelayed.Move(3, 1); 773 | Assert.Fail("Only one move allowed"); 774 | } 775 | catch (Exception e) 776 | { 777 | Assert.IsInstanceOfType(e, typeof(InvalidOperationException)); 778 | } 779 | } 780 | Assert.IsTrue(1 == this._firedPropertyEvents.Count, "Incorrect number of PropertyChanged notifications"); 781 | Assert.IsTrue(1 == this._firedCollectionEvents.Count, "Incorrect number of CollectionChanged notifications"); 782 | } 783 | 784 | #endregion 785 | 786 | #region ReEntranceTest 787 | 788 | public void ReEntranceTestHelper() where T : new() 789 | { 790 | ObservableCollectionEx target = new ObservableCollectionEx(); 791 | 792 | target.Add(new T()); 793 | target.Add(new T()); 794 | 795 | (target as INotifyCollectionChanged).CollectionChanged += ReEntranceHandler1TestHelper; 796 | target.Add(new T()); 797 | } 798 | 799 | public void ReEntranceHandler1TestHelper(object sender, NotifyCollectionChangedEventArgs en) 800 | { 801 | IList target = sender as IList; 802 | 803 | (target as INotifyCollectionChanged).CollectionChanged -= ReEntranceHandler1TestHelper; 804 | (sender as INotifyCollectionChanged).CollectionChanged += ((s, ex) => ReEntranceHandler2TestHelper(s, ex)); 805 | 806 | target.RemoveAt(0); 807 | } 808 | 809 | public void ReEntranceHandler2TestHelper(object sender, NotifyCollectionChangedEventArgs en) 810 | { 811 | IList target = sender as IList; 812 | 813 | (sender as INotifyCollectionChanged).CollectionChanged += ((s, ex) => ReEntranceHandler2TestHelper(s, ex)); 814 | 815 | try 816 | { 817 | target.RemoveAt(0); 818 | Assert.Fail("Modifying of collection during Change notification is not handled"); 819 | } 820 | catch (Exception e) 821 | { 822 | Assert.IsInstanceOfType(e, typeof(InvalidOperationException)); 823 | } 824 | } 825 | 826 | #endregion 827 | 828 | #region Other 829 | 830 | private ObservableCollectionEx CreateTargetHelper() 831 | { 832 | ObservableCollectionEx target = new ObservableCollectionEx(); 833 | (target as INotifyCollectionChanged).CollectionChanged += ((s, e) => this._firedCollectionEvents.Add(e)); 834 | (target as INotifyPropertyChanged).PropertyChanged += ((s, e) => this._firedPropertyEvents.Add(e)); 835 | 836 | return target; 837 | } 838 | 839 | #endregion 840 | 841 | #endregion 842 | } 843 | } 844 | -------------------------------------------------------------------------------- /UnitTests/UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net47 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 1.4.{build} 2 | image: Visual Studio 2022 3 | configuration: Release 4 | platform: Any CPU 5 | dotnet_csproj: 6 | patch: true 7 | file: '**\*.csproj' 8 | version: '1.4.1' 9 | # package_version: $(appveyor_build_version) 10 | before_build: 11 | - cmd: dotnet restore 12 | build: 13 | parallel: true 14 | verbosity: minimal 15 | artifacts: 16 | - path: '**\*.nupkg' 17 | name: ObservableCollectionEx 18 | --------------------------------------------------------------------------------