tags:

views:

939

answers:

2

Well BindingList and ObservableCollection work great to keep data updated and to notify when one of it's objects has changed. However, when notifying a property is about to change, I think these options are not very good.

What I have to do right now to solve this (and I warn this is not elegant AT ALL), is to implement INotifyPropertyChanging on the list's type object and then tie that to the object that holds the list PropertyChanging event, or something like the following:

// this object will be the type of the BindingList
public class SomeObject : INotifyPropertyChanging, INotifyPropertyChanged
{
 private int _intProperty = 0;
 private string _strProperty = String.Empty;

 public int IntProperty
 {
  get { return this._intProperty; }
  set
  {
   if (this._intProperty != value)
   {
    NotifyPropertyChanging("IntProperty");
    this._intProperty = value;
    NotifyPropertyChanged("IntProperty");
   }
  }
 }

 public string StrProperty
 {
  get { return this._strProperty; }
  set
  {
   if (this._strProperty != value)
   {
    NotifyPropertyChanging("StrProperty");
    this._strProperty = value;
    NotifyPropertyChanged("StrProperty");
   }
  }
 }

 #region INotifyPropertyChanging Members

 public event PropertyChangingEventHandler PropertyChanging;

 #endregion

 #region INotifyPropertyChanged Members

 public event PropertyChangedEventHandler PropertyChanged;

 #endregion

 public void NotifyPropertyChanging(string propertyName)
 {
  if (this.PropertyChanging != null)
   PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
 }

 public void NotifyPropertyChanged(string propertyName)
 {
  if (this.PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }
}

public class ObjectThatHoldsTheList : INotifyPropertyChanging, INotifyPropertyChanged
{
 public BindingList<SomeObject> BindingList { get; set; }

 public ObjectThatHoldsTheList()
 {
  this.BindingList = new BindingList<SomeObject>();
 }

 // this helps notifie Changing and Changed on Add
 private void AddItem(SomeObject someObject)
 {
  // this will tie the PropertyChanging and PropertyChanged events of SomeObject to this object
  // so it gets notifies because the BindingList does not notify PropertyCHANGING
  someObject.PropertyChanging += new PropertyChangingEventHandler(someObject_PropertyChanging);
  someObject.PropertyChanged += new PropertyChangedEventHandler(someObject_PropertyChanged);

  this.NotifyPropertyChanging("BindingList");
  this.BindingList.Add(someObject);
  this.NotifyPropertyChanged("BindingList");
 }

 // this helps notifies Changing and Changed on Delete
 private void DeleteItem(SomeObject someObject)
 {
  if (this.BindingList.IndexOf(someObject) > 0)
  {
   // this unlinks the handlers so the garbage collector can clear the objects
   someObject.PropertyChanging -= new PropertyChangingEventHandler(someObject_PropertyChanging);
   someObject.PropertyChanged -= new PropertyChangedEventHandler(someObject_PropertyChanged);
  }

  this.NotifyPropertyChanging("BindingList");
  this.BindingList.Remove(someObject);
  this.NotifyPropertyChanged("BindingList");
 }

 // this notifies an item in the list is about to change
 void someObject_PropertyChanging(object sender, PropertyChangingEventArgs e)
 {
  NotifyPropertyChanging("BindingList." + e.PropertyName);
 }

 // this notifies an item in the list has changed
 void someObject_PropertyChanged(object sender, PropertyChangedEventArgs e)
 {
  NotifyPropertyChanged("BindingList." + e.PropertyName);
 }

 #region INotifyPropertyChanging Members

 public event PropertyChangingEventHandler PropertyChanging;

 #endregion

 #region INotifyPropertyChanged Members

 public event PropertyChangedEventHandler PropertyChanged;

 #endregion

 public void NotifyPropertyChanging(string propertyName)
 {
  if (this.PropertyChanging != null)
   PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
 }

 public void NotifyPropertyChanged(string propertyName)
 {
  if (this.PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }
}

Sorry, I know this is a lot of code, which takes me back to my main point IT'S A LOT OF CODE to implement this. So my question is, does anyone know a better, shorter, more elegant solution?

Thanks for your time and suggestions.

+1  A: 

You can create a wrapper class, that implements ICustomTypeDescriptor. This wrapper will also implement necessary interfaces (such as INotifyPropertyChanging), intercept properties reads/writes on underlying object, and you will be able to call NotifyPropertyChanging() and NotifyPropertyChanged() methods implemented by a wrapper. The data consumers will work with wrapped objects same as they work with original objects.

But implementing such a wrapper will not be easy if you are not an experienced developer.

Here is the possible, yet not finished implementation of such a wrapper. It already supports INotifyPropertyChanged, and it's easy to understand how to implement INotifyPropertyChanging.

public class Wrapper : ICustomTypeDescriptor, INotifyPropertyChanged, IEditableObject, IChangeTracking
{
 private bool _isChanged;

 public object DataSource { get; set; }

 public Wrapper(object dataSource)
 {
  if (dataSource == null)
   throw new ArgumentNullException("dataSource");
  DataSource = dataSource;
 }

 #region ICustomTypeDescriptor Members

 public AttributeCollection GetAttributes()
 {
  return new AttributeCollection(
    DataSource.GetType()
         .GetCustomAttributes(true)
         .OfType<Attribute>()
         .ToArray());
 }

 public string GetClassName()
 {
  return DataSource.GetType().Name;
 }

 public string GetComponentName()
 {
  return DataSource.ToString();
 }

 public TypeConverter GetConverter()
 {
  return new TypeConverter();
 }

 public EventDescriptor GetDefaultEvent()
 {
  return null;
 }

 public PropertyDescriptor GetDefaultProperty()
 {
  return null;
 }

 public object GetEditor(Type editorBaseType)
 {
  return Activator.CreateInstance(editorBaseType);
 }

 public EventDescriptorCollection GetEvents(Attribute[] attributes)
 {
  return TypeDescriptor.GetEvents(DataSource, attributes);
 }

 public EventDescriptorCollection GetEvents()
 {
  return TypeDescriptor.GetEvents(DataSource);
 }

 public PropertyDescriptorCollection GetProperties(Attribute[] attributes)
 {
  return GetProperties();
 }

 private IEnumerable<PropertyDescriptor> _Properties;

 public IEnumerable<PropertyDescriptor> Properties
 {
  get
  {
   if (_Properties == null)
    _Properties = TypeDescriptor.GetProperties(DataSource)
    .Cast<PropertyDescriptor>()
    .Select(pd => new WrapperPropertyDescriptor(pd) as PropertyDescriptor)
    .ToList();
   return _Properties;
  }

 }

 public PropertyDescriptorCollection GetProperties()
 {
  return new PropertyDescriptorCollection(Properties.ToArray());
 }

 public object GetPropertyOwner(PropertyDescriptor pd)
 {
  return this;
 }

 #endregion ICustomTypeDescriptor

 #region ToString, Equals, GetHashCode
 public override string ToString()
 {
  return DataSource.ToString();
 }

 public override bool Equals(object obj)
 {
  var wrapper = obj as Wrapper;
  if (wrapper == null)
   return base.Equals(obj);
  else
   return DataSource.Equals(wrapper.DataSource);
 }

 public override int GetHashCode()
 {
  return DataSource.GetHashCode();
 }
 #endregion

 #region INotifyPropertyChanged
 public event PropertyChangedEventHandler PropertyChanged;

 public void OnPropertyChanged(string propertyName)
 {
  if (String.IsNullOrEmpty(propertyName))
   throw new ArgumentNullException("propertyName");

  _isChanged = true;

  if (PropertyChanged != null)
   PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
 }
 #endregion

 public IDictionary<string, object> MakeDump()
 {
  var result = new Dictionary<String, object>();
  foreach (var item in Properties)
   result[item.Name] = item.GetValue(this);

  return result;
 }

 #region IEditableObject Members

 private IDictionary<string, object> LastDump;

 public void BeginEdit()
 {
  LastDump = MakeDump();
 }

 public void CancelEdit()
 {
  if (LastDump != null)
  {
   foreach (var item in Properties)
    item.SetValue(this, LastDump[item.Name]);
   _isChanged = false;
  }
 }

 public void EndEdit()
 {
  AcceptChanges();
 }

 #endregion IEditableObject

 #region IChangeTracking
 public void AcceptChanges()
 {
  LastDump = null;
  _isChanged = false;
 }

 public bool IsChanged
 {
  get { return _isChanged;  }
 }
 #endregion IChangeTracking
}

public class WrapperPropertyDescriptor : PropertyDescriptor
{
 private Wrapper _wrapper;
 private readonly PropertyDescriptor SourceDescriptor;

 public WrapperPropertyDescriptor(PropertyDescriptor sourceDescriptor) :
  base(sourceDescriptor)
 {
  if (sourceDescriptor == null)
   throw new ArgumentNullException("sourceDescriptor");
  SourceDescriptor = sourceDescriptor;
 }


 public override Type ComponentType
 {
  get
  {
   return SourceDescriptor.ComponentType;
  }
 }

 public override bool IsReadOnly
 {
  get
  {
   return SourceDescriptor.IsReadOnly;
  }
 }

 public override Type PropertyType
 {
  get
  {
   return SourceDescriptor.PropertyType;
  }
 }

 public override object GetValue(object component)
 {
  var wrapper = component as Wrapper;
  if (wrapper == null)
   throw new ArgumentException("Unexpected component", "component");

  var value = SourceDescriptor.GetValue(wrapper.DataSource);
  if (value == null)
   return value;

  var type = value.GetType();

  // If value is user class or structure it should 
  // be wrapped before return.
  if (type.Assembly != typeof(String).Assembly)
  {
   if (typeof(IEnumerable).IsAssignableFrom(type))
    throw new NotImplementedException("Here we should construct and return wrapper for collection");

   if (_wrapper == null) 
    _wrapper = new Wrapper(value);
   else 
    _wrapper.DataSource = value; 

   return _wrapper;
  }

  return value;
 }

 public override void SetValue(object component, object value)
 {
  var wrapper = component as Wrapper;
  if (wrapper == null)
   throw new ArgumentException("Unexpected component", "component");

  var actualValue = value;

  var valueWrapper = value as Wrapper;
  if (valueWrapper != null)
   actualValue = valueWrapper.DataSource;

  // Make dump of data source's previous values
  var dump = wrapper.MakeDump();

  SourceDescriptor.SetValue(wrapper.DataSource, actualValue);

  foreach (var item in wrapper.Properties)
  {
   var itemValue = item.GetValue(wrapper);
   if (!itemValue.Equals(dump[item.Name]))
    wrapper.OnPropertyChanged(item.Name);
  }
 }

 public override void ResetValue(object component)
 {
  var wrapper = component as Wrapper;
  if (wrapper == null)
   throw new ArgumentException("Unexpected component", "component");
  SourceDescriptor.ResetValue(wrapper.DataSource);
 }

 public override bool ShouldSerializeValue(object component)
 {
  var wrapper = component as Wrapper;
  if (wrapper == null)
   throw new ArgumentException("Unexpected component", "component");
  return SourceDescriptor.ShouldSerializeValue(wrapper.DataSource);
 }

 public override bool CanResetValue(object component)
 {
  var wrapper = component as Wrapper;
  if (wrapper == null)
   throw new ArgumentException("Unexpected component", "component");
  return SourceDescriptor.CanResetValue(wrapper.DataSource);
 }
}

Again, this is not a complete version, but it can already be used in simple scenarios. The possible usage can look like this:

IList<Customer> customers = CustomerRepository.GetAllCustomers();  
IList<Wrapper> wrappedCustomers = customers.Select(c => new Wrapper(c)).ToList();
/* If you don't like LINQ in the line above you can use foreach to transform
list of Customer object to a list of Wrapper<Customer> objects */
comboBoxCustomers.DataSource = wrappedCustomers;
// or
dataGridViewCustomers.DataSource = wrappedCustomers;

So with one simple line of code you have a collection of objects that support INotifyPropertyChanged, IEditableObject, IChangeTracking interfaces!

Good luck!

nightcoder
A: 

This is a classic example for cross-cutting concern, which cries for AOP approach. Aspect Oriented Programming is a paradigm which extends classical OOP and lets you solve problems like "I want all method calls on this object to be logged".

There are several ways to do this in .NET, this is a nice list of most of them:

http://ayende.com/Blog/archive/2007/07/02/7-Approaches-for-AOP-in-.Net.aspx

One of the approaches listed is PostSharp, IL rewriter which let's you do AOP very easily. Here is an example of implementing INotifyPropertyChanged using this tool (another example comes with PostSharp, I think):

http://thetreeknowseverything.net/2009/01/21/auto-implement-inotifypropertychanged-with-aspects/

bbmud