Implementing an “observable” item within ObservableCollection
The C# ObservableCollection provides a great way for developers to handle automatic inserts and deletes and to implement a Model-View-ViewModel (MVVM) approach within C# applications; this article extends this model to the objects contained within the ObservableCollection.
I am using this model in C#, thought it might be of use. While the added expense of my ConditionalAssignment certainly may make this approach lose in high-volume situations, I think it makes the end-developer’s job extremely easy.
Start with Class #1: the observable item base class. The secret sauce is that we extend the default PropertyChanged
event to store the original value as an object:
using System; using System.ComponentModel; namespace ArchG2.CodeLibrary.Sl.DAL { /// <summary> /// An observable item automatically supports property notification /// events. /// </summary> public class ArchG2ObservableItem : INotifyPropertyChanged { #region Events /// <summary> /// A specific extension to property changed event; allows the /// original value to be stored /// </summary> public class PropertyChangedEventExtArgs : PropertyChangedEventArgs { /// <summary> /// Allow the extension args to be created just like the base class /// </summary> /// <param name="propertyName">Property being changed</param> public PropertyChangedEventExtArgs(string propertyName) : base(propertyName) {} /// <summary> /// Allow the extension args to be created just like the base class /// </summary> /// <param name="propertyName">Property being changed</param> /// <param name="origValue">Extension: Allow original value to be set</param> public PropertyChangedEventExtArgs(string propertyName, object origValue) : base(propertyName) { OrigValue = origValue; } /// <summary> /// On a property changed event, provides the original value /// </summary> public object OrigValue { get; set; } /// <summary> /// Can be invoked by a caller to determine if the given value /// has changed from the <see cref="OrigValue"/>. /// </summary> /// <remarks> /// This method accounts for *simple* and *non-array* property changes /// only. This method checks the passed object for the property /// (<see cref="PropertyChangedEventArgs.PropertyName"/> property of this class) and performs the /// following logic: /// <list type="bullet"> /// <item><c>sender</c> must not be NULL</item> /// <item><see cref="PropertyChangedEventArgs.PropertyName"/> must reference a valid, non-array property</item> /// <item>If both the <see cref="OrigValue"/> and the new value are NULL, return FALSE</item> /// <item>If both the <see cref="OrigValue"/> or the new value are NULL, return TRUE</item> /// <item>If both values are non-NULL, first perform a simple compare ("==" operation). If they are the same, return FALSE</item> /// <item>Finally, perform a logical equality by invoking the <c>Equals</c> method. Return the *inverse* of that result.</item> /// </list> /// As can be seen, this analysis is not exhaustive by any means and does /// not attempt to do anything fancy with the values. Consider a simple /// <see cref="Object"/> property stored in a class where the previous value /// was a <see cref="String"/> and the current value is a <see cref="DateTime"/>. /// In this case, the <c>IsValueChanged</c> function always returns FALSE /// because no built-in conversion exists between these object types. /// Alternatively, consider a class that contains a user-defined type where /// the user-defined type does not override the <c>Equals</c> method. In that /// case, our <c>IsValueChanged</c> function will also return FALSE unless the /// two objects are truly identical (that is, references to the same instance). /// The moral is not to depend on this <c>IsValueChanged</c> function to perform /// any magic, and to use it only when comparing simple types or types that /// can be compared logically (both types have overridden the /// <c>Equals</c> method). /// </remarks> /// <param name="sender">The object on which a property changed</param> /// <returns>TRUE if the value has changed (persistent data sources /// should be updated)</returns> public bool IsValueChanged(object sender) { // edge condition if (sender == null) throw new ArgumentNullException("Must pass valid sender object"); // locate the object in question PropertyInfo pi = sender.GetType().GetProperty(PropertyName); if (pi == null) throw new ArgumentException("Unable to locate property " + PropertyName); // get the value (scalar only, no arrays) object newValue = pi.GetValue(sender, null); // if both are NULL, return FALSE (unchanged) if (OrigValue == null && newValue == null) return false; // if either are NULL, return TRUE (changed) if (OrigValue == null || newValue == null) return true; // check physical equality (for unchanged) if (OrigValue == newValue) return false; // check logical equality (return inverse) return !OrigValue.Equals(newValue); } } /// <summary> /// The PropertyChanged event is added automatically when we /// implement INotifyPropertyChanged. /// </summary> public event PropertyChangedEventHandler PropertyChanged; #endregion #region Property change support /// <summary> /// Given a property changed event argument object, return the original /// value associated with a change. /// </summary> /// <typeparam name="T">Allows the result to be typesafe</typeparam> /// <param name="e">The event arguments from the property change</param> /// <returns>The typecast original value if it exists or NULL</returns> public static T OrigValue<T>(PropertyChangedEventArgs e) { var extArgs = e as PropertyChangedEventExtArgs; if (extArgs != null) { return (T)extArgs.OrigValue; } //if // we should probably throw an exception here... return default(T); } /// <summary> /// Fires the property changed event for the named property /// </summary> /// <param name="propertyName">The property being changed</param> /// <param name="origValue">Original value</param> protected void OnPropertyChanged(string propertyName, object origValue) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventExtArgs(propertyName, origValue)); } //if } //OnPropertyChanged /// <summary> /// Perform a conditional assignment from a new value to /// an existing value for a given property. Note that the /// property name itself is derived automatically. /// Upon successful update of the old value, a PropertyChanged /// event is raised. /// </summary> /// <typeparam name="T">The type of assignment being made</typeparam> /// <param name="propertyName">The explicit property name (must be passed, cannot be inferred due to Linq issues)</param> /// <param name="oldValue">The old value (passed by reference); will be updated as necessary</param> /// <param name="newValue">The new value; only assigned to old value if different</param> protected void AssignConditional<T>(string propertyName, ref T oldValue, T newValue) { // anything to do? if (oldValue == null && newValue == null) return; bool needsAssignment = false; if (oldValue == null || newValue == null) { needsAssignment = true; } else { needsAssignment = !oldValue.Equals(newValue); } //if if (!needsAssignment) return; // set the value now that we know everything is a-ok T origValue = oldValue; oldValue = newValue; // do we have anything else to do? if (PropertyChanged == null) return; /* NOTE: This was the original code; was slow and did not work as desired. // find the property name string propertyName = null; try { System.Diagnostics.StackTrace stack = new System.Diagnostics.StackTrace(); System.Diagnostics.StackFrame frame = stack.GetFrame(1); propertyName = frame.GetMethod().Name; } catch { } //try */ if (string.IsNullOrEmpty(propertyName)) { throw new ArchG2Exception( string.Format("Unable to locate property name for observable item: {0}", GetType().Name) ); } //if /* if (!propertyName.StartsWith("set_")) { throw new ArchG2Exception( string.Format("Property name for {0}.{1} not recognized as valid setter", GetType().Name, propertyName) ); } //if // extract it out propertyName = propertyName.Substring(4); */ // fire the event OnPropertyChanged(propertyName, origValue); } #endregion } }
Now we move to Class 2: The observable collection base class. The added value here is that, as each element (or range of elements) is added to the Collection, that element is checked to see if it implements the INotifyPropertyChanged
. If so, the list will handle property changes and bubble them up to the list owner:
using System; using System.ComponentModel; using System.Collections.Generic; using System.Collections.ObjectModel; namespace ArchG2.CodeLibrary.Sl.DAL { /// <summary> /// An observable collection with AddRange support. All of our DAL functions /// return collections derived from this class, which can (in theory) /// be applied to any Telerik control and updates can be detected and /// automatically applied back to the host. /// </summary> /// <typeparam name="T">Type of the collection</typeparam> public class ArchG2ObservableCollection<T>: ObservableCollection<T> { #region Events /// <summary> /// This event will get raised whenever an item is changed and that /// item supports the INotifyPropertyChanged interface. /// </summary> public event PropertyChangedEventHandler ItemPropertyChanged; #endregion #region Overrides for property change management /// <summary> /// Handle property change management automatically by attaching to /// items being added. Only if the type being managed by this /// collection supports INotifyPropertyChanged /// </summary> /// <param name="index">The index to insert at</param> /// <param name="item">The item to insert into the collection</param> protected override void InsertItem(int index, T item) { // base class base.InsertItem(index, item); INotifyPropertyChanged iface = item as INotifyPropertyChanged; if (iface != null) { iface.PropertyChanged -= Item_PropertyChanged; iface.PropertyChanged += new PropertyChangedEventHandler(Item_PropertyChanged); } //if } /// <summary> /// If the managed items for this collection support INotifyPropertyChanged, /// automatically remove the added PropertyChanged event handler from the /// item. /// </summary> /// <param name="index"></param> protected override void RemoveItem(int index) { INotifyPropertyChanged iface = this[index] as INotifyPropertyChanged; if (iface != null) { iface.PropertyChanged -= Item_PropertyChanged; } //if // base class base.RemoveItem(index); } /// <summary> /// Internal handler for bubbling up property changed events /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void Item_PropertyChanged(object sender, PropertyChangedEventArgs e) { // notify the property changed first if (ItemPropertyChanged != null) { ItemPropertyChanged(sender, e); } //if } #endregion #region Extensions /// <summary> /// Extension: allow shallow copy of list /// </summary> /// <param name="list">List to work from</param> public void AddRange(IList<T> list) { foreach (var item in list) { Add(item); } //foreach } //AddRange /// <summary> /// Extension: allow loop processing on list (similar to <c>All</c> except /// that the action code need not return a bool result). /// </summary> /// <param name="action">Action to invoke</param> public void ForEach(Action<T> action) { foreach (var obj in this) { action(obj); } //foreach } //ForEach /// <summary> /// Given a search function, find and remove all matching items from the list. /// This method first iterates all items in the list to find matches (by /// index) and then uses <c>RemoveAt</c> to remove each match. /// Two-loop approach to avoid modifying the list during the first iteration. /// Not thread-safe. /// </summary> /// <param name="predicate">The search function to use</param> public void RemoveMatching(Func<T, bool> predicate) { var matchingItems = new List<int>(); int idx = 0; for (idx = 0; idx < Count; ++idx) { if (predicate(this[idx])) { matchingItems.Add(idx); } } //for for (idx = matchingItems.Count; idx > 0; --idx) { RemoveAt(idx - 1); } //for } //RemoveMatching #endregion } }
Class 3: Leverage the above base classes to create an “observable collection item” that tracks one of the object elements (ApcBinID
) and fires the ItemPropertyChanged
event as part of a custom class. To track additional column-level changes, the designer simply uses the AssignConditional
pattern shown below:
/// Wrapper class for set of total scores for all APCs. /// The "ApcBinID" is tracked on any change to any item within the collection. public class swm_sp_GetAllScores : ArchG2ObservableItem { public int SystemID { get; set; } public string SystemName { get; set; } public string SystemDesc { get; set; } public double Score { get; set; } public int MappingStatus { get; set; } /// Users can be notified of changes to this item private int _apcBinID; public int ApcBinID { get { return _apcBinID; } set { AssignConditional(ref _apcBinID, value); } } }
Finally, we come to Class 4: a specific typesafe observable collection for the above “observable item”. Note that there is zero code for the developer; any changes to the item are propagated via the observable collection as an ItemPropertyChanged event.
/// Typesafe list of total scores for all APCs public class swm_sp_GetAllScoresList : ArchG2ObservableCollection<swm_sp_GetAllScores> { } ... and let's use this class somewhere... public class SomeUiObject { swm_sp_GetAllScoresList _list = new swm_sp_GetAllScoresList(); public void Init() { _list.ItemPropertyChanged += list_ItemPropertyChanged ; } //Init /// and the event handler, which gets fired when an Item changes in the ObservableCollection void list_ItemPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { // the "sender" in this case is the same as the object variable containing the property var theMessage = "Property Change Notification: PropertyName is " + e.PropertyName; // do something with this message... } //list_ItemPropertyChanged }
The above use case is quite simplistic, yes? It appears to be a lot of work simply to get a notification that a particular property changed in a particular list. How about a little more useful use case where we take the property name and apply it to a different object – in our case a Data Access Layer (DAL) object? Then we can use that information to force a database update specific to the field being modified:
public class SomeUiObject { swm_sp_GetAllScoresList _list = new swm_sp_GetAllScoresList(); public void Init() { _list.ItemPropertyChanged += list_ItemPropertyChanged ; } //Init /// and the event handler, which gets fired when an Item changes in the ObservableCollection void list_ItemPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e) { // ASSUME: "dataItem" is a different object from a database query (perhaps LINQ) var dataItem = [some database query returning a single tuple] // ASSUME: "myDAL" is a data access layer object that supports an update var myDAL = [get the data access layer object] // get our extension to the PropertyChangedEventArgs. This extension has the original value. extE = e as PropertyChangedEventExtArgs ; if (extE == null) { throw new ArchG2MessageOnlyException( "The property event args for " + e.PropertyName + " are not our extended object." ); } //if // get property name, and extract the value on and locate on the dataItem var normalizedPropertyName = e.PropertyName.ToLowerInvariant(); Type type = dataItem.GetType(); var pi = type.GetProperty(e.PropertyName); if (pi == null) { throw new ArchG2MessageOnlyException( "The property " + e.PropertyName + " cannot be found on the database." ); }; var newValue = pi.GetValue(dataItem, null); // now we can update the data item explicitly with the old (orig) value and the new value myDAL.Update_swm_sp_GetAllScores( dataItem, extE.OrigValue, newValue ) ; } //list_ItemPropertyChanged }
Leave a Reply