Skip to content

Commit 71d1bda

Browse files
committed
Merge branch 'dev' of https://github.com/Microsoft/InventorySample into dev
2 parents d2e077e + 1afe0e1 commit 71d1bda

4 files changed

Lines changed: 375 additions & 1 deletion

File tree

docs/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,13 @@
4141
- [MainShellView](chapters/architecture/navigation-app.md#mainshellview)
4242
- [ShellView](chapters/architecture/navigation-app.md#shellview)
4343
- [Frame Navigation](chapters/architecture/navigation-app.md#frame-navigation)
44+
- [Saving the state of the view when Navigate](chapters/architecture/navigation-app.md#Saving-the-state-of-the-view-when-Navigate)
4445
- [Data Access](chapters/architecture/dataaccess.md#data-access)
4546
- [Inventory.Data project](chapters/architecture/dataaccess.md#inventory.data-project)
4647
- [Accessing the data from the app](chapters/architecture/dataaccess.md#accessing-the-data-from-the-app)
48+
- [Message Service](chapters/architecture/message-service.md#message-service)
49+
- [How to use it](chapters/architecture/message-service.md#How-to-use-it)
50+
- [Message Service Implementation](chapters/architecture/message-service.md#Message-Service-Implementation)
4751
- Localization
4852
- ...
4953
- [Data Access](chapters/dataaccess.md)
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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+
![event aggregator](../img/eventaggregator.png)
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+
```

docs/chapters/architecture/navigation-app.md

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,91 @@ The difference with the main shell is the lack of a `NavigationView` control. Th
7777

7878
It's important to have clear the Shells that we are going to display in the app in order to understand how the *Navigation* of the app works.
7979

80-
### Frame Navigation
80+
### Frame Navigation
81+
82+
The *Main Frame* defined in the prevoius Shells is the reponsable for the navigation in the app. We can check how the Frame control is used for navigate [here](navigation-service.md#INavigationService-implementation).
83+
84+
When the MainShellView or the ShellView is loaded the Navigation Service has to be initialiaze in order to use the Frame as navigation control:
85+
86+
```csharp
87+
private void InitializeNavigation()
88+
{
89+
_navigationService = ServiceLocator.Current.GetService<INavigationService>();
90+
_navigationService.Initialize(frame);
91+
frame.Navigated += OnFrameNavigated;
92+
CurrentView.BackRequested += OnBackRequested;
93+
}
94+
```
95+
96+
Once the Navigation Service is initialized from the Shell views. We are ready to navigate using the `INavigationService` in our ViewModels to navigate.
97+
98+
### Saving the state of the view when Navigate
99+
100+
As you may know, in UWP apps the pages are recreated everytime we navigate to them, unless we want to preserve them in memory setting the property `NavigationCacheMode` to `Required` or `Enabled`.
101+
102+
With the purpose of having the best performance we can, we have decided not to *cache* the pages and save the state of them in the Navigation process. This page state will be passed as an argument when we navigate to a page, and retreived when we navigate back.
103+
104+
So, the first thing to do is defined an argument per ViewModel. Let's take for example the `CustomerListViewModel`. We will have a class defined for the arguments of this `CustomerListArgs`:
105+
106+
```csharp
107+
public class CustomerListArgs
108+
{
109+
static public CustomerListArgs CreateEmpty() => new CustomerListArgs { IsEmpty = true };
110+
111+
public CustomerListArgs()
112+
{
113+
OrderBy = r => r.FirstName;
114+
}
115+
116+
public bool IsEmpty { get; set; }
117+
118+
public string Query { get; set; }
119+
120+
public Expression<Func<Customer, object>> OrderBy { get; set; }
121+
public Expression<Func<Customer, object>> OrderByDesc { get; set; }
122+
}
123+
```
124+
125+
As we can see, we are saving the possible values of the actions that the user can do over a list.
126+
127+
#### Passing the state as an argument in the navigation
128+
129+
If we have a look at the `Navigate` method implemented of the *Navigation Service*, we have a nullable `parameter` in addition of the type of the ViewModel we want to navigate to.
130+
```csharp
131+
public bool Navigate(Type viewModelType, object parameter = null)
132+
{
133+
if (Frame == null)
134+
{
135+
throw new InvalidOperationException("Navigation frame not initialized.");
136+
}
137+
return Frame.Navigate(GetView(viewModelType), parameter);
138+
}
139+
```
140+
141+
We will pass the in this `parameter` the `CustomerListArgs` previously defined:
142+
```csharp
143+
NavigationService.Navigate(viewModel, new CustomerListArgs());
144+
```
145+
146+
Now we need to reflect the passed state in the navigation into the View. To accomplish that, we need to overrride the View method `OnNavigatedTo` and get the state in the `Parameter` property of the `NavigationEventArgs` received:
147+
```csharp
148+
protected override async void OnNavigatedTo(NavigationEventArgs e)
149+
{
150+
ViewModel.Subscribe();
151+
await ViewModel.LoadAsync(e.Parameter as CustomerListArgs);
152+
}
153+
```
154+
155+
For those ViewModels we want to save the state, we are creating a public method in them to be called from our Views, the `LoadAsync` method. This method, just simple receive the status saved in the `CustomerListArgs` object, and load it in the ViewModel:
156+
```csharp
157+
public async Task LoadAsync(CustomerListArgs args)
158+
{
159+
ViewModelArgs = args ?? CustomerListArgs.CreateEmpty();
160+
Query = args.Query;
161+
162+
StartStatusMessage("Loading customers...");
163+
await RefreshAsync();
164+
EndStatusMessage("Customers loaded");
165+
}
166+
```
167+
2.62 KB
Loading

0 commit comments

Comments
 (0)