From 1a05bf9d634f5c29789e75885d79e4e03730684c Mon Sep 17 00:00:00 2001 From: Markus Ewald Date: Sat, 3 Mar 2012 10:57:08 +0000 Subject: [PATCH] Added some helper classes for INotifyPropertyChanged; added unit tests for the ObservableSet class; documented the second Count property exposed by the multi dictionary git-svn-id: file:///srv/devel/repo-conversion/nusu@262 d2e56fa2-650e-0410-a79f-9358c0239efd --- Nuclex.Support (net-4.0).csproj | 88 +++--- Nuclex.Support (xna-4.0-phone7).csproj | 92 ++++--- Nuclex.Support (xna-4.0-xbox360).csproj | 92 ++++--- .../Collections/MultiDictionary.Interfaces.cs | 22 +- Source/Collections/ObservableSet.Test.cs | 159 ++++++++++- Source/Collections/ObservableSet.cs | 22 +- Source/IntegerHelper.Test.cs | 2 +- Source/Observable.Test.cs | 170 ++++++++++++ Source/Observable.cs | 133 +++++++++ Source/ObservableHelper.Test.cs | 91 +++++++ Source/ObservableHelper.cs | 70 +++++ Source/PropertyChangedEventArgsHelper.Test.cs | 117 ++++++++ Source/PropertyChangedEventArgsHelper.cs | 256 ++++++++++++++++++ 13 files changed, 1175 insertions(+), 139 deletions(-) create mode 100644 Source/Observable.Test.cs create mode 100644 Source/Observable.cs create mode 100644 Source/ObservableHelper.Test.cs create mode 100644 Source/ObservableHelper.cs create mode 100644 Source/PropertyChangedEventArgsHelper.Test.cs create mode 100644 Source/PropertyChangedEventArgsHelper.cs diff --git a/Nuclex.Support (net-4.0).csproj b/Nuclex.Support (net-4.0).csproj index 7057908..14f0286 100644 --- a/Nuclex.Support (net-4.0).csproj +++ b/Nuclex.Support (net-4.0).csproj @@ -57,9 +57,6 @@ - - AffineThreadPool.cs - @@ -186,27 +183,6 @@ ReverseComparer.cs - - - - EnumHelper.cs - - - - PrototypeFactory.cs - - - - Semaphore.cs - - - - PartialStream.cs - - - - RingMemoryStream.cs - TransformingReadOnlyCollection.cs @@ -221,13 +197,17 @@ WeakCollection.cs - - - FloatHelper.cs + + + PartialStream.cs - - - IntegerHelper.cs + + + RingMemoryStream.cs + + + + ChainStream.cs @@ -246,10 +226,6 @@ CommandLine.cs - - - PathHelper.cs - AssemblyLoadEventArgs.cs @@ -261,6 +237,10 @@ FactoryEmployer.cs + + + PrototypeFactory.cs + InstanceEmployer.cs @@ -284,14 +264,46 @@ PluginRepository.cs + + + PropertyChangedEventArgsHelper.cs + + + + AffineThreadPool.cs + + + + EnumHelper.cs + + + + Observable.cs + + + + ObservableHelper.cs + + + + Semaphore.cs + + + + FloatHelper.cs + + + + IntegerHelper.cs + + + + PathHelper.cs + Shared.cs - - - ChainStream.cs - StringBuilderHelper.cs diff --git a/Nuclex.Support (xna-4.0-phone7).csproj b/Nuclex.Support (xna-4.0-phone7).csproj index 67cf800..923c4f4 100644 --- a/Nuclex.Support (xna-4.0-phone7).csproj +++ b/Nuclex.Support (xna-4.0-phone7).csproj @@ -22,7 +22,7 @@ full false bin\xna-4.0-phone7\Debug\ - TRACE;DEBUG;WINDOWS_PHONE;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS + TRACE;DEBUG;WINDOWS_PHONE;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS;NO_CONCURRENT_COLLECTIONS prompt 4 true @@ -34,7 +34,7 @@ pdbonly true bin\xna-4.0-phone7\Release\ - TRACE;WINDOWS_PHONE;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS + TRACE;WINDOWS_PHONE;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS;NO_CONCURRENT_COLLECTIONS prompt 4 true @@ -88,9 +88,6 @@ - - AffineThreadPool.cs - @@ -217,27 +214,6 @@ ReverseComparer.cs - - - - EnumHelper.cs - - - - PrototypeFactory.cs - - - - Semaphore.cs - - - - PartialStream.cs - - - - RingMemoryStream.cs - TransformingReadOnlyCollection.cs @@ -252,13 +228,17 @@ WeakCollection.cs - - - FloatHelper.cs + + + PartialStream.cs - - - IntegerHelper.cs + + + RingMemoryStream.cs + + + + ChainStream.cs @@ -277,10 +257,6 @@ CommandLine.cs - - - PathHelper.cs - AssemblyLoadEventArgs.cs @@ -292,6 +268,10 @@ FactoryEmployer.cs + + + PrototypeFactory.cs + InstanceEmployer.cs @@ -315,14 +295,46 @@ PluginRepository.cs + + + PropertyChangedEventArgsHelper.cs + + + + AffineThreadPool.cs + + + + EnumHelper.cs + + + + Observable.cs + + + + ObservableHelper.cs + + + + Semaphore.cs + + + + FloatHelper.cs + + + + IntegerHelper.cs + + + + PathHelper.cs + Shared.cs - - - ChainStream.cs - StringBuilderHelper.cs diff --git a/Nuclex.Support (xna-4.0-xbox360).csproj b/Nuclex.Support (xna-4.0-xbox360).csproj index aa75ddc..719a295 100644 --- a/Nuclex.Support (xna-4.0-xbox360).csproj +++ b/Nuclex.Support (xna-4.0-xbox360).csproj @@ -22,7 +22,7 @@ full false bin\xna-4.0-xbox360\Debug\ - TRACE;DEBUG;XBOX;XBOX360;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS + TRACE;DEBUG;XBOX;XBOX360;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS;NO_LINQ_EXPRESSIONS;NO_CONCURRENT_COLLECTIONS prompt 4 true @@ -34,7 +34,7 @@ pdbonly true bin\xna-4.0-xbox360\Release\ - TRACE;XBOX;XBOX360;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS + TRACE;XBOX;XBOX360;NO_CLONING;NO_SERIALIZATION;NO_XMLSCHEMA;NO_SYSTEMEVENTS;NO_EXITCONTEXT;NO_SPECIALIZED_COLLECTIONS;NO_LINQ_EXPRESSIONS;NO_CONCURRENT_COLLECTIONS prompt 4 true @@ -99,9 +99,6 @@ - - AffineThreadPool.cs - @@ -228,27 +225,6 @@ ReverseComparer.cs - - - - EnumHelper.cs - - - - PrototypeFactory.cs - - - - Semaphore.cs - - - - PartialStream.cs - - - - RingMemoryStream.cs - TransformingReadOnlyCollection.cs @@ -263,13 +239,17 @@ WeakCollection.cs - - - FloatHelper.cs + + + PartialStream.cs - - - IntegerHelper.cs + + + RingMemoryStream.cs + + + + ChainStream.cs @@ -288,10 +268,6 @@ CommandLine.cs - - - PathHelper.cs - AssemblyLoadEventArgs.cs @@ -303,6 +279,10 @@ FactoryEmployer.cs + + + PrototypeFactory.cs + InstanceEmployer.cs @@ -326,14 +306,46 @@ PluginRepository.cs + + + PropertyChangedEventArgsHelper.cs + + + + AffineThreadPool.cs + + + + EnumHelper.cs + + + + Observable.cs + + + + ObservableHelper.cs + + + + Semaphore.cs + + + + FloatHelper.cs + + + + IntegerHelper.cs + + + + PathHelper.cs + Shared.cs - - - ChainStream.cs - StringBuilderHelper.cs diff --git a/Source/Collections/MultiDictionary.Interfaces.cs b/Source/Collections/MultiDictionary.Interfaces.cs index 040ee01..845e62d 100644 --- a/Source/Collections/MultiDictionary.Interfaces.cs +++ b/Source/Collections/MultiDictionary.Interfaces.cs @@ -239,9 +239,25 @@ namespace Nuclex.Support.Collections { return this.typedDictionary.GetEnumerator(); } - /// Removes the specified key/value pair from the dictionary - /// Key/value pair that will be removed - /// True if the key/value pair was contained in the dictionary + /// Number of unique keys in the dictionary + /// + /// + /// This Count property returns a different value from the main interface of + /// the multi dictionary to stay consistent with the implemented interfaces. + /// + /// + /// If you cast a multi dictionary to a collection of collections, the count + /// property of the outer collection should, of course, be the number of inner + /// collections it contains (and not the sum of the items contained in all of + /// the inner collections). + /// + /// + /// If you use the count property in the main interface of the multi dictionary, + /// the value collections are hidden (it behaves as if the key was in the + /// dictionary multiple times), so now the sum of all key-value pairs should + /// be returned. + /// + /// int ICollection>>.Count { get { return this.typedDictionary.Count; } } diff --git a/Source/Collections/ObservableSet.Test.cs b/Source/Collections/ObservableSet.Test.cs index a2954d1..7a28f76 100644 --- a/Source/Collections/ObservableSet.Test.cs +++ b/Source/Collections/ObservableSet.Test.cs @@ -39,22 +39,46 @@ namespace Nuclex.Support.Collections { public interface IObservableCollectionSubscriber { - /// Raised when an item has been added to the collection - event EventHandler> ItemAdded; - /// Raised when an item is removed from the collection - event EventHandler> ItemRemoved; - /// Raised when an item is replaced in the collection - event EventHandler> ItemReplaced; - /// Raised when the collection is about to be cleared - event EventHandler Clearing; - /// Raised when the collection has been cleared - event EventHandler Cleared; + /// Called when an item has been added to the collection + void ItemAdded(object sender, ItemEventArgs arguments); + /// Called when an item is removed from the collection + void ItemRemoved(object sender, ItemEventArgs arguments); + /// Called when an item is replaced in the collection + void ItemReplaced(object sender, ItemReplaceEventArgs arguments); + /// Called when the collection is about to be cleared + void Clearing(object sender, EventArgs arguments); + /// Called when the collection has been cleared + void Cleared(object sender, EventArgs arguments); } #endregion // interface IObservableCollectionSubscriber - + /// Called before each test is run + [SetUp] + public void Setup() { + this.mockFactory = new MockFactory(); + this.observableSet = new ObservableSet(); + + this.subscriber = this.mockFactory.CreateMock>(); + this.observableSet.ItemAdded += this.subscriber.MockObject.ItemAdded; + this.observableSet.ItemRemoved += this.subscriber.MockObject.ItemRemoved; + this.observableSet.ItemReplaced += this.subscriber.MockObject.ItemReplaced; + this.observableSet.Clearing += this.subscriber.MockObject.Clearing; + this.observableSet.Cleared += this.subscriber.MockObject.Cleared; + } + + /// Called after each test has run + [TearDown] + public void Teardown() { + if(this.mockFactory != null) { + this.mockFactory.VerifyAllExpectationsHaveBeenMet(); + + this.subscriber = null; + this.mockFactory.Dispose(); + this.mockFactory = null; + } + } /// /// Verifies that the observable set has a default constructor @@ -64,6 +88,119 @@ namespace Nuclex.Support.Collections { Assert.IsNotNull(new ObservableSet()); } + /// + /// Verifies that adding items to the set triggers the 'ItemAdded' event + /// + [Test] + public void AddingItemsTriggersEvent() { + this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); + this.observableSet.Add(123); + } + + /// + /// Verifies that adding items to the set triggers the 'ItemAdded' event + /// + [Test] + public void AddingAlreadyContainedItemDoesNotTriggerEvent() { + this.subscriber.Expects.One.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); + this.observableSet.Add(123); + + this.subscriber.Expects.No.Method((s) => s.ItemAdded(null, null)).WithAnyArguments(); + this.observableSet.Add(123); + } + + /// + /// Verifies that excepting the set with itself empties the set + /// + [Test] + public void ExceptWithSelfEmptiesSet() { + var set = new ObservableSet(); + set.Add(1); + set.Add(2); + set.Add(3); + + Assert.AreEqual(3, set.Count); + set.ExceptWith(set); + Assert.AreEqual(0, set.Count); + } + + /// + /// Verifies that a set can be excepted with a collection + /// + [Test] + public void SetCanBeExceptedWithCollection() { + var set = new ObservableSet(); + set.Add(1); + set.Add(2); + + var collection = new List() { 1 }; + + Assert.AreEqual(2, set.Count); + set.ExceptWith(collection); + Assert.AreEqual(1, set.Count); + Assert.IsTrue(set.Contains(2)); + } + + /// + /// Verifies that a set can be intersected with a collection + /// + [Test] + public void SetCanBeIntersectedWithCollection() { + var set = new ObservableSet(); + set.Add(1); + set.Add(2); + + var collection = new List() { 1 }; + + Assert.AreEqual(2, set.Count); + set.IntersectWith(collection); + Assert.AreEqual(1, set.Count); + Assert.IsTrue(set.Contains(1)); + } + + /// + /// Verifies that it's possible to determine whether a set is a proper subset + /// or superset of another set + /// + [Test] + public void CanDetermineProperSubsetAndSuperset() { + var set1 = new ObservableSet() { 1, 2, 3 }; + var set2 = new ObservableSet() { 1, 3 }; + + Assert.IsTrue(set1.IsProperSupersetOf(set2)); + Assert.IsTrue(set2.IsProperSubsetOf(set1)); + + set2.Add(2); + + Assert.IsFalse(set1.IsProperSupersetOf(set2)); + Assert.IsFalse(set2.IsProperSubsetOf(set1)); + } + + /// + /// Verifies that it's possible to determine whether a set is a subset + /// or a superset of another set + /// + [Test] + public void CanDetermineSubsetAndSuperset() { + var set1 = new ObservableSet() { 1, 2, 3 }; + var set2 = new ObservableSet() { 1, 2, 3 }; + + Assert.IsTrue(set1.IsSupersetOf(set2)); + Assert.IsTrue(set2.IsSubsetOf(set1)); + + set2.Add(4); + + Assert.IsFalse(set1.IsSupersetOf(set2)); + Assert.IsFalse(set2.IsSubsetOf(set1)); + } + + /// Creates mock object for the test + private MockFactory mockFactory; + /// Observable set being tested + private ObservableSet observableSet; + /// Subscriber for the observable set's events + private Mock> subscriber; + } } // namespace Nuclex.Support.Collections diff --git a/Source/Collections/ObservableSet.cs b/Source/Collections/ObservableSet.cs index 76ad175..a680f15 100644 --- a/Source/Collections/ObservableSet.cs +++ b/Source/Collections/ObservableSet.cs @@ -37,9 +37,9 @@ namespace Nuclex.Support.Collections { ISet, ICollection, #if !NO_SPECIALIZED_COLLECTIONS - INotifyCollectionChanged, + INotifyCollectionChanged, #endif - IObservableCollection { + IObservableCollection { /// Raised when an item has been added to the collection public event EventHandler> ItemAdded; @@ -109,12 +109,22 @@ namespace Nuclex.Support.Collections { /// /// Other set this set will be filtered by public void IntersectWith(IEnumerable other) { - foreach(TItem item in other) { - if(!other.Contains(item)) { - this.set.Remove(item); - OnRemoved(item); + var otherSet = other as ISet; + if(otherSet == null) { + otherSet = new HashSet(other); + } + + var itemsToRemove = new List(); + foreach(TItem item in this.set) { + if(!otherSet.Contains(item)) { + itemsToRemove.Add(item); } } + + for(int index = 0; index < itemsToRemove.Count; ++index) { + this.set.Remove(itemsToRemove[index]); + OnRemoved(itemsToRemove[index]); + } } /// diff --git a/Source/IntegerHelper.Test.cs b/Source/IntegerHelper.Test.cs index 88b46b2..1d8800b 100644 --- a/Source/IntegerHelper.Test.cs +++ b/Source/IntegerHelper.Test.cs @@ -127,4 +127,4 @@ namespace Nuclex.Support { } // namespace Nuclex.Support -#endif // UNITTEST \ No newline at end of file +#endif // UNITTEST diff --git a/Source/Observable.Test.cs b/Source/Observable.Test.cs new file mode 100644 index 0000000..da86ed1 --- /dev/null +++ b/Source/Observable.Test.cs @@ -0,0 +1,170 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2012 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +#if UNITTEST + +using System; +using System.ComponentModel; + +using NUnit.Framework; +using NMock; + +namespace Nuclex.Support { + + /// Unit tests for observable class + [TestFixture] + internal class ObservableTest { + + #region class TestObservable + + /// Example class on which unit test generates change notifications + public class TestObservable : Observable { + + /// Triggers the property changed event for the specified property + /// + /// Name of the property that will be reported as changed + /// + public void FirePropertyChanged(string propertyName) { + OnPropertyChanged(propertyName); + } + + /// Fires the property changed event for the 'SomePropety' property + public void FireSomePropertyChanged() { + OnPropertyChanged(() => SomeProperty); + } + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // class TestObservable + + #region class MockedSubscriber + + /// Mocked change notification subscriber + public class MockedSubscriber { + + /// Called when the value of a property has changed + /// Object of which a property has changed + /// Contains the name of the changed property + public void PropertyChanged(object sender, PropertyChangedEventArgs arguments) { + this.wasNotified = true; + this.changedPropertyName = arguments.PropertyName; + } + + /// Whether the subscriber was notified of a property change + public bool WasNotified { + get { return this.wasNotified; } + } + + /// + /// Checks whether a change notification for the specified property was received + /// + /// Name of the property that will be checked for + /// + /// True if a change notification for the specified property was received + /// + public bool WasNotifiedOfChangeTo(string propertyName) { + if(!this.wasNotified) { + return false; + } + + if(string.IsNullOrEmpty(propertyName)) { + return string.IsNullOrEmpty(this.changedPropertyName); + } + + return (propertyName == this.changedPropertyName); + } + + /// Whether a change notification was received + private bool wasNotified; + /// Name of the property for which a change notification was received + private string changedPropertyName; + + } + + #endregion // class MockedSubscriber + + /// Called before each unit test is run + [SetUp] + public void Setup() { + this.testObservable = new TestObservable(); + this.subscriber = new MockedSubscriber(); + + this.testObservable.PropertyChanged += this.subscriber.PropertyChanged; + } + + /// + /// Verifies that the name of the changed property can be specified manually + /// when triggering the PropertyChanged event + /// + [Test] + public void PropertyNameCanBeSpecifiedManually() { + this.testObservable.FirePropertyChanged("SomeProperty"); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo("SomeProperty")); + } + +#if DEBUG // The check is conditionally performed only in debug mode + /// + /// Verifies that specifying the name of a property that doesn't exist + /// causes an ArgumentException to be thrown + /// + [Test] + public void SpecifyingInvalidPropertyNameThrowsArgumentException() { + Assert.Throws( + delegate() { this.testObservable.FirePropertyChanged("DoesntExist"); } + ); + } +#endif + + /// + /// Verifies that the observable is capable of deducing the name of the property + /// from a lambda expression + /// + [Test] + public void PropertyNameCanBeDeducedFromLambdaExpression() { + this.testObservable.FireSomePropertyChanged(); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo("SomeProperty")); + } + + /// + /// Verifies that change notifications for all properties of a type can + /// be generated + /// + [Test] + public void WildcardChangeNotificationsCanBeSent() { + this.testObservable.FirePropertyChanged(string.Empty); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo(null)); + + this.testObservable.FirePropertyChanged(null); + Assert.IsTrue(this.subscriber.WasNotifiedOfChangeTo(string.Empty)); + } + + /// Observable object being tested + private TestObservable testObservable; + /// Subscriber to the observable object being tested + private MockedSubscriber subscriber; + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/Observable.cs b/Source/Observable.cs new file mode 100644 index 0000000..d587a9f --- /dev/null +++ b/Source/Observable.cs @@ -0,0 +1,133 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2012 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +using System.ComponentModel; +using System.Diagnostics; +#if !NO_LINQ_EXPRESSIONS +using System.Linq.Expressions; +#endif +using System.Reflection; + +namespace Nuclex.Support { + + /// Base class for objects that support property change notifications +#if !NO_SERIALIZATION + [Serializable] +#endif + public abstract class Observable : INotifyPropertyChanged { + + /// Raised when a property of the instance has changed its value +#if !NO_SERIALIZATION + [field: NonSerialized] +#endif + public event PropertyChangedEventHandler PropertyChanged; + +#if !NO_LINQ_EXPRESSIONS + /// Triggers the PropertyChanged event for the specified property + /// + /// Lambda expression for the property that will be reported to have changed + /// + /// + /// + /// This notification should be fired post-change, i.e. when the property has + /// already changed its value. + /// + /// + /// + /// public int Limit { + /// get { return this.limit; } + /// set { + /// if(value != this.limit) { + /// this.limit = value; + /// OnPropertyChanged(() => Limit); + /// } + /// } + /// } + /// + /// + /// + protected void OnPropertyChanged(Expression> property) { + PropertyChangedEventHandler copy = PropertyChanged; + if(copy != null) { + copy(this, PropertyChangedEventArgsHelper.GetArgumentsFor(property)); + } + } +#endif // !NO_LINQ_EXPRESSIONS + + /// Triggers the PropertyChanged event for the specified property + /// Name of the property that has changed its value + /// + /// + /// This notification should be fired post-change, i.e. when the property has + /// already changed its value. If possible, use the other overload of this + /// method to ensure the property name will be updated during F2 refactoring. + /// + /// + /// + /// public int Limit { + /// get { return this.limit; } + /// set { + /// if(value != this.limit) { + /// this.limit = value; + /// OnPropertyChanged("Limit"); // Note: prefer lambda exp whenever possible + /// } + /// } + /// } + /// + /// + /// + protected virtual void OnPropertyChanged(string propertyName) { + enforceChangedPropertyExists(propertyName); + + PropertyChangedEventHandler copy = PropertyChanged; + if(copy != null) { + copy(this, PropertyChangedEventArgsHelper.GetArgumentsFor(propertyName)); + } + } + + /// Ensures that a property with the specified name exists in the type + /// Property name that will be checked + [Conditional("DEBUG")] + private void enforceChangedPropertyExists(string propertyName) { + + // An empty string or null indicates that all properties have changed + if(string.IsNullOrEmpty(propertyName)) { + return; + } + + // Any other string needs to match a property name + PropertyInfo property = GetType().GetProperty(propertyName); + if(property == null) { + throw new ArgumentException( + string.Format( + "Type '{0}' tried to raise a change notification for property '{1}', " + + "but no such property exists!", + GetType().Name, propertyName + ), + "propertyName" + ); + } + + } + + } + +} // namespace Nuclex.Support diff --git a/Source/ObservableHelper.Test.cs b/Source/ObservableHelper.Test.cs new file mode 100644 index 0000000..7a3cde6 --- /dev/null +++ b/Source/ObservableHelper.Test.cs @@ -0,0 +1,91 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2012 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +#if UNITTEST + +using System; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit tests for the observable helper + [TestFixture] + internal class ObservableHelperTest { + + #region class TestReferenceType + + /// Example class on which unit test generates change notifications + public class TestReferenceType { + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // class TestReferenceType + + #region struct TestValueType + + /// Example class on which unit test generates change notifications + public struct TestValueType { + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // struct TestValueType + + /// + /// Verifies that the name of a property accessed in a lambda expression + /// can be obtained. + /// + [Test] + public void CanObtainPropertyNameFromLambdaExpression() { + string propertyName = ObservableHelper.GetPropertyName( + () => SomeReferenceType.SomeProperty + ); + Assert.AreEqual("SomeProperty", propertyName); + } + + /// + /// Verifies that the name of a property assigned in a lambda expression + /// can be obtained. + /// + [Test] + public void CanObtainPropertyNameFromBoxedLambdaExpression() { + string propertyName = ObservableHelper.GetPropertyName( + () => (object)(SomeValueType.SomeProperty) + ); + Assert.AreEqual("SomeProperty", propertyName); + } + + /// Helper used to construct lambda expressions + protected static TestReferenceType SomeReferenceType { get; set; } + + /// Helper used to construct lambda expressions + protected static TestValueType SomeValueType { get; set; } + + } + +} + +#endif // UNITTEST diff --git a/Source/ObservableHelper.cs b/Source/ObservableHelper.cs new file mode 100644 index 0000000..57e6e1f --- /dev/null +++ b/Source/ObservableHelper.cs @@ -0,0 +1,70 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2012 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +#if !NO_LINQ_EXPRESSIONS +using System.Linq.Expressions; +#endif + +namespace Nuclex.Support { + + /// Contains helper methods for observing property changed + public static class ObservableHelper { + +#if !NO_LINQ_EXPRESSIONS + /// Obtains the name of a property from a lambda expression + /// + /// Lambda expression for the property whose name will be returned + /// + /// The name of the property contained in the lamba expression + /// + /// + /// This method obtains the textual name of a property specified in a lambda + /// expression. By going through a lambda expression, the property will be + /// stated as actual code, allowing F2 refactoring to correctly update any + /// references to the property when it is renamed. + /// + /// + /// + /// string propertyName = ObservableHelper.GetPropertyName(() => SomeValue); + /// Assert.AreEqual("SomeValue", propertyName); + /// + /// + /// + public static string GetPropertyName(Expression> property) { + var lambda = (LambdaExpression)property; + + MemberExpression memberExpression; + { + var unaryExpression = lambda.Body as UnaryExpression; + if(unaryExpression != null) { + memberExpression = (MemberExpression)unaryExpression.Operand; + } else { + memberExpression = (MemberExpression)lambda.Body; + } + } + + return memberExpression.Member.Name; + } +#endif // !NO_LINQ_EXPRESSIONS + + } + +} // namespace Nuclex.Support diff --git a/Source/PropertyChangedEventArgsHelper.Test.cs b/Source/PropertyChangedEventArgsHelper.Test.cs new file mode 100644 index 0000000..1b49174 --- /dev/null +++ b/Source/PropertyChangedEventArgsHelper.Test.cs @@ -0,0 +1,117 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2012 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +#if UNITTEST + +using System; +using System.ComponentModel; + +using NUnit.Framework; + +namespace Nuclex.Support { + + /// Unit tests for the property change event argument helper + [TestFixture] + internal class PropertyChangedEventArgsHelperTest { + + #region class TestViewModel + + /// Example class on which unit test generates change notifications + public class TestViewModel { + + /// Example property that will be reported to have changed + public int SomeProperty { get; set; } + + } + + #endregion // class TestViewModel + + /// + /// Verifies that a property change notification matching the property + /// passed to the AreAffecting() method is recognized + /// + [Test] + public void MatchingPropertyChangeNotificationIsRecognized() { + var arguments = new PropertyChangedEventArgs("SomeProperty"); + Assert.IsTrue(arguments.AreAffecting(() => ViewModel.SomeProperty)); + Assert.IsTrue(arguments.AreAffecting("SomeProperty")); + } + + /// + /// Ensures that a mismatching property change notification will + /// not report the property as being affected. + /// + [Test] + public void MismatchingPropertyIsReportedAsUnaffected() { + var arguments = new PropertyChangedEventArgs("AnotherProperty"); + Assert.IsFalse(arguments.AreAffecting(() => ViewModel.SomeProperty)); + Assert.IsFalse(arguments.AreAffecting("SomeProperty")); + } + + /// + /// Verifies that any specific property is reported as being affected + /// when the property change notification is a null wildcard + /// + [Test] + public void SpecificPropertyIsAffectedByNullWildcard() { + var nullArguments = new PropertyChangedEventArgs(null); + Assert.IsTrue(nullArguments.AreAffecting(() => ViewModel.SomeProperty)); + Assert.IsTrue(nullArguments.AreAffecting("SomeProperty")); + } + + /// + /// Verifies that any specific property is reported as being affected + /// when the property change notification is an empty wildcard + /// + [Test] + public void SpecificPropertyIsAffectedByEmptyWildcard() { + var emptyArguments = new PropertyChangedEventArgs(string.Empty); + Assert.IsTrue(emptyArguments.AreAffecting(() => ViewModel.SomeProperty)); + Assert.IsTrue(emptyArguments.AreAffecting("SomeProperty")); + } + + /// + /// Tests whether the helper can recognize a wildcard property change + /// notification using null as the wildcard. + /// + [Test] + public void NullWildcardIsRecognized() { + var nullArguments = new PropertyChangedEventArgs(null); + Assert.IsTrue(nullArguments.AffectAllProperties()); + } + + /// + /// Tests whether the helper can recognize a wildcard property change + /// notification using an empty string as the wildcard. + /// + [Test] + public void EmptyWildcardIsRecognized() { + var emptyArguments = new PropertyChangedEventArgs(string.Empty); + Assert.IsTrue(emptyArguments.AffectAllProperties()); + } + + /// Helper used to construct lambda expressions + protected static TestViewModel ViewModel { get; set; } + + } + +} // namespace Nuclex.Support + +#endif // UNITTEST diff --git a/Source/PropertyChangedEventArgsHelper.cs b/Source/PropertyChangedEventArgsHelper.cs new file mode 100644 index 0000000..e9105e2 --- /dev/null +++ b/Source/PropertyChangedEventArgsHelper.cs @@ -0,0 +1,256 @@ +#region CPL License +/* +Nuclex Framework +Copyright (C) 2002-2012 Nuclex Development Labs + +This library is free software; you can redistribute it and/or +modify it under the terms of the IBM Common Public License as +published by the IBM Corporation; either version 1.0 of the +License, or (at your option) any later version. + +This library is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +IBM Common Public License for more details. + +You should have received a copy of the IBM Common Public +License along with this library +*/ +#endregion + +using System; +#if NO_CONCURRENT_COLLECTIONS +using System.Collections.Generic; +#else +using System.Collections.Concurrent; +#endif +using System.ComponentModel; +#if !NO_LINQ_EXPRESSIONS +using System.Linq.Expressions; +#endif + +namespace Nuclex.Support { + + /// Contains helper methods for property change notifications + public static class PropertyChangedEventArgsHelper { + + /// + /// A property change event argument container that indicates that all + /// properties have changed their value. + /// + public static readonly PropertyChangedEventArgs Wildcard = + new PropertyChangedEventArgs(null); + + /// Initializes a new property changed argument helper + static PropertyChangedEventArgsHelper() { +#if NO_CONCURRENT_COLLECTIONS + cache = new Dictionary(); +#else + cache = new ConcurrentDictionary(); +#endif + } + +#if !NO_LINQ_EXPRESSIONS + /// + /// Provides a property change argument container for the specified property + /// + /// + /// Property for which an event argument container will be provided + /// + /// The event argument container for a property of the specified name + /// + /// + /// This method transparently caches instances of the argument containers + /// to avoid feeding the garbage collector. A typical application only has + /// in the order of tens to hundreds of different properties for which changes + /// will be reported, making a cache to avoid garbage collections viable. + /// + /// + /// + /// PropertyChangedEventArgs arguments = + /// PropertyChangedEventArgsHelper.GetArgumentsFor(() => SomeProperty); + /// + /// + /// + public static PropertyChangedEventArgs GetArgumentsFor( + Expression> property + ) { + return GetArgumentsFor(ObservableHelper.GetPropertyName(property)); + } +#endif + + /// + /// Provides a property change argument container for the specified property + /// + /// + /// Property for which an event argument container will be provided + /// + /// The event argument container for a property of the specified name + /// + /// + /// This method transparently caches instances of the argument containers + /// to avoid feeding the garbage collector. A typical application only has + /// in the order of tens to hundreds of different properties for which changes + /// will be reported, making a cache to avoid garbage collections viable. + /// + /// + /// + /// PropertyChangedEventArgs arguments = + /// PropertyChangedEventArgsHelper.GetArgumentsFor("SomeProperty"); + /// + /// + /// + public static PropertyChangedEventArgs GetArgumentsFor(string propertyName) { + if(string.IsNullOrEmpty(propertyName)) { + return Wildcard; + } + +#if NO_CONCURRENT_COLLECTIONS + lock(cache) { + // Try to reuse the change notification if an instance already exists + PropertyChangedEventArgs arguments; + if(!cache.TryGetValue(propertyName, out arguments)) { + arguments = new PropertyChangedEventArgs(propertyName); + cache.Add(propertyName, arguments); + } + + return arguments; + } +#else + // If an instance for this property already exists, just return it + PropertyChangedEventArgs arguments; + if(cache.TryGetValue(propertyName, out arguments)) { + return arguments; + } + + // No instance existed (at least a short moment ago), so create a new one + return cache.GetOrAdd(propertyName, new PropertyChangedEventArgs(propertyName)); +#endif + } + +#if !NO_LINQ_EXPRESSIONS + /// + /// Determines whether the property change affects the specified property + /// + /// + /// Type of the property that will be tested for being affected + /// + /// + /// Property change that has been reported by the observed object + /// + /// Property that will be tested for being affected + /// Whether the specified property is affected by the property change + /// + /// + /// By using this method, you can shorten the code needed to test whether + /// a property change notification affects a specific property. You also + /// avoid hardcoding the property name, which would have the adverse effect + /// of not updating the textual property names during F2 refactoring. + /// + /// + /// + /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { + /// if(arguments.AreAffecting(() => ViewModel.DisplayedValue)) { + /// updateDisplayedValueFromViewModel(); + /// } // Do not use else if here or wildcards will not work + /// if(arguments.AreAffecting(() => ViewModel.OtherValue)) { + /// updateOtherValueFromViewModel(); + /// } + /// } + /// + /// + /// + public static bool AreAffecting( + this PropertyChangedEventArgs arguments, Expression> property + ) { + if(arguments.AffectAllProperties()) { + return true; + } + + string propertyName = ObservableHelper.GetPropertyName(property); + return (arguments.PropertyName == propertyName); + } +#endif + + /// + /// Determines whether the property change affects the specified property + /// + /// + /// Type of the property that will be tested for being affected + /// + /// + /// Property change that has been reported by the observed object + /// + /// Property that will be tested for being affected + /// Whether the specified property is affected by the property change + /// + /// + /// By using this method, you can shorten the code needed to test whether + /// a property change notification affects a specific property. + /// + /// + /// + /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { + /// if(arguments.AreAffecting("DisplayedValue")) { + /// updateDisplayedValueFromViewModel(); + /// } // Do not use else if here or wildcards will not work + /// if(arguments.AreAffecting("OtherValue")) { + /// updateOtherValueFromViewModel(); + /// } + /// } + /// + /// + /// + public static bool AreAffecting( + this PropertyChangedEventArgs arguments, string propertyName + ) { + if(arguments.AffectAllProperties()) { + return true; + } + + return (arguments.PropertyName == propertyName); + } + + /// Determines whether a property change notification is a wildcard + /// + /// Property change notification that will be checked on being a wildcard + /// + /// + /// Whether the property change is a wildcard, indicating that all properties + /// have changed. + /// + /// + /// + /// As stated on MSDN: "The PropertyChanged event can indicate all properties + /// on the object have changed by using either Nothing or String.Empty as + /// the property name in the PropertyChangedEventArgs." + /// + /// + /// This method offers an expressive way of checking for that eventuality. + /// + /// + /// + /// private void propertyChanged(object sender, PropertyChangedEventArgs arguments) { + /// if(arguments.AffectAllProperties()) { + /// // Do something + /// } + /// } + /// + /// + /// + public static bool AffectAllProperties(this PropertyChangedEventArgs arguments) { + return string.IsNullOrEmpty(arguments.PropertyName); + } + + /// + /// Caches PropertyChangedEventArgs instances to avoid feeding the garbage collector + /// +#if NO_CONCURRENT_COLLECTIONS + private static readonly Dictionary cache; +#else + private static readonly ConcurrentDictionary cache; +#endif + + } + +} // namespace Nuclex.Support