2009-07-11 Michael Barker <mike@middlesoft.co.uk>
[mono.git] / mcs / class / System.Data.Linq / src / DbLinq / Data / Linq / Implementation / MemberModificationHandler.cs
1 #region MIT license\r
2 // \r
3 // MIT license\r
4 //\r
5 // Copyright (c) 2007-2008 Jiri Moudry, Pascal Craponne\r
6 // \r
7 // Permission is hereby granted, free of charge, to any person obtaining a copy\r
8 // of this software and associated documentation files (the "Software"), to deal\r
9 // in the Software without restriction, including without limitation the rights\r
10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\r
11 // copies of the Software, and to permit persons to whom the Software is\r
12 // furnished to do so, subject to the following conditions:\r
13 // \r
14 // The above copyright notice and this permission notice shall be included in\r
15 // all copies or substantial portions of the Software.\r
16 // \r
17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\r
18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\r
19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\r
20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\r
21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\r
22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\r
23 // THE SOFTWARE.\r
24 // \r
25 #endregion\r
26 using System;\r
27 using System.Collections.Generic;\r
28 using System.ComponentModel;\r
29 using System.Data.Linq.Mapping;\r
30 using System.Reflection;\r
31 using DbLinq.Util;\r
32 \r
33 namespace DbLinq.Data.Linq.Implementation\r
34 {\r
35     /// <summary>\r
36     /// ModificationHandler class handles entities in two ways:\r
37     /// 1. if entity implements IModifed, uses the interface and its IsModifed flag property\r
38     /// 2. otherwise, the handler keeps a dictionary of raw data per entity\r
39     /// </summary>\r
40     internal class MemberModificationHandler : IMemberModificationHandler\r
41     {\r
42         private readonly IDictionary<object, IDictionary<string, object>> rawDataEntities = new Dictionary<object, IDictionary<string, object>>(new ReferenceEqualityComparer<object>());\r
43         private readonly IDictionary<object, IDictionary<string, MemberInfo>> modifiedProperties = new Dictionary<object, IDictionary<string, MemberInfo>>(new ReferenceEqualityComparer<object>());\r
44 \r
45         private static readonly IDictionary<string, MemberInfo> propertyChangingSentinal = new Dictionary<string, MemberInfo>();\r
46 \r
47         /// <summary>\r
48         /// Gets the column members.\r
49         /// </summary>\r
50         /// <param name="entityType">Type of the entity.</param>\r
51         /// <param name="metaModel">The meta model.</param>\r
52         /// <returns></returns>\r
53         protected virtual IEnumerable<MemberInfo> GetColumnMembers(Type entityType, MetaModel metaModel)\r
54         {\r
55             foreach (var dataMember in metaModel.GetTable(entityType).RowType.PersistentDataMembers)\r
56             {\r
57                 yield return dataMember.Member;\r
58             }\r
59         }\r
60 \r
61         /// <summary>\r
62         /// Determines whether the specified type is primitive type.\r
63         /// </summary>\r
64         /// <param name="type">The type.</param>\r
65         /// <returns>\r
66         ///     <c>true</c> if the specified type is primitive type; otherwise, <c>false</c>.\r
67         /// </returns>\r
68         protected static bool IsPrimitiveType(Type type)\r
69         {\r
70             if (type.IsValueType)\r
71                 return true;\r
72             if (type == typeof(string))\r
73                 return true;\r
74             return false;\r
75         }\r
76 \r
77         /// <summary>\r
78         /// Adds simple (value) properties of an object to a given dictionary\r
79         /// and recurses if a property contains complex data\r
80         /// </summary>\r
81         /// <param name="entity"></param>\r
82         /// <param name="rawData"></param>\r
83         /// <param name="prefix"></param>\r
84         /// <param name="metaModel"></param>\r
85         protected void AddRawData(object entity, IDictionary<string, object> rawData, string prefix, MetaModel metaModel)\r
86         {\r
87             if (entity == null)\r
88                 return;\r
89             foreach (var memberInfo in GetColumnMembers(entity.GetType(), metaModel))\r
90             {\r
91                 var propertyValue = memberInfo.GetMemberValue(entity);\r
92                 // if it is a value, it can be stored directly\r
93                 var memberType = memberInfo.GetMemberType();\r
94                 if (IsPrimitiveType(memberType))\r
95                 {\r
96                     rawData[prefix + memberInfo.Name] = propertyValue;\r
97                 }\r
98                 else if (memberType.IsArray)\r
99                 {\r
100                     if (propertyValue != null)\r
101                     {\r
102                         var arrayValue = (Array) propertyValue;\r
103                         for (int arrayIndex = 0; arrayIndex < arrayValue.Length; arrayIndex++)\r
104                         {\r
105                             rawData[string.Format("{0}[{1}]", memberInfo.Name, arrayIndex)] =\r
106                                 arrayValue.GetValue(arrayIndex);\r
107                         }\r
108                     }\r
109                 }\r
110                 else // otherwise, we recurse, and prefix the current property name to sub properties to avoid conflicts\r
111                 {\r
112                     AddRawData(propertyValue, rawData, memberInfo.Name + ".", metaModel);\r
113                 }\r
114             }\r
115         }\r
116 \r
117         /// <summary>\r
118         /// Creates a "flat view" from a composite object\r
119         /// </summary>\r
120         /// <param name="entity"></param>\r
121         /// <param name="metaModel"></param>\r
122         /// <returns>a pair of {property name, property value}</returns>\r
123         protected IDictionary<string, object> GetEntityRawData(object entity, MetaModel metaModel)\r
124         {\r
125             var rawData = new Dictionary<string, object>();\r
126             AddRawData(entity, rawData, string.Empty, metaModel);\r
127             return rawData;\r
128         }\r
129 \r
130         /// <summary>\r
131         /// Tells if the object notifies a change\r
132         /// </summary>\r
133         /// <param name="entity"></param>\r
134         /// <returns></returns>\r
135         private static bool IsNotifying(object entity)\r
136         {\r
137             return entity is INotifyPropertyChanged\r
138                    || entity is INotifyPropertyChanging;\r
139         }\r
140 \r
141         /// <summary>\r
142         /// Start to watch an entity. From here, changes will make IsModified() return true\r
143         /// </summary>\r
144         /// <param name="entity"></param>\r
145         /// <param name="metaModel"></param>\r
146         public void Register(object entity, MetaModel metaModel)\r
147         {\r
148             Register(entity, entity, metaModel);\r
149         }\r
150 \r
151         /// <summary>\r
152         /// Start to watch an entity. From here, changes will make IsModified() return true if the entity has changed\r
153         /// If the entity is already registered, there's no error, but the entity is reset to its original state\r
154         /// </summary>\r
155         /// <param name="entity"></param>\r
156         /// <param name="entityOriginalState"></param>\r
157         /// <param name="metaModel"></param>\r
158         public void Register(object entity, object entityOriginalState, MetaModel metaModel)\r
159         {\r
160             // notifying, we need to wait for changes\r
161             if (IsNotifying(entity))\r
162             {\r
163                 RegisterNotification(entity, entityOriginalState, metaModel);\r
164             }\r
165             // raw data, we keep a snapshot of the current state\r
166             else\r
167             {\r
168                 if (!rawDataEntities.ContainsKey(entity) && entityOriginalState != null)\r
169                     rawDataEntities[entity] = GetEntityRawData(entityOriginalState, metaModel);\r
170             }\r
171         }\r
172 \r
173         private void RegisterNotification(object entity, object entityOriginalState, MetaModel metaModel)\r
174         {\r
175             if (modifiedProperties.ContainsKey(entity))\r
176                 return;\r
177             modifiedProperties[entity] = null;\r
178 \r
179             var entityChanged = entity as INotifyPropertyChanged;\r
180             if (entityChanged != null)\r
181             {\r
182                 entityChanged.PropertyChanged += OnPropertyChangedEvent;\r
183             }\r
184 \r
185             var entityChanging = entity as INotifyPropertyChanging;\r
186             if (entityChanging != null)\r
187             {\r
188                 entityChanging.PropertyChanging += OnPropertyChangingEvent;\r
189             }\r
190 \r
191             // then check all properties, and note them as changed if they already did\r
192             if (!ReferenceEquals(entity, entityOriginalState)) // only if we specified another original entity\r
193             {\r
194                 foreach (var dataMember in metaModel.GetTable(entity.GetType()).RowType.PersistentDataMembers)\r
195                 {\r
196                     var memberInfo = dataMember.Member;\r
197                     if (entityOriginalState == null ||\r
198                         IsPropertyModified(memberInfo.GetMemberValue(entity),\r
199                                            memberInfo.GetMemberValue(entityOriginalState)))\r
200                     {\r
201                         SetPropertyChanged(entity, memberInfo.Name);\r
202                     }\r
203                 }\r
204             }\r
205         }\r
206 \r
207         /// <summary>\r
208         /// Occurs on INotifyPropertyChanged.PropertyChanged\r
209         /// </summary>\r
210         /// <param name="sender"></param>\r
211         /// <param name="e"></param>\r
212         private void OnPropertyChangedEvent(object sender, PropertyChangedEventArgs e)\r
213         {\r
214             SetPropertyChanged(sender, e.PropertyName);\r
215         }\r
216 \r
217         private void OnPropertyChangingEvent(object entity, PropertyChangingEventArgs e)\r
218         {\r
219             if (modifiedProperties[entity] == null)\r
220                 modifiedProperties[entity] = propertyChangingSentinal;\r
221         }\r
222 \r
223         /// <summary>\r
224         /// Unregisters an entity.\r
225         /// This is useful when it is switched from update to delete list\r
226         /// </summary>\r
227         /// <param name="entity"></param>\r
228         public void Unregister(object entity)\r
229         {\r
230             if (IsNotifying(entity))\r
231                 UnregisterNotification(entity);\r
232             else\r
233             {\r
234                 if (rawDataEntities.ContainsKey(entity))\r
235                     rawDataEntities.Remove(entity);\r
236             }\r
237         }\r
238 \r
239         private void UnregisterNotification(object entity)\r
240         {\r
241             if (!modifiedProperties.ContainsKey(entity))\r
242                 return;\r
243             modifiedProperties.Remove(entity);\r
244             INotifyPropertyChanged npc = entity as INotifyPropertyChanged;\r
245             if (npc != null)\r
246             {\r
247                 npc.PropertyChanged -= OnPropertyChangedEvent;\r
248             }\r
249             var changing = entity as INotifyPropertyChanging;\r
250             if (changing != null)\r
251             {\r
252                 changing.PropertyChanging -= OnPropertyChangingEvent;\r
253             }\r
254         }\r
255 \r
256         /// <summary>\r
257         /// This method is called when a notifying object sends an event because of a property change\r
258         /// We may keep track of the precise change in the future\r
259         /// </summary>\r
260         /// <param name="entity"></param>\r
261         /// <param name="propertyName"></param>\r
262         private void SetPropertyChanged(object entity, string propertyName)\r
263         {\r
264             PropertyInfo pi = GetProperty(entity, propertyName);\r
265             if (pi == null)\r
266                 throw new ArgumentException("Incorrect property changed");\r
267 \r
268             if (modifiedProperties[entity] == null || \r
269                     ReferenceEquals(propertyChangingSentinal, modifiedProperties[entity]))\r
270             {\r
271                 modifiedProperties[entity] = new Dictionary<string, MemberInfo>();\r
272             }\r
273             modifiedProperties[entity][propertyName] = pi;\r
274         }\r
275 \r
276         /// <summary>\r
277         /// Returns if the entity was modified since it has been Register()ed for the first time\r
278         /// </summary>\r
279         /// <param name="entity"></param>\r
280         /// <param name="metaModel"></param>\r
281         /// <returns></returns>\r
282         public bool IsModified(object entity, MetaModel metaModel)\r
283         {\r
284             // 1. event notifying case (INotify*)\r
285             if (IsNotifying(entity))\r
286                 return IsNotifyingModified(entity);\r
287 \r
288             // 2. raw data\r
289             return IsRawModified(entity, metaModel);\r
290         }\r
291 \r
292         /// <summary>\r
293         /// Determines whether the specified notifiying entity is modified.\r
294         /// </summary>\r
295         /// <param name="entity">The entity.</param>\r
296         /// <returns>\r
297         ///     <c>true</c> if the specified notifiying entity is modified; otherwise, <c>false</c>.\r
298         /// </returns>\r
299         private bool IsNotifyingModified(object entity)\r
300         {\r
301             if (!modifiedProperties.ContainsKey(entity) || modifiedProperties[entity] == null)\r
302                 return false;\r
303             return ReferenceEquals(propertyChangingSentinal, modifiedProperties[entity]) ||\r
304                 modifiedProperties[entity].Count > 0;\r
305         }\r
306 \r
307         /// <summary>\r
308         /// Determines whether the specified property has changed, by comparing its current and previous value.\r
309         /// </summary>\r
310         /// <param name="p1">The p1.</param>\r
311         /// <param name="p2">The p2.</param>\r
312         /// <returns>\r
313         ///     <c>true</c> if the specified property has changed; otherwise, <c>false</c>.\r
314         /// </returns>\r
315         private static bool IsPropertyModified(object p1, object p2)\r
316         {\r
317             return !Equals(p1, p2);\r
318         }\r
319 \r
320         /// <summary>\r
321         /// Determines whether the specified raw entity has changed.\r
322         /// </summary>\r
323         /// <param name="entity">The entity.</param>\r
324         /// <param name="metaModel">The meta model.</param>\r
325         /// <returns>\r
326         ///     <c>true</c> if the specified raw entity has changed; otherwise, <c>false</c>.\r
327         /// </returns>\r
328         private bool IsRawModified(object entity, MetaModel metaModel)\r
329         {\r
330             // if not present, maybe it was inserted (or set to dirty)\r
331             // TODO: this will be useless when we will support the differential properties\r
332             if (!rawDataEntities.ContainsKey(entity))\r
333                 return true;\r
334 \r
335             IDictionary<string, object> originalData = rawDataEntities[entity];\r
336             IDictionary<string, object> currentData = GetEntityRawData(entity, metaModel);\r
337 \r
338             foreach (string key in originalData.Keys)\r
339             {\r
340                 object originalValue = originalData[key];\r
341                 object currentValue = currentData[key];\r
342                 if (IsPropertyModified(originalValue, currentValue))\r
343                     return true;\r
344             }\r
345             return false;\r
346         }\r
347 \r
348         /// <summary>\r
349         /// Returns a list of all modified properties since last Register/ClearModified\r
350         /// </summary>\r
351         /// <param name="entity"></param>\r
352         /// <param name="metaModel"></param>\r
353         /// <returns></returns>\r
354         public IList<MemberInfo> GetModifiedProperties(object entity, MetaModel metaModel)\r
355         {\r
356             if (IsNotifying(entity))\r
357                 return GetNotifyingModifiedProperties(entity, metaModel);\r
358 \r
359             return GetRawModifiedProperties(entity, metaModel);\r
360         }\r
361 \r
362         /// <summary>\r
363         /// Gets all column properties.\r
364         /// </summary>\r
365         /// <param name="entity">The entity.</param>\r
366         /// <param name="metaModel">The meta model.</param>\r
367         /// <returns></returns>\r
368         protected IList<MemberInfo> GetAllColumnProperties(object entity, MetaModel metaModel)\r
369         {\r
370             if (entity == null)\r
371                 throw new ArgumentNullException("entity");\r
372             var properties = new List<MemberInfo>(GetColumnMembers(entity.GetType(), metaModel));\r
373             return properties;\r
374         }\r
375 \r
376         /// <summary>\r
377         /// Gets the self declaring entity modified properties.\r
378         /// </summary>\r
379         /// <param name="entity">The entity.</param>\r
380         /// <param name="metaModel">The meta model.</param>\r
381         /// <returns></returns>\r
382         protected IList<MemberInfo> GetSelfDeclaringModifiedProperties(object entity, MetaModel metaModel)\r
383         {\r
384             return GetAllColumnProperties(entity, metaModel);\r
385         }\r
386 \r
387         /// <summary>\r
388         /// Gets the notifying entity modified properties.\r
389         /// </summary>\r
390         /// <param name="entity">The entity.</param>\r
391         /// <param name="metaModel">The meta model.</param>\r
392         /// <returns></returns>\r
393         protected IList<MemberInfo> GetNotifyingModifiedProperties(object entity, MetaModel metaModel)\r
394         {\r
395             IDictionary<string, MemberInfo> properties;\r
396             // if we don't have it, it is fully dirty\r
397             if (!modifiedProperties.TryGetValue(entity, out properties) || \r
398                     ReferenceEquals(propertyChangingSentinal, modifiedProperties[entity]))\r
399                 return GetAllColumnProperties(entity, metaModel);\r
400             return new List<MemberInfo>(properties.Values);\r
401         }\r
402 \r
403         /// <summary>\r
404         /// Gets modified properties for entity, by using raw compare method.\r
405         /// </summary>\r
406         /// <param name="entity">The entity.</param>\r
407         /// <param name="metaModel">The meta model.</param>\r
408         /// <returns></returns>\r
409         protected IList<MemberInfo> GetRawModifiedProperties(object entity, MetaModel metaModel)\r
410         {\r
411             var properties = new List<MemberInfo>();\r
412 \r
413             IDictionary<string, object> originalData;\r
414             // if we don't have this entity we consider all its properties as having been modified\r
415             if (!rawDataEntities.TryGetValue(entity, out originalData))\r
416                 return GetAllColumnProperties(entity, metaModel);\r
417             var currentData = GetEntityRawData(entity, metaModel);\r
418 \r
419             // otherwise, we iterate and find what's changed\r
420             foreach (string key in currentData.Keys)\r
421             {\r
422                 var currentValue = currentData[key];\r
423                 var originalValue = originalData[key];\r
424                 if (IsPropertyModified(originalValue, currentValue))\r
425                     properties.Add(GetProperty(entity, key));\r
426             }\r
427 \r
428             return properties;\r
429         }\r
430 \r
431         /// <summary>\r
432         /// Marks the entity as not dirty.\r
433         /// </summary>\r
434         /// <param name="entity"></param>\r
435         /// <param name="metaModel"></param>\r
436         public void ClearModified(object entity, MetaModel metaModel)\r
437         {\r
438             if (IsNotifying(entity))\r
439                 ClearNotifyingModified(entity);\r
440             else\r
441                 ClearRawModified(entity, metaModel);\r
442         }\r
443 \r
444         /// <summary>\r
445         /// Sets the notifying entity as unmodified.\r
446         /// </summary>\r
447         /// <param name="entity">The entity.</param>\r
448         private void ClearNotifyingModified(object entity)\r
449         {\r
450             modifiedProperties[entity] = null;\r
451         }\r
452 \r
453         /// <summary>\r
454         /// Sets the raw entity as unmodified.\r
455         /// </summary>\r
456         /// <param name="entity">The entity.</param>\r
457         /// <param name="metaModel">The meta model.</param>\r
458         private void ClearRawModified(object entity, MetaModel metaModel)\r
459         {\r
460             rawDataEntities[entity] = GetEntityRawData(entity, metaModel);\r
461         }\r
462 \r
463         /// <summary>\r
464         /// Gets the property, given a property name.\r
465         /// </summary>\r
466         /// <param name="entity">The entity.</param>\r
467         /// <param name="propertyName">Name of the property.</param>\r
468         /// <returns></returns>\r
469         private static PropertyInfo GetProperty(object entity, string propertyName)\r
470         {\r
471             return entity.GetType().GetProperty(propertyName);\r
472         }\r
473     }\r
474 }\r