// Copyright © 2007 John M Rusk (http://dotnet.agilekiwi.com) // // You may use this source code ("The Software") in any manner you wish, // subject to the following conditions: // // (a) The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // (b) THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using NUnit.Framework; namespace AgileKiwi.ClaytonsInterception { // This code demonstrates automatic property change notification in .net // I.e. The "property setter" implementation does not need to explicitly // name the property that is being set, and yet INotifyPropertyChanged // events are still correctly raised, with the right property name #region Sample domain object public class SampleObject: DomainObject { string _foo; int _bar; public string Foo { get { return _foo; } set { SetValue(ref _foo, value); } // Note that we call the inherited SetValue method, but we do not actually tell it // which property has changed. It figures that out for itself. } public int Bar { get { return _bar; } set { SetValue(ref _bar, value); } } } #endregion #region Test to demonstrate basic functionality [TestFixture] public class TestPropertyNamingInDomainObject { [Test] public void CanDetectPropertyChanges() { // create an object SampleObject s = new SampleObject(); // listen for property changes string lastPropertyChanged = null; s.PropertyChanged += delegate(object sender, PropertyChangedEventArgs args) { lastPropertyChanged = args.PropertyName; }; // make some changes, and check that we were notified for each s.Foo = "testing"; Assert.AreEqual("Foo", lastPropertyChanged); s.Bar = 5; Assert.AreEqual("Bar", lastPropertyChanged); } } #endregion #region Base class for domain objects (supports automatic property changed notification) public class DomainObject: INotifyPropertyChanged { PropertyChangedEventHandler _propertyChangeDelegate; protected void SetValue(ref T field, T newValue) { // Identify the property // We identify teh property by finding the offset off the property's field // from some known field. Any field (in this object) will do, so use // the propertyChangeDelegate, since that's the one field that we actually have int offset = UnsafeFieldUtils.OffsetFinder.GetOffset(ref _propertyChangeDelegate, ref field); string propertyName = PropertyNameCache.GetNameFromOffset(GetType(), offset); if(propertyName == null) return; // this happens when we automatically initialize each class. In that case, it is cleanest and safest to bail out here // set the new value field = newValue; // Do property change notification NotifyPropertyChanged(propertyName); } void NotifyPropertyChanged(string propertyName) { if (_propertyChangeDelegate != null) _propertyChangeDelegate(this, new PropertyChangedEventArgs(propertyName)); } public event PropertyChangedEventHandler PropertyChanged { add { _propertyChangeDelegate += value; } remove { _propertyChangeDelegate -= value; } } } #endregion #region Helper class for the domain object /* Note that UnsafeFieldUtils is in managed C++, and therefore is not present * in this source file BUT YOU WILL NEED IT TO RUN THIS CODE. * It's only method looks like this: * generic static int GetOffset(T% referencePoint, U% field) { pin_ptr pinnedReferencePoint = &referencePoint; Byte* rawReferencePoint = (Byte*)pinnedReferencePoint; pin_ptr pinnedField = &field; Byte* rawFieldPointer = (Byte*)pinnedField; return rawFieldPointer - rawReferencePoint; } * * To use, you will need to compile this into your own managed C++ * dll. When you do so, I recommend that you double check this code * because I do not normally work in C++, and I provide this "as is", * with no warranties of any kind. (See licence at top of this file.) */ public static class PropertyNameCache { [ThreadStatic] // we are running one cache per thread in this example. The alternative would be to use locking. static Dictionary> _cache; /// /// Given a type and a field offset, returns the name of the corresponding property. /// Performs just-in-time initialization of its cache, when required. /// /// It doesn't matter which (other) field the offsets are measured relative to, /// as long as it is always the same one for each public static string GetNameFromOffset(Type type, int offset) { if (_cache == null) _cache = new Dictionary>(); // if we don't have cached information, then initialise it now if(!_cache.ContainsKey(type)) { if (_gettingOffsets) { // Record that, for the property which is currently undergoing a "trial set", this is the offset we found // This will be hit once for each property, triggered by GetAllOffsets _offsetDetected = offset; return null; // we don't return anything valid in this case } else { // run "trial sets" on all properties to detect their offsets _cache[type] = GetAllOffsets(type); // then continue with rest of routine, to return the now-cached value that the caller wanted } } // get the cached offset return _cache[type][offset]; } [ThreadStatic] static bool _gettingOffsets; [ThreadStatic] static int _offsetDetected; /// /// Detect all the offsets for the given type by creating a dummy object, then /// setting it's properties one by one. We do this in /// mode, which means we detect and record the offsets as we hit each property. /// static Dictionary GetAllOffsets(Type type) { Dictionary result = new Dictionary(); _gettingOffsets = true; try { object dummyObject = Activator.CreateInstance(type); PropertyInfo[] allProperties = type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); foreach (PropertyInfo property in allProperties) { if (property.CanWrite) // we only process writable properties, on the basis that read-only ones don't need our offset logic at all { Type propertyType = property.PropertyType; object defaultValue = GetDefaultValue(propertyType); // set the property - this should set _offsetDetected, since _gettingOffsets is true _offsetDetected = int.MinValue; property.SetValue(dummyObject, defaultValue, null); if (_offsetDetected == int.MinValue) throw new NotSupportedException("Could not find offset for property named " + property.Name + " on " + type.FullName); // record the property name against it's corresponding offset result[_offsetDetected] = property.Name; } } } finally { _gettingOffsets = false; } return result; } /// /// Get the default value of /// /// Is just a way of calling default(T) when can only identify T by /// a Type found at runtime. Uses reflection (slow), but that's OK because we only use it in one-off initalization static object GetDefaultValue(Type requestedType) { MethodInfo info = typeof(PropertyNameCache).GetMethod("DoGetDefaultValue"); MethodInfo constructedInfo = info.MakeGenericMethod(requestedType); return constructedInfo.Invoke(null, null); } public static T DoGetDefaultValue() { return default(T); } } #endregion #region Additional Tests [TestFixture] public class TestOffsetFinder { string _foo; int _bar; bool _other; bool _other2; [Test] public void CanGetOffsets() { // If you're curious about why the actual offset values are as they are, // see AutoLayout here http://www.codeproject.com/dotnet/pointers.asp?df=100&forumid=15320&exp=0&select=781403 // But note that the actual offset values are irrelevant for our purposes, all we // need is that each field (technically each settable property) // maps to a unique offset within the containing object - which will be the case, as long // as each field is only used by one settable property Console.WriteLine(UnsafeFieldUtils.OffsetFinder.GetOffset(ref _foo, ref _foo)); Console.WriteLine(UnsafeFieldUtils.OffsetFinder.GetOffset(ref _foo, ref _bar)); Console.WriteLine(UnsafeFieldUtils.OffsetFinder.GetOffset(ref _foo, ref _other)); Console.WriteLine(UnsafeFieldUtils.OffsetFinder.GetOffset(ref _foo, ref _other2)); } } #endregion }