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