For a WPF application I needed a two-way binding between a DataGrid and a collection of business entities. First I used a simple ObservableCollection, and although the binding itself works fine, there is no fully built-in functionality to keep tracking information of added, changed or removed entities. So I decided to write a generic ObservableTrackableCollection. In my project I used the MVVM pattern: the ViewModel is bound to the view and uses the model entities.
Important to know first is that I wanted the model to be responsible for its validation. Here’s a simplified version of the SupplierModel:
1: public class SupplierModel
2: {
3: public int Id { get; set; }
4: public string Name { get; set; }
5: public string Description{ get; set; }
6: public string Address { get; set; }
7: public string Number { get; set; }
8: public int PaymentTerms { get; set; }
9: public string ValidationErrors
10: {
11: get
12: {
13: StringBuilder message = new StringBuilder();
14: message.Append(this["Name"]);
15: message.Append(this["PaymentTerms"]);
16: return message.ToString();
17: }
18: }
19: public string this[string property]
20: {
21: get
22: {
23: string message = null;
24: switch (property)
25: {
26: case "Name":
27: if (string.IsNullOrEmpty(Name))
28: {
29: message = "Name is mandatory!";
30: }
31: break;
32: case "PaymentTerms":
33: if (PaymentTerms > 365)
34: {
35: message = "Payment terms shoud be smaller than or equal to 365!";
36: }
37: break;
38: default:
39: break;
40: }
41: return message;
42: }
43: }
44: }
The view model is bound to the DataGrid, so it implements INotifyPropertyChanged so that property changes can be detected by the binding mechanism, and also IDataErrorInfo so that validation errors can be detected. The view model exposes the model by encapsulating it. Validation is done by delegating the validation task to the SupplierModel. Here’s the SupplierViewModel:
1: public class SupplierViewModel : INotifyPropertyChanged, IDataErrorInfo
2: {
3: public event PropertyChangedEventHandler PropertyChanged;
4:
5: private SupplierModel supplier = null;
6:
7: public SupplierViewModel()
8: {
9: this.supplier = new SupplierModel();
10: }
11: public SupplierViewModel(SupplierModel supplier)
12: {
13: this.supplier = supplier;
14: }
15: protected void RaisePropertyChanged<T>(Expression<Func<T>> property)
16: {
17: PropertyChangedEventHandler handler = this.PropertyChanged;
18:
19: if (handler != null)
20: handler(this, property.CreateChangeEventArgs());
21: }
22: public int Id
23: {
24: get { return supplier.Id; }
25: set { supplier.Id = value; RaisePropertyChanged(() => Id); }
26: }
27: public string Name
28: {
29: get { return supplier.Name; }
30: set { supplier.Name = value; RaisePropertyChanged(() => Name); }
31: }
32: public string Description
33: {
34: get { return supplier.Description; }
35: set { supplier.Description= value; RaisePropertyChanged(() => Description); }
36: }
37: public string Address
38: {
39: get { return supplier.Address; }
40: set { supplier.Address = value; RaisePropertyChanged(() => Address); }
41: }
42: public string Number
43: {
44: get { return supplier.Number; }
45: set { supplier.Number = value; RaisePropertyChanged(() => Number); }
46: }
47: public int PaymentTerms
48: {
49: get { return supplier.PaymentTerms; }
50: set { supplier.PaymentTerms = value; RaisePropertyChanged(() => PaymentTerms); }
51: }
52: #region IDataErrorInfo Members
53: /// <summary>
54: /// A summary of the errors for the supplier object. The supplier model takes care of the
55: /// actual validation, and its results is used by the view model to update the validation state.
56: /// </summary>
57: public string Error
58: {
59: get
60: {
61: return supplier.ValidationErrors;
62: }
63: }
64: public string this[string property]
65: {
66: get
67: {
68: return supplier[property];
69: }
70: }
71: #endregion
72: }
73:
As you see, every time a property is set, RaisePropertyChanged is used to indicate a change. It uses the following extension method CreateChangeEventArgs:
1: public static class PropertyExtensions
2: {
3: public static PropertyChangedEventArgs CreateChangeEventArgs<T>(this Expression<Func<T>> property)
4: {
5: var expression = property.Body as MemberExpression;
6: var member = expression.Member;
7: return new PropertyChangedEventArgs(member.Name);
8: }
9: }
This way we don’t have to use strings when notifying a property has changed and we avoid runtime errors if we mistype something.
I also have a MainViewModel, to which the DataCOntext of the view is set. This view model contains a ObservableTrackableCollection<SupplierViewModel> property, which is bound to the DataGrid on the view.
1: public class MainViewModel
2: {
3: private ObservableTrackableCollection<SupplierViewModel>
4: suppliers = new ObservableTrackableCollection<SupplierViewModel>();
5: public MainViewModel()
6: {
7: Suppliers.ItemsAdded += new ObservableTrackableCollection<SupplierViewModel>.CollectionItemHandler(Suppliers_ItemsAdded);
8: Suppliers.ItemsRemoved += new ObservableTrackableCollection<SupplierViewModel>.CollectionItemHandler(Suppliers_ItemsRemoved);
9: Suppliers.ItemPropertyChanged += new ObservableTrackableCollection<SupplierViewModel>.ItemPropertyHandler(Suppliers_ItemPropertyChanged);
10: }
11: public ObservableTrackableCollection<SupplierViewModel> Suppliers
12: {
13: get { return suppliers; }
14: }
15: void Suppliers_ItemPropertyChanged(object sender, ItemPropertyEventArgs args)
16: {
17: System.Diagnostics.Debug.WriteLine("changed: " + args.PropertyName);
18: }
19: void Suppliers_ItemsRemoved(object sender, CollectionItemEventArgs<SupplierViewModel> args)
20: {
21: foreach (var item in args.CollectionItems)
22: {
23: System.Diagnostics.Debug.WriteLine("removed: " + item.Name);
24: }
25: }
26: void Suppliers_ItemsAdded(object sender, CollectionItemEventArgs<SupplierViewModel> args)
27: {
28: foreach (var item in args.CollectionItems)
29: {
30: System.Diagnostics.Debug.WriteLine("added: " + item.Name);
31: }
32: }
33:
34: public void LoadSuppliers()
35: {
36: IList<SupplierModel> supplierList = SupplierRepository.GetSuppliers();
37: foreach (SupplierModel supplier in supplierList)
38: {
39: suppliers.Add(new SupplierViewModel(supplier));
40: }
41: suppliers.ResetTrackInfo();
42: }
43: private DelegateCommand saveSuppliersCommand;
44: public ICommand SaveSuppliersCommand
45: {
46: get
47: {
48: if (saveSuppliersCommand == null)
49: {
50: saveSuppliersCommand = new DelegateCommand(SaveSuppliers, CanSaveSuppliers);
51: }
52: return saveSuppliersCommand;
53: }
54: }
55: }
As you can see, thanks to the ObservableTrackableCollection we have a number of built-in features:
- events like ItemsAdded, ItemsRemoved, ItemPropertyChanged allow the view model to be informed of changes
- the property IsCollectionValid is true when every item in the collection validates, or false when there is at least one error
- the GetChanges method allows us to get the added, modified, deleted and original entities in the collection
- the ResetTrackInfo sets the track information of all collection items to ‘not modified’.
Note that I use the model’s SupplierRepository to fill the suppliers collection with SupplierModel items, using a service:
1: public class SupplierRepository
2: {
3: public static IList<Schepers.Client.SCSA.Models.SupplierModel> GetSuppliers()
4: {
5: StockServiceReference.StockServiceClient client = new StockServiceClient();
6: GetSuppliersResponse response = client.GetSuppliers(new GetSuppliersRequest() { });
7: client.Close();
8: IList<SupplierModel> suppliers = response.Suppliers.TransformList<StockServiceReference.Supplier, SupplierModel>();
9: return suppliers;
10: }
11: }
And here is the implementation of my ObservableTrackableCollection:
1: /// <summary>
2: /// An ObservableTrackableCollection is an observable collection of a
3: /// generic type with tracking capabilities. The generic type has to
4: /// implement INotifyPropertyChanged so that the ObservableTrackableCollection
5: /// is notified when a property of the type changed. It also has to
6: /// implement IDataErrorInfo, to that it is able to validate its collection
7: /// items.
8: /// </summary>
9: /// <typeparam name="T">Type that collection consists of.</typeparam>
10: public class ObservableTrackableCollection<T> : ObservableCollection<T>
11: where T : INotifyPropertyChanged, IDataErrorInfo
12: {
13: // Delegate for collection handler
14: public delegate void CollectionItemHandler(object sender, CollectionItemEventArgs<T> args);
15: public delegate void ItemPropertyHandler(object sender, ItemPropertyEventArgs args);
16: // Events
17: public event CollectionItemHandler ItemsAdded;
18: public event CollectionItemHandler ItemsRemoved;
19: public event ItemPropertyHandler ItemPropertyChanged;
20: /// <summary>
21: /// True if all collection items validate, false if there is a validation
22: /// error in one or more items. It uses the IDataErrorInfo implementation of
23: /// the entities to do this validation.
24: /// </summary>
25: public bool IsCollectionValid
26: {
27: get
28: {
29: bool areErrors = false;
30: foreach (IDataErrorInfo item in this)
31: {
32: if (item.Error != string.Empty)
33: {
34: areErrors = true;
35: break;
36: }
37: }
38: return !areErrors;
39: }
40: }
41: /// <summary>
42: /// Occurs when items have been added to the collection.
43: /// </summary>
44: /// <param name="sender"></param>
45: /// <param name="args"></param>
46: protected void OnItemsAdded(object sender, CollectionItemEventArgs<T> args)
47: {
48: if (ItemsAdded != null)
49: {
50: ItemsAdded(sender, args);
51: }
52: }
53: /// <summary>
54: /// Occurs when items have been removed from the collection.
55: /// </summary>
56: /// <param name="sender"></param>
57: /// <param name="args"></param>
58: protected void OnItemsRemoved(object sender, CollectionItemEventArgs<T> args)
59: {
60: if (ItemsRemoved != null)
61: {
62: ItemsRemoved(sender, args);
63: }
64: }
65: /// <summary>
66: /// Occurs when a property has been changed.
67: /// </summary>
68: /// <param name="sender"></param>
69: /// <param name="args"></param>
70: protected void OnItemPropertyChanged(object sender, ItemPropertyEventArgs args)
71: {
72: if (ItemPropertyChanged != null)
73: {
74: ItemPropertyChanged(sender, args);
75: }
76: }
77: // List of all trackable objects.
78: private List<Trackable<T>> items = new List<Trackable<T>>();
79: /// <summary>
80: /// Occurs when an item has been added to or removed from the observable collection.
81: /// </summary>
82: /// <param name="e"></param>
83: protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
84: {
85: base.OnCollectionChanged(e);
86: switch (e.Action)
87: {
88: case System.Collections.Specialized.NotifyCollectionChangedAction.Add:
89: // One or more items have been added.
90: IList<T> addedCollectionItems = new List<T>();
91: foreach (var item in e.NewItems)
92: {
93: // Add item to trackable collection.
94: Trackable<T> trackable = new Trackable<T>((T)item, TrackingState.Added);
95: items.Add(trackable);
96: addedCollectionItems.Add((T)item);
97: // Every time a new item is inserted, attach a PropertyChangedEventHandler so that we can
98: // track property changes of the item.
99: ((T)item).PropertyChanged += new PropertyChangedEventHandler(ObservableTrackableCollection_PropertyChanged);
100: }
101: OnItemsAdded(this, new CollectionItemEventArgs<T>(addedCollectionItems));
102: break;
103: case System.Collections.Specialized.NotifyCollectionChangedAction.Remove:
104: // One or more items have been deleted.
105: IList<T> removedCollectionItems = new List<T>();
106: foreach (T item in e.OldItems)
107: {
108: Trackable<T> trackable = items.Find(t => t.Item.Equals(item));
109: switch (trackable.TrackingState)
110: {
111: case TrackingState.Unmodified:
112: case TrackingState.Changed:
113: // Item was original or modified, and then deleted.
114: trackable.TrackingState = TrackingState.Removed;
115: break;
116: case TrackingState.Added:
117: // Item was added and then deleted, no need to track it anymore.
118: items.Remove(trackable);
119: break;
120: default:
121: break;
122: }
123: removedCollectionItems.Add((T)item);
124: }
125: OnItemsRemoved(this, new CollectionItemEventArgs<T>(removedCollectionItems));
126: break;
127: case System.Collections.Specialized.NotifyCollectionChangedAction.Replace:
128: throw new NotImplementedException("Replace not yet handled.");
129: }
130: }
131: /// <summary>
132: /// Occurs when a property has changed.
133: /// </summary>
134: /// <param name="sender"></param>
135: /// <param name="e"></param>
136: void ObservableTrackableCollection_PropertyChanged(object sender, PropertyChangedEventArgs e)
137: {
138: // Mark as modified only if it was original item.
139: Trackable<T> trackable = items.Find(t => t.Item.Equals(sender));
140: switch (trackable.TrackingState)
141: {
142: case TrackingState.Unmodified:
143: trackable.TrackingState = TrackingState.Changed;
144: break;
145: default:
146: break;
147: }
148: OnItemPropertyChanged(this, new ItemPropertyEventArgs(e.PropertyName));
149: }
150: /// <summary>
151: /// Resets tracking information of all items in the collection.
152: /// </summary>
153: public void ResetTrackInfo()
154: {
155: foreach (Trackable<T> trackable in items)
156: {
157: trackable.TrackingState = TrackingState.Unmodified;
158: }
159: }
160: /// <summary>
161: /// Gets changes in the collection for specified track state.
162: /// </summary>
163: /// <param name="trackingState">Tracking state.</param>
164: /// <returns>List of changed items in collection.</returns>
165: public List<T> GetChanges(TrackingState trackingState)
166: {
167: var results = from i in items
168: where i.TrackingState == trackingState
169: select i.Item;
170: return results.ToList<T>();
171: }
172: /// <summary>
173: /// This class is used by ObservableTrackableCollection to enable tracking of
174: /// items of type U.
175: /// </summary>
176: /// <typeparam name="U">Type to track.</typeparam>
177: private class Trackable<U>
178: {
179: public Trackable(U item, TrackingState trackingState)
180: {
181: Item = item;
182: TrackingState = trackingState;
183: }
184: public U Item;
185: public TrackingState TrackingState;
186: }
187: }
188:
189: /// <summary>
190: /// Tracking states.
191: /// </summary>
192: public enum TrackingState
193: {
194: /// <summary>
195: /// Indicates that an item is not changed, deleted or added.
196: /// </summary>
197: Unmodified,
198: /// <summary>
199: /// Indicates an item was changed.
200: /// </summary>
201: Changed,
202: /// <summary>
203: /// Indicates an item was removed.
204: /// </summary>
205: Removed,
206: /// <summary>
207: /// Indicates an item was added.
208: /// </summary>
209: Added
210: };
211:
212: /// <summary>
213: /// EventArgs for collection items.
214: /// </summary>
215: /// <typeparam name="T"></typeparam>
216: public class CollectionItemEventArgs<T> : EventArgs
217: {
218: public IList<T> CollectionItems { get; set; }
219: public CollectionItemEventArgs(IList<T> collectionItems)
220: {
221: this.CollectionItems = collectionItems;
222: }
223: }
224: /// <summary>
225: /// EventArgs for item properties.
226: /// </summary>
227: public class ItemPropertyEventArgs : EventArgs
228: {
229: public string PropertyName { get; set; }
230: public ItemPropertyEventArgs(string propertyName)
231: {
232: this.PropertyName = propertyName;
233: }
234: }
This is my first version, so it may be improved, but the general idea is that this generic ObservableTrackableCollection gives you validation and tracking capabilities, but that each component in the MVVM pattern still has its own responsability: the model contains model data and validation logic, the view model encapsulates this model and contains everything that is needed by the view like for example property change notifications, properties the view can bound too, etc…