|
| 1 | +## Message Service |
| 2 | + |
| 3 | +Another service that it's important to know is the one represented by the interface `IMessageService`. The mission of this service is to send messages between ViewModels or Views, and once the subscriber receive the message, react to it executing some action. This pattern is known as the **Event Aggregator Pattern**: |
| 4 | + |
| 5 | + |
| 6 | + |
| 7 | +The `IMessageService` exposes the following methods: |
| 8 | +```csharp |
| 9 | +public interface IMessageService |
| 10 | +{ |
| 11 | + void Subscribe<TSender>(object target, Action<TSender, string, object> action) where TSender : class; |
| 12 | + void Subscribe<TSender, TArgs>(object target, Action<TSender, string, TArgs> action) where TSender : class; |
| 13 | + |
| 14 | + void Unsubscribe(object target); |
| 15 | + void Unsubscribe<TSender>(object target) where TSender : class; |
| 16 | + void Unsubscribe<TSender, TArgs>(object target) where TSender : class; |
| 17 | + |
| 18 | + void Send<TSender, TArgs>(TSender sender, string message, TArgs args) where TSender : class; |
| 19 | +} |
| 20 | +``` |
| 21 | + |
| 22 | +| Methods | Description | |
| 23 | +| ------ | ------ | |
| 24 | +| Subscribe | This is used to subscribe your class to a specific event. When we subscribe to an event, we need to indicate the sender (`TSender`), the target object, and the action to execute when the message is received | |
| 25 | +| Unsubscribe | Unsubscribe an event already registered. It's important to unsubscribe events in order to avoid memory leaks | |
| 26 | +| Send | Communicate to subscriber that a specific event has occurred. When we send a message we are passing the sender (`TSender`), the message that identifies the event occurred (`message`) and additional arguments when necessary | |
| 27 | + |
| 28 | + |
| 29 | +### How to use it |
| 30 | + |
| 31 | +The best thing to understand how it works is to check how it's being used in the *Inventory App*. For example, lets see how the `CustomersViewModel` loads the detail of a Customer, when it is selected from the List of Customers. In this process the following actors are involved: |
| 32 | + |
| 33 | +- `CustomerListViewModel`: When an element of the list is selected, this ViewModel will use the `IMessageService` to send to the Customer selected to the possible subscribers. |
| 34 | + |
| 35 | +```csharp |
| 36 | +private TModel _selectedItem = default(TModel); |
| 37 | +public TModel SelectedItem |
| 38 | +{ |
| 39 | + get => _selectedItem; |
| 40 | + set |
| 41 | + { |
| 42 | + if (Set(ref _selectedItem, value)) |
| 43 | + { |
| 44 | + if (!IsMultipleSelection) |
| 45 | + { |
| 46 | + MessageService.Send(this, "ItemSelected", _selectedItem); |
| 47 | + } |
| 48 | + } |
| 49 | + } |
| 50 | +} |
| 51 | +``` |
| 52 | + |
| 53 | +- `CustomersViewModel`: This ViewModel will *Subcribe* to the Customers list *ItemSelected* event. It will also unsubscribe from this event when necessary. |
| 54 | +```csharp |
| 55 | +public void Subscribe() |
| 56 | +{ |
| 57 | + MessageService.Subscribe<CustomerListViewModel>(this, OnMessage); |
| 58 | + CustomerList.Subscribe(); |
| 59 | + CustomerDetails.Subscribe(); |
| 60 | + CustomerOrders.Subscribe(); |
| 61 | +} |
| 62 | + |
| 63 | +private async void OnMessage(CustomerListViewModel viewModel, string message, object args) |
| 64 | +{ |
| 65 | + if (viewModel == CustomerList && message == "ItemSelected") |
| 66 | + { |
| 67 | + await ContextService.RunAsync(() => |
| 68 | + { |
| 69 | + OnItemSelected(); |
| 70 | + }); |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +private async void OnItemSelected() |
| 75 | +{ |
| 76 | + if (CustomerDetails.IsEditMode) |
| 77 | + { |
| 78 | + StatusReady(); |
| 79 | + CustomerDetails.CancelEdit(); |
| 80 | + } |
| 81 | + CustomerOrders.IsMultipleSelection = false; |
| 82 | + var selected = CustomerList.SelectedItem; |
| 83 | + if (!CustomerList.IsMultipleSelection) |
| 84 | + { |
| 85 | + if (selected != null && !selected.IsEmpty) |
| 86 | + { |
| 87 | + await PopulateDetails(selected); |
| 88 | + await PopulateOrders(selected); |
| 89 | + } |
| 90 | + } |
| 91 | + CustomerDetails.Item = selected; |
| 92 | +} |
| 93 | +``` |
| 94 | + |
| 95 | +We are executing the `OnItemSelected` method when we received from the `CustomerListViewModel` a message `ItemSelected`. |
| 96 | + |
| 97 | +Finally, as we previously mentioned, it's important to control the subscriptions and unsubcriptions of the events to prevent memory leaks. We will do this overriding the events `OnNavigatedTo` and `OnNavigatingFrom`methods of our Views: |
| 98 | +```csharp |
| 99 | +protected override async void OnNavigatedTo(NavigationEventArgs e) |
| 100 | +{ |
| 101 | + ViewModel.Subscribe(); |
| 102 | + await ViewModel.LoadAsync(e.Parameter as CustomerListArgs); |
| 103 | +} |
| 104 | + |
| 105 | +protected override void OnNavigatingFrom(NavigatingCancelEventArgs e) |
| 106 | +{ |
| 107 | + ViewModel.Unload(); |
| 108 | + ViewModel.Unsubscribe(); |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +### Message Service Implementation |
| 113 | + |
| 114 | +The purpouse of this section is to show how an *Event Aggregator Pattern* can be implemented. The key to unsderstand the whole process are the `Subscriber` and `Subscriptions` internal classes. |
| 115 | + |
| 116 | +#### Subscriptions class |
| 117 | + |
| 118 | +This class is just a Diccionary of actions associated with a `Type`. We will use it to store the action to execute when a Subscriber receives a message. |
| 119 | + |
| 120 | +```csharp |
| 121 | +class Subscriptions |
| 122 | +{ |
| 123 | + private Dictionary<Type, Delegate> _subscriptions = null; |
| 124 | + |
| 125 | + public Subscriptions() |
| 126 | + { |
| 127 | + _subscriptions = new Dictionary<Type, Delegate>(); |
| 128 | + } |
| 129 | + |
| 130 | + public bool IsEmpty => _subscriptions.Count == 0; |
| 131 | + |
| 132 | + public void AddSubscription<TSender, TArgs>(Action<TSender, string, TArgs> action) |
| 133 | + { |
| 134 | + _subscriptions.Add(typeof(TArgs), action); |
| 135 | + } |
| 136 | + |
| 137 | + public void RemoveSubscription<TArgs>() |
| 138 | + { |
| 139 | + _subscriptions.Remove(typeof(TArgs)); |
| 140 | + } |
| 141 | + |
| 142 | + public void TryInvoke<TArgs>(object sender, string message, TArgs args) |
| 143 | + { |
| 144 | + var argsType = typeof(TArgs); |
| 145 | + foreach (var keyValue in _subscriptions.Where(r => r.Key.IsAssignableFrom(argsType))) |
| 146 | + { |
| 147 | + var action = keyValue.Value; |
| 148 | + action?.DynamicInvoke(sender, message, args); |
| 149 | + } |
| 150 | + } |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +#### Subscriber class |
| 155 | + |
| 156 | +The Subscriber class is the responsable of: |
| 157 | + |
| 158 | +- Container of the subscriptions defined for a specific `Type`. |
| 159 | +- Create the `WeakReference` between the Publisher and the Subscriber. |
| 160 | +- Unregister subscriptions. |
| 161 | +- Invoke the action to execute when the subscriber receives a message. |
| 162 | + |
| 163 | +```csharp |
| 164 | +class Subscriber |
| 165 | +{ |
| 166 | + private WeakReference _reference = null; |
| 167 | + |
| 168 | + private Dictionary<Type, Subscriptions> _subscriptions; |
| 169 | + |
| 170 | + public Subscriber(object target) |
| 171 | + { |
| 172 | + _reference = new WeakReference(target); |
| 173 | + _subscriptions = new Dictionary<Type, Subscriptions>(); |
| 174 | + } |
| 175 | + |
| 176 | + public object Target => _reference.Target; |
| 177 | + |
| 178 | + public bool IsEmpty => _subscriptions.Count == 0; |
| 179 | + |
| 180 | + public void AddSubscription<TSender, TArgs>(Action<TSender, string, TArgs> action) |
| 181 | + { |
| 182 | + if (!_subscriptions.TryGetValue(typeof(TSender), out Subscriptions subscriptions)) |
| 183 | + { |
| 184 | + subscriptions = new Subscriptions(); |
| 185 | + _subscriptions.Add(typeof(TSender), subscriptions); |
| 186 | + } |
| 187 | + subscriptions.AddSubscription(action); |
| 188 | + } |
| 189 | + |
| 190 | + public void RemoveSubscription<TSender>() |
| 191 | + { |
| 192 | + _subscriptions.Remove(typeof(TSender)); |
| 193 | + } |
| 194 | + public void RemoveSubscription<TSender, TArgs>() |
| 195 | + { |
| 196 | + if (_subscriptions.TryGetValue(typeof(TSender), out Subscriptions subscriptions)) |
| 197 | + { |
| 198 | + subscriptions.RemoveSubscription<TArgs>(); |
| 199 | + if (subscriptions.IsEmpty) |
| 200 | + { |
| 201 | + _subscriptions.Remove(typeof(TSender)); |
| 202 | + } |
| 203 | + } |
| 204 | + } |
| 205 | + |
| 206 | + public void TryInvoke<TArgs>(object sender, string message, TArgs args) |
| 207 | + { |
| 208 | + var target = _reference.Target; |
| 209 | + if (_reference.IsAlive) |
| 210 | + { |
| 211 | + var senderType = sender.GetType(); |
| 212 | + foreach (var keyValue in _subscriptions.Where(r => r.Key.IsAssignableFrom(senderType))) |
| 213 | + { |
| 214 | + var subscriptions = keyValue.Value; |
| 215 | + subscriptions.TryInvoke(sender, message, args); |
| 216 | + } |
| 217 | + } |
| 218 | + } |
| 219 | +} |
| 220 | +``` |
| 221 | + |
| 222 | +#### IMessageService implementation |
| 223 | + |
| 224 | +Once we understand how the internal classes `Subscriptions` and `Subscriber` work, the implementation of the `IMessageService` becomes much simplier. These are the main three methods of `IMessageService`: |
| 225 | + |
| 226 | +- **Subscribe**: We are just simply creating a new `Subscriber` in case it wasn't already defined, and associate an `Action` to be executed. |
| 227 | +```csharp |
| 228 | +public void Subscribe<TSender, TArgs>(object target, Action<TSender, string, TArgs> action) where TSender : class |
| 229 | +{ |
| 230 | + if (target == null) |
| 231 | + throw new ArgumentNullException(nameof(target)); |
| 232 | + if (action == null) |
| 233 | + throw new ArgumentNullException(nameof(action)); |
| 234 | + |
| 235 | + lock (_sync) |
| 236 | + { |
| 237 | + var subscriber = _subscribers.Where(r => r.Target == target).FirstOrDefault(); |
| 238 | + if (subscriber == null) |
| 239 | + { |
| 240 | + subscriber = new Subscriber(target); |
| 241 | + _subscribers.Add(subscriber); |
| 242 | + } |
| 243 | + subscriber.AddSubscription<TSender, TArgs>(action); |
| 244 | + } |
| 245 | +} |
| 246 | +``` |
| 247 | + |
| 248 | +- **Unsubscribe**: Just remove a subscription for the collection. |
| 249 | +```csharp |
| 250 | +public void Unsubscribe(object target) |
| 251 | +{ |
| 252 | + if (target == null) |
| 253 | + throw new ArgumentNullException(nameof(target)); |
| 254 | + |
| 255 | + lock (_sync) |
| 256 | + { |
| 257 | + var subscriber = _subscribers.Where(r => r.Target == target).FirstOrDefault(); |
| 258 | + if (subscriber != null) |
| 259 | + { |
| 260 | + _subscribers.Remove(subscriber); |
| 261 | + } |
| 262 | + } |
| 263 | +} |
| 264 | +``` |
| 265 | + |
| 266 | +- **Send**: Execute the actions associated to Subscribers when a specific event occurs: |
| 267 | + |
| 268 | +```csharp |
| 269 | +public void Send<TSender, TArgs>(TSender sender, string message, TArgs args) where TSender : class |
| 270 | +{ |
| 271 | + if (sender == null) |
| 272 | + throw new ArgumentNullException(nameof(sender)); |
| 273 | + |
| 274 | + foreach (var subscriber in GetSubscribersSnapshot()) |
| 275 | + { |
| 276 | + // Avoid sending message to self |
| 277 | + if (subscriber.Target != sender) |
| 278 | + { |
| 279 | + subscriber.TryInvoke(sender, message, args); |
| 280 | + } |
| 281 | + } |
| 282 | +} |
| 283 | +``` |
0 commit comments