/ReactiveUI/IReactiveObject.cs

https://github.com/neuroblaster/ReactiveUI · C# · 237 lines · 152 code · 43 blank · 42 comment · 7 complexity · 04ba2754de463163efd3559e20cbbf1e MD5 · raw file

  1. using System;
  2. using System.Runtime.CompilerServices;
  3. using System.Reactive.Linq;
  4. using System.Reactive.Subjects;
  5. using System.Reactive.Concurrency;
  6. using System.Threading;
  7. using System.Reactive.Disposables;
  8. using System.Diagnostics.Contracts;
  9. using System.ComponentModel;
  10. using Splat;
  11. using System.Collections.Generic;
  12. namespace ReactiveUI
  13. {
  14. public interface IReactiveObject : INotifyPropertyChanged, INotifyPropertyChanging, IEnableLogger
  15. {
  16. event PropertyChangingEventHandler PropertyChanging;
  17. event PropertyChangedEventHandler PropertyChanged;
  18. void RaisePropertyChanging(PropertyChangingEventArgs args);
  19. void RaisePropertyChanged(PropertyChangedEventArgs args);
  20. }
  21. [Preserve(AllMembers = true)]
  22. public static class IReactiveObjectExtensions
  23. {
  24. static ConditionalWeakTable<IReactiveObject, IExtensionState<IReactiveObject>> state = new ConditionalWeakTable<IReactiveObject, IExtensionState<IReactiveObject>>();
  25. internal static IObservable<IObservedChange<TSender, object>> getChangedObservable<TSender>(this TSender This) where TSender : IReactiveObject
  26. {
  27. var val = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));
  28. return val.Changed.Cast<IObservedChange<TSender, object>>();
  29. }
  30. internal static IObservable<IObservedChange<TSender, object>> getChangingObservable<TSender>(this TSender This) where TSender : IReactiveObject
  31. {
  32. var val = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));
  33. return val.Changing.Cast<IObservedChange<TSender, object>>();
  34. }
  35. internal static IObservable<Exception> getThrownExceptionsObservable<TSender>(this TSender This) where TSender : IReactiveObject
  36. {
  37. var s = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));
  38. return s.ThrownExceptions;
  39. }
  40. internal static void raisePropertyChanging<TSender>(this TSender This, string propertyName) where TSender : IReactiveObject
  41. {
  42. Contract.Requires(propertyName != null);
  43. var s = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));
  44. s.raisePropertyChanging(propertyName);
  45. }
  46. internal static void raisePropertyChanged<TSender>(this TSender This, string propertyName) where TSender : IReactiveObject
  47. {
  48. Contract.Requires(propertyName != null);
  49. var s = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));
  50. s.raisePropertyChanged(propertyName);
  51. }
  52. internal static IDisposable suppressChangeNotifications<TSender>(this TSender This) where TSender : IReactiveObject
  53. {
  54. var s = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));
  55. return s.suppressChangeNotifications();
  56. }
  57. internal static bool areChangeNotificationsEnabled<TSender>(this TSender This) where TSender : IReactiveObject
  58. {
  59. var s = state.GetValue(This, key => (IExtensionState<IReactiveObject>)new ExtensionState<TSender>(This));
  60. return s.areChangeNotificationsEnabled();
  61. }
  62. /// <summary>
  63. /// RaiseAndSetIfChanged fully implements a Setter for a read-write
  64. /// property on a ReactiveObject, using CallerMemberName to raise the notification
  65. /// and the ref to the backing field to set the property.
  66. /// </summary>
  67. /// <typeparam name="TObj">The type of the This.</typeparam>
  68. /// <typeparam name="TRet">The type of the return value.</typeparam>
  69. /// <param name="This">The <see cref="ReactiveObject"/> raising the notification.</param>
  70. /// <param name="backingField">A Reference to the backing field for this
  71. /// property.</param>
  72. /// <param name="newValue">The new value.</param>
  73. /// <param name="propertyName">The name of the property, usually
  74. /// automatically provided through the CallerMemberName attribute.</param>
  75. /// <returns>The newly set value, normally discarded.</returns>
  76. public static TRet RaiseAndSetIfChanged<TObj, TRet>(
  77. this TObj This,
  78. ref TRet backingField,
  79. TRet newValue,
  80. [CallerMemberName] string propertyName = null) where TObj : IReactiveObject
  81. {
  82. Contract.Requires(propertyName != null);
  83. if (EqualityComparer<TRet>.Default.Equals(backingField, newValue)) {
  84. return newValue;
  85. }
  86. This.raisePropertyChanging(propertyName);
  87. backingField = newValue;
  88. This.raisePropertyChanged(propertyName);
  89. return newValue;
  90. }
  91. /// <summary>
  92. /// Use this method in your ReactiveObject classes when creating custom
  93. /// properties where raiseAndSetIfChanged doesn't suffice.
  94. /// </summary>
  95. /// <param name="This">The instance of ReactiveObject on which the property has changed.</param>
  96. /// <param name="propertyName">
  97. /// A string representing the name of the property that has been changed.
  98. /// Leave <c>null</c> to let the runtime set to caller member name.
  99. /// </param>
  100. public static void RaisePropertyChanged(this IReactiveObject This, [CallerMemberName] string propertyName = null)
  101. {
  102. This.raisePropertyChanged(propertyName);
  103. }
  104. /// <summary>
  105. /// Use this method in your ReactiveObject classes when creating custom
  106. /// properties where raiseAndSetIfChanged doesn't suffice.
  107. /// </summary>
  108. /// <param name="This">The instance of ReactiveObject on which the property has changed.</param>
  109. /// <param name="propertyName">
  110. /// A string representing the name of the property that has been changed.
  111. /// Leave <c>null</c> to let the runtime set to caller member name.
  112. /// </param>
  113. public static void RaisePropertyChanging(this IReactiveObject This, [CallerMemberName] string propertyName = null)
  114. {
  115. This.raisePropertyChanging(propertyName);
  116. }
  117. class ExtensionState<TSender> : IExtensionState<TSender> where TSender : IReactiveObject
  118. {
  119. private long changeNotificationsSuppressed;
  120. private ISubject<IObservedChange<TSender, object>> changingSubject;
  121. private ISubject<IObservedChange<TSender, object>> changedSubject;
  122. private ISubject<Exception> thrownExceptions;
  123. private TSender sender;
  124. /// <summary>
  125. /// Initializes a new instance of the <see cref="ExtensionState{TSender}"/> class.
  126. /// </summary>
  127. public ExtensionState(TSender sender)
  128. {
  129. this.sender = sender;
  130. this.changingSubject = new Subject<IObservedChange<TSender, object>>();
  131. this.changedSubject = new Subject<IObservedChange<TSender, object>>();
  132. this.thrownExceptions = new ScheduledSubject<Exception>(Scheduler.Immediate, RxApp.DefaultExceptionHandler);
  133. }
  134. public IObservable<IObservedChange<TSender, object>> Changing {
  135. get { return this.changingSubject; }
  136. }
  137. public IObservable<IObservedChange<TSender, object>> Changed {
  138. get { return this.changedSubject; }
  139. }
  140. public IObservable<Exception> ThrownExceptions {
  141. get { return thrownExceptions; }
  142. }
  143. public bool areChangeNotificationsEnabled()
  144. {
  145. return (Interlocked.Read(ref changeNotificationsSuppressed) == 0);
  146. }
  147. /// <summary>
  148. /// When this method is called, an object will not fire change
  149. /// notifications (neither traditional nor Observable notifications)
  150. /// until the return value is disposed.
  151. /// </summary>
  152. /// <returns>An object that, when disposed, reenables change
  153. /// notifications.</returns>
  154. public IDisposable suppressChangeNotifications()
  155. {
  156. Interlocked.Increment(ref changeNotificationsSuppressed);
  157. return Disposable.Create(() => Interlocked.Decrement(ref changeNotificationsSuppressed));
  158. }
  159. public void raisePropertyChanging(string propertyName)
  160. {
  161. if (!this.areChangeNotificationsEnabled())
  162. return;
  163. sender.RaisePropertyChanging(new PropertyChangingEventArgs(propertyName));
  164. this.notifyObservable(sender, new ObservedChange<TSender, object>(sender, propertyName, null), this.changingSubject);
  165. }
  166. public void raisePropertyChanged(string propertyName)
  167. {
  168. if (!this.areChangeNotificationsEnabled())
  169. return;
  170. sender.RaisePropertyChanged(new PropertyChangedEventArgs(propertyName));
  171. this.notifyObservable(sender, new ObservedChange<TSender, object>(sender, propertyName, null), this.changedSubject);
  172. }
  173. internal void notifyObservable<T>(IReactiveObject rxObj, T item, ISubject<T> subject)
  174. {
  175. try {
  176. subject.OnNext(item);
  177. } catch (Exception ex) {
  178. rxObj.Log().ErrorException("ReactiveObject Subscriber threw exception", ex);
  179. thrownExceptions.OnNext(ex);
  180. }
  181. }
  182. }
  183. interface IExtensionState<out TSender> where TSender: IReactiveObject
  184. {
  185. IObservable<IObservedChange<TSender, object>> Changing { get; }
  186. IObservable<IObservedChange<TSender, object>> Changed { get; }
  187. void raisePropertyChanging(string propertyName);
  188. void raisePropertyChanged(string propertyName);
  189. IObservable<Exception> ThrownExceptions { get; }
  190. bool areChangeNotificationsEnabled();
  191. IDisposable suppressChangeNotifications();
  192. }
  193. }
  194. }