ObservableRecipient.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. // Licensed to the .NET Foundation under one or more agreements.
  2. // The .NET Foundation licenses this file to you under the MIT license.
  3. // See the LICENSE file in the project root for more information.
  4. // This file is inspired from the MvvmLight library (lbugnion/MvvmLight),
  5. // more info in ThirdPartyNotices.txt in the root of the project.
  6. using System;
  7. using System.Collections.Generic;
  8. using System.Diagnostics.CodeAnalysis;
  9. using System.Runtime.CompilerServices;
  10. using CommunityToolkit.Mvvm.Messaging;
  11. using CommunityToolkit.Mvvm.Messaging.Messages;
  12. namespace CommunityToolkit.Mvvm.ComponentModel;
  13. /// <summary>
  14. /// A base class for observable objects that also acts as recipients for messages. This class is an extension of
  15. /// <see cref="ObservableObject"/> which also provides built-in support to use the <see cref="IMessenger"/> type.
  16. /// </summary>
  17. public abstract class ObservableRecipient : ObservableObject
  18. {
  19. /// <summary>
  20. /// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
  21. /// </summary>
  22. /// <remarks>
  23. /// This constructor will produce an instance that will use the <see cref="WeakReferenceMessenger.Default"/> instance
  24. /// to perform requested operations. It will also be available locally through the <see cref="Messenger"/> property.
  25. /// </remarks>
  26. protected ObservableRecipient()
  27. : this(WeakReferenceMessenger.Default)
  28. {
  29. }
  30. /// <summary>
  31. /// Initializes a new instance of the <see cref="ObservableRecipient"/> class.
  32. /// </summary>
  33. /// <param name="messenger">The <see cref="IMessenger"/> instance to use to send messages.</param>
  34. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="messenger"/> is <see langword="null"/>.</exception>
  35. protected ObservableRecipient(IMessenger messenger)
  36. {
  37. ArgumentNullException.ThrowIfNull(messenger);
  38. Messenger = messenger;
  39. }
  40. /// <summary>
  41. /// Gets the <see cref="IMessenger"/> instance in use.
  42. /// </summary>
  43. protected IMessenger Messenger { get; }
  44. private bool isActive;
  45. /// <summary>
  46. /// Gets or sets a value indicating whether the current view model is currently active.
  47. /// </summary>
  48. public bool IsActive
  49. {
  50. get => this.isActive;
  51. [RequiresUnreferencedCode(
  52. "When this property is set to true, the OnActivated() method will be invoked, which will register all necessary message handlers for this recipient. " +
  53. "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " +
  54. "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " +
  55. "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type. " +
  56. "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")]
  57. [RequiresDynamicCode(
  58. "When this property is set to true, the OnActivated() method will be invoked, which will register all necessary message handlers for this recipient. " +
  59. "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " +
  60. "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " +
  61. "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime. " +
  62. "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")]
  63. set
  64. {
  65. if (SetProperty(ref this.isActive, value, true))
  66. {
  67. if (value)
  68. {
  69. OnActivated();
  70. }
  71. else
  72. {
  73. OnDeactivated();
  74. }
  75. }
  76. }
  77. }
  78. /// <summary>
  79. /// Invoked whenever the <see cref="IsActive"/> property is set to <see langword="true"/>.
  80. /// Use this method to register to messages and do other initialization for this instance.
  81. /// </summary>
  82. /// <remarks>
  83. /// The base implementation registers all messages for this recipients that have been declared
  84. /// explicitly through the <see cref="IRecipient{TMessage}"/> interface, using the default channel.
  85. /// For more details on how this works, see the <see cref="IMessengerExtensions.RegisterAll"/> method.
  86. /// If you need more fine tuned control, want to register messages individually or just prefer
  87. /// the lambda-style syntax for message registration, override this method and register manually.
  88. /// </remarks>
  89. [RequiresUnreferencedCode(
  90. "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " +
  91. "If this type is removed by the linker, or if the target recipient was created dynamically and was missed by the source generator, a slower fallback " +
  92. "path using a compiled LINQ expression will be used. This will have more overhead in the first invocation of this method for any given recipient type. " +
  93. "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")]
  94. [RequiresDynamicCode(
  95. "This method requires the generated CommunityToolkit.Mvvm.Messaging.__Internals.__IMessengerExtensions type not to be removed to use the fast path. " +
  96. "If that is present, the method is AOT safe, as the only methods being invoked to register the messages will be the ones produced by the source generator. " +
  97. "If it isn't, this method will need to dynamically create the generic methods to register messages, which might not be available at runtime. " +
  98. "Alternatively, OnActivated() can be manually overwritten, and registration can be done individually for each required message for this recipient.")]
  99. protected virtual void OnActivated()
  100. {
  101. Messenger.RegisterAll(this);
  102. }
  103. /// <summary>
  104. /// Invoked whenever the <see cref="IsActive"/> property is set to <see langword="false"/>.
  105. /// Use this method to unregister from messages and do general cleanup for this instance.
  106. /// </summary>
  107. /// <remarks>
  108. /// The base implementation unregisters all messages for this recipient. It does so by
  109. /// invoking <see cref="IMessenger.UnregisterAll"/>, which removes all registered
  110. /// handlers for a given subscriber, regardless of what token was used to register them.
  111. /// That is, all registered handlers across all subscription channels will be removed.
  112. /// </remarks>
  113. protected virtual void OnDeactivated()
  114. {
  115. Messenger.UnregisterAll(this);
  116. }
  117. /// <summary>
  118. /// Broadcasts a <see cref="PropertyChangedMessage{T}"/> with the specified
  119. /// parameters, without using any particular token (so using the default channel).
  120. /// </summary>
  121. /// <typeparam name="T">The type of the property that changed.</typeparam>
  122. /// <param name="oldValue">The value of the property before it changed.</param>
  123. /// <param name="newValue">The value of the property after it changed.</param>
  124. /// <param name="propertyName">The name of the property that changed.</param>
  125. /// <remarks>
  126. /// You should override this method if you wish to customize the channel being
  127. /// used to send the message (eg. if you need to use a specific token for the channel).
  128. /// </remarks>
  129. protected virtual void Broadcast<T>(T oldValue, T newValue, string? propertyName)
  130. {
  131. PropertyChangedMessage<T> message = new(this, propertyName, oldValue, newValue);
  132. _ = Messenger.Send(message);
  133. }
  134. /// <summary>
  135. /// Compares the current and new values for a given property. If the value has changed,
  136. /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
  137. /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
  138. /// </summary>
  139. /// <typeparam name="T">The type of the property that changed.</typeparam>
  140. /// <param name="field">The field storing the property's value.</param>
  141. /// <param name="newValue">The property's value after the change occurred.</param>
  142. /// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
  143. /// <param name="propertyName">(optional) The name of the property that changed.</param>
  144. /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
  145. /// <remarks>
  146. /// This method is just like <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/>, just with the addition
  147. /// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
  148. /// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
  149. /// are not raised if the current and new value for the target property are the same.
  150. /// </remarks>
  151. protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, bool broadcast, [CallerMemberName] string? propertyName = null)
  152. {
  153. T oldValue = field;
  154. // We duplicate the code as in the base class here to leverage
  155. // the intrinsics support for EqualityComparer<T>.Default.Equals.
  156. bool propertyChanged = SetProperty(ref field, newValue, propertyName);
  157. if (propertyChanged && broadcast)
  158. {
  159. Broadcast(oldValue, newValue, propertyName);
  160. }
  161. return propertyChanged;
  162. }
  163. /// <summary>
  164. /// Compares the current and new values for a given property. If the value has changed,
  165. /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
  166. /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
  167. /// See additional notes about this overload in <see cref="SetProperty{T}(ref T,T,bool,string)"/>.
  168. /// </summary>
  169. /// <typeparam name="T">The type of the property that changed.</typeparam>
  170. /// <param name="field">The field storing the property's value.</param>
  171. /// <param name="newValue">The property's value after the change occurred.</param>
  172. /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
  173. /// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
  174. /// <param name="propertyName">(optional) The name of the property that changed.</param>
  175. /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
  176. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/> is <see langword="null"/>.</exception>
  177. protected bool SetProperty<T>([NotNullIfNotNull(nameof(newValue))] ref T field, T newValue, IEqualityComparer<T> comparer, bool broadcast, [CallerMemberName] string? propertyName = null)
  178. {
  179. ArgumentNullException.ThrowIfNull(comparer);
  180. T oldValue = field;
  181. bool propertyChanged = SetProperty(ref field, newValue, comparer, propertyName);
  182. if (propertyChanged && broadcast)
  183. {
  184. Broadcast(oldValue, newValue, propertyName);
  185. }
  186. return propertyChanged;
  187. }
  188. /// <summary>
  189. /// Compares the current and new values for a given property. If the value has changed,
  190. /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
  191. /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event. Similarly to
  192. /// the <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/> method, this overload should only be
  193. /// used when <see cref="ObservableObject.SetProperty{T}(ref T,T,string)"/> can't be used directly.
  194. /// </summary>
  195. /// <typeparam name="T">The type of the property that changed.</typeparam>
  196. /// <param name="oldValue">The current property value.</param>
  197. /// <param name="newValue">The property's value after the change occurred.</param>
  198. /// <param name="callback">A callback to invoke to update the property value.</param>
  199. /// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
  200. /// <param name="propertyName">(optional) The name of the property that changed.</param>
  201. /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
  202. /// <remarks>
  203. /// This method is just like <see cref="ObservableObject.SetProperty{T}(T,T,Action{T},string)"/>, just with the addition
  204. /// of the <paramref name="broadcast"/> parameter. As such, following the behavior of the base method,
  205. /// the <see cref="ObservableObject.PropertyChanging"/> and <see cref="ObservableObject.PropertyChanged"/> events
  206. /// are not raised if the current and new value for the target property are the same.
  207. /// </remarks>
  208. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="callback"/> is <see langword="null"/>.</exception>
  209. protected bool SetProperty<T>(T oldValue, T newValue, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
  210. {
  211. ArgumentNullException.ThrowIfNull(callback);
  212. bool propertyChanged = SetProperty(oldValue, newValue, callback, propertyName);
  213. if (propertyChanged && broadcast)
  214. {
  215. Broadcast(oldValue, newValue, propertyName);
  216. }
  217. return propertyChanged;
  218. }
  219. /// <summary>
  220. /// Compares the current and new values for a given property. If the value has changed,
  221. /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property with
  222. /// the new value, then raises the <see cref="ObservableObject.PropertyChanged"/> event.
  223. /// See additional notes about this overload in <see cref="SetProperty{T}(T,T,Action{T},bool,string)"/>.
  224. /// </summary>
  225. /// <typeparam name="T">The type of the property that changed.</typeparam>
  226. /// <param name="oldValue">The current property value.</param>
  227. /// <param name="newValue">The property's value after the change occurred.</param>
  228. /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
  229. /// <param name="callback">A callback to invoke to update the property value.</param>
  230. /// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
  231. /// <param name="propertyName">(optional) The name of the property that changed.</param>
  232. /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
  233. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/> or <paramref name="callback"/> are <see langword="null"/>.</exception>
  234. protected bool SetProperty<T>(T oldValue, T newValue, IEqualityComparer<T> comparer, Action<T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
  235. {
  236. ArgumentNullException.ThrowIfNull(comparer);
  237. ArgumentNullException.ThrowIfNull(callback);
  238. bool propertyChanged = SetProperty(oldValue, newValue, comparer, callback, propertyName);
  239. if (propertyChanged && broadcast)
  240. {
  241. Broadcast(oldValue, newValue, propertyName);
  242. }
  243. return propertyChanged;
  244. }
  245. /// <summary>
  246. /// Compares the current and new values for a given nested property. If the value has changed,
  247. /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
  248. /// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
  249. /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>, with the difference being that this
  250. /// method is used to relay properties from a wrapped model in the current instance. For more info, see the docs for
  251. /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,TModel,Action{TModel,T},string)"/>.
  252. /// </summary>
  253. /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
  254. /// <typeparam name="T">The type of property (or field) to set.</typeparam>
  255. /// <param name="oldValue">The current property value.</param>
  256. /// <param name="newValue">The property's value after the change occurred.</param>
  257. /// <param name="model">The model </param>
  258. /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
  259. /// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
  260. /// <param name="propertyName">(optional) The name of the property that changed.</param>
  261. /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
  262. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="model"/> or <paramref name="callback"/> are <see langword="null"/>.</exception>
  263. protected bool SetProperty<TModel, T>(T oldValue, T newValue, TModel model, Action<TModel, T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
  264. where TModel : class
  265. {
  266. ArgumentNullException.ThrowIfNull(model);
  267. ArgumentNullException.ThrowIfNull(callback);
  268. bool propertyChanged = SetProperty(oldValue, newValue, model, callback, propertyName);
  269. if (propertyChanged && broadcast)
  270. {
  271. Broadcast(oldValue, newValue, propertyName);
  272. }
  273. return propertyChanged;
  274. }
  275. /// <summary>
  276. /// Compares the current and new values for a given nested property. If the value has changed,
  277. /// raises the <see cref="ObservableObject.PropertyChanging"/> event, updates the property and then raises the
  278. /// <see cref="ObservableObject.PropertyChanged"/> event. The behavior mirrors that of
  279. /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>,
  280. /// with the difference being that this method is used to relay properties from a wrapped model in the
  281. /// current instance. For more info, see the docs for
  282. /// <see cref="ObservableObject.SetProperty{TModel,T}(T,T,IEqualityComparer{T},TModel,Action{TModel,T},string)"/>.
  283. /// </summary>
  284. /// <typeparam name="TModel">The type of model whose property (or field) to set.</typeparam>
  285. /// <typeparam name="T">The type of property (or field) to set.</typeparam>
  286. /// <param name="oldValue">The current property value.</param>
  287. /// <param name="newValue">The property's value after the change occurred.</param>
  288. /// <param name="comparer">The <see cref="IEqualityComparer{T}"/> instance to use to compare the input values.</param>
  289. /// <param name="model">The model </param>
  290. /// <param name="callback">The callback to invoke to set the target property value, if a change has occurred.</param>
  291. /// <param name="broadcast">If <see langword="true"/>, <see cref="Broadcast{T}"/> will also be invoked.</param>
  292. /// <param name="propertyName">(optional) The name of the property that changed.</param>
  293. /// <returns><see langword="true"/> if the property was changed, <see langword="false"/> otherwise.</returns>
  294. /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="comparer"/>, <paramref name="model"/> or <paramref name="callback"/> are <see langword="null"/>.</exception>
  295. protected bool SetProperty<TModel, T>(T oldValue, T newValue, IEqualityComparer<T> comparer, TModel model, Action<TModel, T> callback, bool broadcast, [CallerMemberName] string? propertyName = null)
  296. where TModel : class
  297. {
  298. ArgumentNullException.ThrowIfNull(comparer);
  299. ArgumentNullException.ThrowIfNull(model);
  300. ArgumentNullException.ThrowIfNull(callback);
  301. bool propertyChanged = SetProperty(oldValue, newValue, comparer, model, callback, propertyName);
  302. if (propertyChanged && broadcast)
  303. {
  304. Broadcast(oldValue, newValue, propertyName);
  305. }
  306. return propertyChanged;
  307. }
  308. }