Skip to content

Commit 4c44214

Browse files
committed
Added draft PuttingThingsTogether.md file
1 parent 2c5d3f9 commit 4c44214

File tree

1 file changed

+342
-0
lines changed

1 file changed

+342
-0
lines changed

docs/mvvm/PuttingThingsTogether.md

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
---
2+
title: PuttingThingsTogether
3+
author: Sergio0694
4+
description: An overview of how to combine different features of the MVVM Toolkit into a practical example
5+
keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, service, messenger, messaging, net core, net standard
6+
dev_langs:
7+
- csharp
8+
---
9+
10+
# Putting things together
11+
12+
Now that we've outline all the different components that are available through the `Microsoft.Toolkit.Mvvm` package, we can look at a practical example of them all coming together to build a single, larger example. In this case, we want to build a very simple and minimalistic Reddit browser for a select number of subreddits.
13+
14+
## What do we want to build
15+
16+
Let's start by outlining exactly what we want to build:
17+
18+
- A minimal Reddit browser made up of two "widgets": one showing posts from a subreddit, and the other one showing the currently selected post. The two widget need to be self contained and without strong references to one another.
19+
- We want users to be able to select a subreddit from a list of available options, and we want to save the selected subreddit as a setting and load it up the next time the sample is loaded.
20+
- We want the subreddit widget to also offer a refresh button to reload the current subreddit.
21+
- For the purposes of this sample, we don't need to be able to handle all the possible post types. We'll just assign a sample text to all loaded posts and display that directly, to make things simpler.
22+
23+
## Setting up the viewmodels
24+
25+
Let's start with the viewmodel that will power the subreddit widget and let's go over the tools we need:
26+
27+
- **Commands:** we need the view to be able to request the viewmodel to reload the current list of posts from the selected subreddit. We can use the `AsyncRelayCommand` type to wrap a private method that will fetch the posts from Reddit. Here we're exposing the command through the `IAsyncRelayCommand` interface, to avoid strong references to the exact command type we're using. This will also allow us to potentially change the command type in the future without having to worry about any UI component relying on that specific type being used.
28+
- **Properties:** we need to expose a number of values to the UI, which we can do with either observable properties if they're values we intend to completely replace, or with properties that are themselves observable (eg. `ObservableCollection<T>`). In this case, we have:
29+
- `ObservableCollection<object> Posts`, which is the observable list of loaded posts. Here we're just using `object` as a placeholder, as we haven't created a model to represent posts yet. We can replace this later on.
30+
- `IReadOnlyList<string> Subreddits`, which is a readonly list with the names of the subreddits that we allow users to choose from. This property is never updated, so it doesn't need to be observable either.
31+
- `string SelectedSubreddit`, which is the currently selected subreddit. This property needs to be bound to the UI, as it'll be used both to indicate the last selected subreddit when the sample is loaded, and to be manipulated directly from the UI as the user changes the selection. Here we're using the `SetProperty` method from the `ObservableObject` class.
32+
- `object SelectedPost`, which is the currently selected post. In this case we're using the `SetProperty` method from the `ObservableRecipient` class to indicate that we also want to broadcast notifications when this property changes. This is done to be able to notify the post widget that the current post selection is changed.
33+
- **Methods:** we just need a private `LoadPostsAsync` method which will be wrapped by our async command, and which will contain the logic to load posts from the selected subreddit.
34+
35+
Here's the viewmodel so far:
36+
37+
```csharp
38+
public sealed class SubredditWidgetViewModel : ObservableRecipient
39+
{
40+
/// <summary>
41+
/// Creates a new <see cref="SubredditWidgetViewModel"/> instance.
42+
/// </summary>
43+
public SubredditWidgetViewModel()
44+
{
45+
LoadPostsCommand = new AsyncRelayCommand(LoadPostsAsync);
46+
}
47+
48+
/// <summary>
49+
/// Gets the <see cref="IAsyncRelayCommand"/> instance responsible for loading posts.
50+
/// </summary>
51+
public IAsyncRelayCommand LoadPostsCommand { get; }
52+
53+
/// <summary>
54+
/// Gets the collection of loaded posts.
55+
/// </summary>
56+
public ObservableCollection<object> Posts { get; } = new ObservableCollection<object>();
57+
58+
/// <summary>
59+
/// Gets the collection of available subreddits to pick from.
60+
/// </summary>
61+
public IReadOnlyList<string> Subreddits { get; } = new[]
62+
{
63+
"microsoft",
64+
"windows",
65+
"surface",
66+
"windowsphone",
67+
"dotnet",
68+
"csharp"
69+
};
70+
71+
private string selectedSubreddit;
72+
73+
/// <summary>
74+
/// Gets or sets the currently selected subreddit.
75+
/// </summary>
76+
public string SelectedSubreddit
77+
{
78+
get => selectedSubreddit;
79+
set => SetProperty(ref selectedSubreddit, value);
80+
}
81+
82+
private object selectedPost;
83+
84+
/// <summary>
85+
/// Gets or sets the currently selected subreddit.
86+
/// </summary>
87+
public object SelectedPost
88+
{
89+
get => selectedPost;
90+
set => SetProperty(ref selectedPost, value, true);
91+
}
92+
93+
/// <summary>
94+
/// Loads the posts from a specified subreddit.
95+
/// </summary>
96+
private async Task LoadPostsAsync()
97+
{
98+
// TODO...
99+
}
100+
}
101+
```
102+
103+
Now let's take a look at what we need for viewmodel of the post widget. This will be a much simpler viewmodel, as it really only needs to expose a `Post` property with the currently selected post, and to receive broadcast messages from the subreddit widget to update the `Post` property. It can look something like this:
104+
105+
```csharp
106+
public sealed class PostWidgetViewModel : ObservableRecipient, IRecipient<PropertyChangedMessage<object>>
107+
{
108+
private object post;
109+
110+
/// <summary>
111+
/// Gets the currently selected post, if any.
112+
/// </summary>
113+
public object Post
114+
{
115+
get => post;
116+
private set => SetProperty(ref post, value);
117+
}
118+
119+
/// <inheritdoc/>
120+
public void Receive(PropertyChangedMessage<object> message)
121+
{
122+
if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) &&
123+
message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost))
124+
{
125+
Post = message.NewValue;
126+
}
127+
}
128+
}
129+
```
130+
131+
In this case, we're using the `IRecipient<TMessage>` interface to declare the messages we want our viewmodel to receive. The handlers for the declared messages will be added automatically by the `ObservableRecipient` class when the `IsActive` property is set to `true`. Note that it is not mandatory to use this approach, and manually registering each message handler is also possible, like so:
132+
133+
```csharp
134+
public sealed class PostWidgetViewModel : ObservableRecipient
135+
{
136+
protected override void OnActivated()
137+
{
138+
// We use a method group here, but a lambda expression is also valid
139+
Messenger.Register<PropertyChangedMessage<object>>(this, Receive);
140+
}
141+
142+
/// <inheritdoc/>
143+
public void Receive(PropertyChangedMessage<object> message)
144+
{
145+
if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) &&
146+
message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost))
147+
{
148+
Post = message.NewValue;
149+
}
150+
}
151+
}
152+
```
153+
154+
We now have a draft of our viewmodels ready, and we can start looking into the services we need.
155+
156+
## Building the settings service
157+
158+
Since we want some of our properties to be saved and persisted, we need a way for viewmodels to be able to interact with the application settings. We shouldn't use platform-specific APIs directly in our viewmodels though, as that would prevent us from having all our viewmodels in a portable, .NET Standard project. We can solve this issue by using services, and the `Ioc` class. The idea is to write interfaces that represent all the API surface that we need, and then to implement platform-specific types implementing this interface on all our application targets. The viewmodels will only interact with the interfaces, so they will not have any strong reference to any platform-specific type at all.
159+
160+
Here's a simple interface for a settings service:
161+
162+
```csharp
163+
public interface ISettingsService
164+
{
165+
/// <summary>
166+
/// Assigns a value to a settings key.
167+
/// </summary>
168+
/// <typeparam name="T">The type of the object bound to the key.</typeparam>
169+
/// <param name="key">The key to check.</param>
170+
/// <param name="value">The value to assign to the setting key.</param>
171+
void SetValue<T>(string key, T value);
172+
173+
/// <summary>
174+
/// Reads a value from the current <see cref="IServiceProvider"/> instance and returns its casting in the right type.
175+
/// </summary>
176+
/// <typeparam name="T">The type of the object to retrieve.</typeparam>
177+
/// <param name="key">The key associated to the requested object.</param>
178+
[Pure]
179+
T GetValue<T>(string key);
180+
}
181+
```
182+
183+
We can assume that platform-specific types implementing this interface will take care of dealing with all the logic necessary to actually serialize the settings, store them to disk and then read them back. We can now use this service in our `SubredditWidgetViewModel`, in order to make the `SelectedSubreddit` property persistent:
184+
185+
```csharp
186+
/// <summary>
187+
/// Gets the <see cref="ISettingsService"/> instance to use.
188+
/// </summary>
189+
private readonly ISettingsService SettingsService = Ioc.Default.GetRequiredService<ISettingsService>();
190+
191+
/// <summary>
192+
/// Creates a new <see cref="SubredditWidgetViewModel"/> instance.
193+
/// </summary>
194+
public SubredditWidgetViewModel()
195+
{
196+
selectedSubreddit = SettingsService.GetValue<string>(nameof(SelectedSubreddit)) ?? Subreddits[0];
197+
}
198+
199+
private string selectedSubreddit;
200+
201+
/// <summary>
202+
/// Gets or sets the currently selected subreddit.
203+
/// </summary>
204+
public string SelectedSubreddit
205+
{
206+
get => selectedSubreddit;
207+
set
208+
{
209+
SetProperty(ref selectedSubreddit, value);
210+
211+
SettingsService.SetValue(nameof(SelectedSubreddit), value);
212+
}
213+
}
214+
```
215+
216+
Here we're using the service locator pattern, which is one of the supported patterns by the `Ioc` class. We've declared an `ISettingsService SettingsService` field that just stores our settings service (we're retrieving it from the static `Ioc.Default` instance), and then we're initializing the `SelectedSubreddit` property in the constructor, by either using the previous value or just the first available subreddit. Then we also modified the `SelectedSubreddit` setter, so that it will also use the settings service to save the new value to disk.
217+
218+
Great! Now we just need to write a platform specific version of this service, this time directly inside one of our app projects. Here's what that service might look like on UWP:
219+
220+
```csharp
221+
public sealed class SettingsService : ISettingsService
222+
{
223+
/// <summary>
224+
/// The <see cref="IPropertySet"/> with the settings targeted by the current instance.
225+
/// </summary>
226+
private readonly IPropertySet SettingsStorage = ApplicationData.Current.LocalSettings.Values;
227+
228+
/// <inheritdoc/>
229+
public void SetValue<T>(string key, T value)
230+
{
231+
if (!SettingsStorage.ContainsKey(key)) SettingsStorage.Add(key, value);
232+
else SettingsStorage[key] = value;
233+
}
234+
235+
/// <inheritdoc/>
236+
public T GetValue<T>(string key)
237+
{
238+
if (SettingsStorage.TryGetValue(key, out object value))
239+
{
240+
return (T)value;
241+
}
242+
243+
return default;
244+
}
245+
}
246+
```
247+
248+
The final piece of the puzzle is to inject this platform-specific service into our service provider instance, which in this case is the `Ioc.Default` instance. We can do this at startup, like so:
249+
250+
```csharp
251+
Ioc.Default.ConfigureServices(services =>
252+
{
253+
services.AddSingleton<ISettingsService, SettingsService>();
254+
});
255+
```
256+
257+
This will register a singleton instance of our `SettingsService` as a type implementing `ISettingsService`. This means that every time one of our viewmodels uses `Ioc.Default.GetService<ISettingsService>()` while the app in use is the UWP one, it will receive a `SettingsService` instance, which will use the UWP APIs behind the scene to manipulate settings. Perfect!
258+
259+
## Building the UI
260+
261+
Now that all the backend is completed, we can write the UI for our widgets. Note how using the MVVM pattern let us focus exclusively on the business logic at first, without having to write any UI-related code until now. Here we'll remove all the UI code that's not interacting with our viewmodels, for simplicity, and we'll go through each different control one by one. The full source code can be found in the sample app.
262+
263+
Let's start with the subreddit widget, which features a `ComboBox` to select a subreddit, a `Button` to refresh the feed, a `ListView` to display posts and a `ProgressBar` to indicate when the feed is loading. We'll assume that the `ViewModel` property represents an instance of the viewmodel we've described before - this can be declared either in XAML or directly in code behind.
264+
265+
### Subreddit selector:
266+
267+
```xml
268+
<ComboBox
269+
ItemsSource="{x:Bind ViewModel.Subreddits}"
270+
SelectedItem="{x:Bind ViewModel.SelectedSubreddit, Mode=TwoWay}">
271+
<interactivity:Interaction.Behaviors>
272+
<core:EventTriggerBehavior EventName="SelectionChanged">
273+
<core:InvokeCommandAction Command="{x:Bind ViewModel.LoadPostsCommand}"/>
274+
</core:EventTriggerBehavior>
275+
</interactivity:Interaction.Behaviors>
276+
</ComboBox>
277+
```
278+
279+
Here we're binding the source to the `Subreddits` property, and the selected item to the `SelectedSubreddit` property. Note how the `Subreddits` property is only bound once, as the collection itself sends change notifications, while the `SelectedSubreddit` property is bound with the `TwoWay` mode, as we need it both to be able to load the value we retrieve from our settings, as well as updating the property in the viewmodel when the user changes the selection. Additionally, we're using a XAML behavior to invoke our command whenever the selection changes.
280+
281+
### Refresh button:
282+
283+
```xml
284+
<Button Command="{x:Bind ViewModel.LoadPostsCommand}"/>
285+
```
286+
287+
This component is extremely simple, we're just binding our custom command to the `Command` property of the button, so that the command will be invoked whenever the user clicks on it.
288+
289+
### Posts list:
290+
291+
```xml
292+
<ListView
293+
ItemsSource="{x:Bind ViewModel.Posts}"
294+
SelectedItem="{x:Bind ViewModel.SelectedPost, Mode=TwoWay}">
295+
<ListView.ItemTemplate>
296+
<DataTemplate x:DataType="models:Post">
297+
<Grid>
298+
<TextBlock Text="{x:Bind Title}"/>
299+
<controls:ImageEx Source="{x:Bind Thumbnail}"/>
300+
</Grid>
301+
</DataTemplate>
302+
</ListView.ItemTemplate>
303+
</ListView>
304+
```
305+
306+
Here we have a `ListView` binding the source and selection to our viewmodel property, and also a template used to display each post that is available. We're using `x:DataType` to enable `x:Bind` in our template, and we have two controls binding directly to the `Title` and `Thumbnail` properties of our post.
307+
308+
### Loading bar:
309+
310+
```xml
311+
<ProgressBar Visibility="{x:Bind ViewModel.LoadPostsCommand.IsRunning, Mode=OneWay}"/>
312+
```
313+
314+
Here we're binding to the `IsRunning` property, which is part of the `IAsyncRelayCommand` interface. The `AsyncRelayCommand` type will take care of raising notifications for that property whenever the asynchronous operation starts or completes for that command.
315+
316+
---
317+
318+
The last missing piece is the UI for the post widget. As before, we've removed all the UI-related code that was not necessary to interact with the viewmodels, for simplicity. The full source code is available in the sample app.
319+
320+
```xml
321+
<Grid>
322+
323+
<!--Header-->
324+
<Grid>
325+
<TextBlock Text="{x:Bind ViewModel.Post.Title, Mode=OneWay}"/>
326+
<controls:ImageEx Source="{x:Bind ViewModel.Post.Thumbnail, Mode=OneWay}"/>
327+
</Grid>
328+
329+
<!--Content-->
330+
<ScrollViewer>
331+
<TextBlock Text="{x:Bind ViewModel.Post.SelfText, Mode=OneWay}"/>
332+
</ScrollViewer>
333+
</Grid>
334+
```
335+
336+
Here we just have a header, with a `TextBlock` and an `ImageEx` control binding their `Text` and `Source` properties to the respective properties in our `Post` model, and a simple `TextBlock` inside a `ScrollViewer` that is used to display the (sample) content of the selected post.
337+
338+
## Good to go! 🚀
339+
340+
We've now built all our viewmodels, the necessary services, and the UI for our widgets - our simple Reddit browser is completed! This was just meant to be an example of how to build an app following the MVVM pattern and using the APIs from the MVVM Toolkit.
341+
342+
As stated above, this is only a reference, and you're free to modify this structure to fit your needs and/or to pick and choose only a subset of components from the library. Regardless of the approach you take, the MVVM Toolkit should provide a solid foundation to hit the ground running when starting a new application, by letting you focus on your business logic instead of having to worry about manually doing all the necessary plumbing to enable proper support for the MVVM pattern.

0 commit comments

Comments
 (0)