Report abuse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
/// <summary>
/// Manages weak references to <see cref="INotifyPropertyChanged"/> 
/// subscribers and provides a friendly interface for <see cref="INotifyPropertyChanged"/> 
/// implementors to expose typed event subscription without relying on 
/// property string names.
/// </summary>
/// <remarks>
/// This class never leaks references to subscribers. 
/// </remarks>
/// <typeparam name="TSource">The type of the property changed source.</typeparam>
public class PropertyChangeManager<TSource>
	where TSource : INotifyPropertyChanged
{
	private List<Subscription> subscriptions = new List<Subscription>();
	private TSource source;

	/// <summary>
	/// Initializes a new instance of the <see cref="PropertyChangeManager&lt;TSource&gt;"/> class.
	/// </summary>
	/// <param name="source">The property changed source.</param>
	public PropertyChangeManager(TSource source)
	{
		this.source = source;
	}

	/// <summary>
	/// Subscribes to changes in the property referenced in the given 
	/// <paramref name="propertyExpression"/> with the given 
	/// <paramref name="callbackAction"/> delegate.
	/// </summary>
	/// <param name="propertyExpression">A lambda expression that accesses a property, such as <c>x => x.Name</c> 
	/// (where the type of x is <typeparamref name="TSource"/>).</param>
	/// <param name="callbackAction">The callback action to invoke when the given property changes.</param>
	public IDisposable SubscribeChanged(Expression<Func<TSource, object>> propertyExpression, Action<TSource> callbackAction)
	{
		return AddSubscription(new Subscription
		{
			IsStatic = callbackAction.Target == null,
			PropertyName = Reflect<TSource>.GetProperty(propertyExpression).Name,
			SubscriberReference = new WeakReference(callbackAction.Target),
			MethodCallback = callbackAction.Method
		});
	}

	/// <summary>
	/// Registers a regular event handler for change notification.
	/// </summary>
	public IDisposable AddHandler(PropertyChangedEventHandler handler)
	{
		return AddSubscription(new Subscription
		{
			IsStatic = handler.Target == null,
			SubscriberReference = new WeakReference(handler.Target),
			MethodCallback = handler.Method
		});
	}

	/// <summary>
	/// Unregisters the given event handler from change notification.
	/// </summary>
	/// <param name="handler">The value.</param>
	public void RemoveHandler(PropertyChangedEventHandler handler)
	{
		CleanupSubscribers();

		subscriptions.RemoveAll(s => s.SubscriberReference.Target == handler.Target && s.MethodCallback == handler.Method);
	}

	/// <summary>
	/// Notifies subscribers that the given property has changed.
	/// </summary>
	/// <param name="propertyExpression">A lambda expression that accesses a property, such as <c>x => x.Name</c> 
	/// (where the type of x is <typeparamref name="TSource"/>).</param>
	public void NotifyChanged(Expression<Func<TSource, object>> propertyExpression)
	{
		CleanupSubscribers();

		var propertyName = Reflect<TSource>.GetProperty(propertyExpression).Name;

		foreach (var subscription in subscriptions.Where(s => s.PropertyName == propertyName))
		{
			try
			{
				subscription.MethodCallback.Invoke(subscription.SubscriberReference.Target, new object[] { this.source });
			}
			catch (TargetInvocationException tie)
			{
				tie.InnerException.RethrowWithNoStackTraceLoss();
			}
		}

		// Call "old-style" handlers with the right signature.
		foreach (var subscription in subscriptions.Where(s => s.PropertyName == null))
		{
			try
			{
				subscription.MethodCallback.Invoke(subscription.SubscriberReference.Target, new object[] { this.source, new PropertyChangedEventArgs(propertyName) });
			}
			catch (TargetInvocationException tie)
			{
				tie.InnerException.RethrowWithNoStackTraceLoss();
			}
		}
	}

	private IDisposable AddSubscription(Subscription subscription)
	{
		CleanupSubscribers();

		subscriptions.Add(subscription);

		return new SubscriptionReference(this.subscriptions, subscription);
	}

	private void CleanupSubscribers()
	{
		subscriptions.RemoveAll(s => !s.IsStatic && !s.SubscriberReference.IsAlive);
	}

	/// <summary>
	/// Provides deterministic removal of a subscription without having to 
	/// create a separate class to hold the delegate reference. 
	/// Callers can simply keep the returned disposable from Subscribe 
	/// and use it to unsubscribe.
	/// </summary>
	private sealed class SubscriptionReference : IDisposable
	{
		private List<Subscription> subscriptions;
		private Subscription entry;

		public SubscriptionReference(List<Subscription> subscriptions, Subscription entry)
		{
			this.subscriptions = subscriptions;
			this.entry = entry;
		}

		public void Dispose()
		{
			this.subscriptions.Remove(this.entry);
		}
	}

	private class Subscription
	{
		public bool IsStatic { get; set; }
		public string PropertyName { get; set; }
		public WeakReference SubscriberReference { get; set; }
		public MethodInfo MethodCallback { get; set; }
	}
}

#region Helpers

/// <summary>
/// Provides strong-typed reflection of the <typeparamref name="TTarget"/> 
/// type.
/// </summary>
/// <typeparam name="TTarget">Type to reflect.</typeparam>
internal static class Reflect<TTarget>
{
	/// <summary>
	/// Gets the method represented by the lambda expression.
	/// </summary>
	/// <param name="method">An expression that invokes a method.</param>
	/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
	/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
	/// <returns>The method info.</returns>
	public static MethodInfo GetMethod(Expression<Action<TTarget>> method)
	{
		return GetMethodInfo(method);
	}

	/// <summary>
	/// Gets the method represented by the lambda expression.
	/// </summary>
	/// <param name="method">An expression that invokes a method.</param>
	/// <typeparam name="T1">Type of the first argument.</typeparam>
	/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
	/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
	/// <returns>The method info.</returns>
	public static MethodInfo GetMethod<T1>(Expression<Action<TTarget, T1>> method)
	{
		return GetMethodInfo(method);
	}

	/// <summary>
	/// Gets the method represented by the lambda expression.
	/// </summary>
	/// <param name="method">An expression that invokes a method.</param>
	/// <typeparam name="T1">Type of the first argument.</typeparam>
	/// <typeparam name="T2">Type of the second argument.</typeparam>
	/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
	/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
	/// <returns>The method info.</returns>
	public static MethodInfo GetMethod<T1, T2>(Expression<Action<TTarget, T1, T2>> method)
	{
		return GetMethodInfo(method);
	}

	/// <summary>
	/// Gets the method represented by the lambda expression.
	/// </summary>
	/// <param name="method">An expression that invokes a method.</param>
	/// <typeparam name="T1">Type of the first argument.</typeparam>
	/// <typeparam name="T2">Type of the second argument.</typeparam>
	/// <typeparam name="T3">Type of the third argument.</typeparam>
	/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
	/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a method invocation.</exception>
	/// <returns>The method info.</returns>
	public static MethodInfo GetMethod<T1, T2, T3>(Expression<Action<TTarget, T1, T2, T3>> method)
	{
		return GetMethodInfo(method);
	}

	/// <summary>
	/// Gets the property represented by the lambda expression.
	/// </summary>
	/// <param name="property">An expression that accesses a property.</param>
	/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
	/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a property access.</exception>
	/// <returns>The property info.</returns>
	public static PropertyInfo GetProperty(Expression<Func<TTarget, object>> property)
	{
		PropertyInfo info = GetMemberInfo(property) as PropertyInfo;
		if (info == null)
		{
			throw new ArgumentException("Member is not a property");
		}

		return info;
	}

	/// <summary>
	/// Gets the field represented by the lambda expression.
	/// </summary>
	/// <param name="field">An expression that accesses a field.</param>
	/// <exception cref="ArgumentNullException">The <paramref name="method"/> is null.</exception>
	/// <exception cref="ArgumentException">The <paramref name="method"/> is not a lambda expression or it does not represent a field access.</exception>
	/// <returns>The field info.</returns>
	public static FieldInfo GetField(Expression<Func<TTarget, object>> field)
	{
		FieldInfo info = GetMemberInfo(field) as FieldInfo;
		if (info == null)
		{
			throw new ArgumentException("Member is not a field");
		}

		return info;
	}

	private static MethodInfo GetMethodInfo(Expression method)
	{
		if (method == null)
		{
			throw new ArgumentNullException("method");
		}

		LambdaExpression lambda = method as LambdaExpression;
		if (lambda == null)
		{
			throw new ArgumentException("Not a lambda expression", "method");
		}

		if (lambda.Body.NodeType != ExpressionType.Call)
		{
			throw new ArgumentException("Not a method call", "method");
		}

		return ((MethodCallExpression)lambda.Body).Method;
	}

	private static MemberInfo GetMemberInfo(Expression member)
	{
		if (member == null)
		{
			throw new ArgumentNullException("member");
		}

		LambdaExpression lambda = member as LambdaExpression;
		if (lambda == null)
		{
			throw new ArgumentException("Not a lambda expression", "member");
		}

		MemberExpression memberExpr = null;

		// The Func<TTarget, object> we use returns an object, so first statement can be either 
		// a cast (if the field/property does not return an object) or the direct member access.
		if (lambda.Body.NodeType == ExpressionType.Convert)
		{
			// The cast is an unary expression, where the operand is the 
			// actual member access expression.
			memberExpr = ((UnaryExpression)lambda.Body).Operand as MemberExpression;
		}
		else if (lambda.Body.NodeType == ExpressionType.MemberAccess)
		{
			memberExpr = lambda.Body as MemberExpression;
		}

		if (memberExpr == null)
		{
			throw new ArgumentException("Not a member access", "member");
		}

		return memberExpr.Member;
	}
}

/// <summary>
/// Utility methods for exceptions.
/// </summary>
public static class ExceptionExtensions
{
	private static readonly FieldInfo remoteStackTraceString =
		typeof(Exception).GetField("_remoteStackTraceString", BindingFlags.Instance | BindingFlags.NonPublic) ??
		typeof(Exception).GetField("remote_stack_trace", BindingFlags.Instance | BindingFlags.NonPublic);

	/// <summary>
	/// Rethrows an exception object without losing the existing stack trace information.
	/// </summary>
	/// <param name="ex">The exception to re-throw.</param>
	/// <remarks>
	/// For more information on this technique, see
	/// http://www.dotnetjunkies.com/WebLog/chris.taylor/archive/2004/03/03/8353.aspx
	/// </remarks>
	public static void RethrowWithNoStackTraceLoss(this Exception ex)
	{
		remoteStackTraceString.SetValue(ex, ex.StackTrace + Environment.NewLine);

		throw ex;
	}
}

#endregion