diff --git a/.gitignore b/.gitignore index dfcfd56..e230a34 100644 --- a/.gitignore +++ b/.gitignore @@ -348,3 +348,4 @@ MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ +*.editorconfig diff --git a/samples/MvvmSample.Core/Helpers/MarkdownHelper.cs b/samples/MvvmSample.Core/Helpers/MarkdownHelper.cs new file mode 100644 index 0000000..145868c --- /dev/null +++ b/samples/MvvmSample.Core/Helpers/MarkdownHelper.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; + +namespace MvvmSample.Helpers +{ + /// + /// A simple class to help with basic operations on markdown documents. + /// + public static class MarkdownHelper + { + /// + /// Gets all the paragraphs in a given markdown document. + /// + /// The input markdown document. + /// The raw paragraphs from . + public static IReadOnlyDictionary GetParagraphs(string text) + { + return + Regex.Matches(text, @"(#+ ([^\n]+)[^#]+)", RegexOptions.Singleline) + .OfType() + .ToDictionary( + m => m.Groups[2].Value.Trim().Replace("<", "<"), + m => m.Groups[1].Value.Trim().Replace("<", "<").Replace("[!WARNING]", "**WARNING:**")); + } + } +} diff --git a/samples/MvvmSample.Core/Models/Post.cs b/samples/MvvmSample.Core/Models/Post.cs new file mode 100644 index 0000000..b1cf219 --- /dev/null +++ b/samples/MvvmSample.Core/Models/Post.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace MvvmSample.Models +{ + /// + /// A class for a query for posts in a given subreddit. + /// + public class PostsQueryResponse + { + /// + /// Gets or sets the listing data for the response. + /// + [JsonProperty("data")] + public PostListing Data { get; set; } + } + + /// + /// A class for a Reddit listing of posts. + /// + public class PostListing + { + /// + /// Gets or sets the items in this listing. + /// + [JsonProperty("children")] + public IList Items { get; set; } + } + + /// + /// A wrapping class for a post. + /// + public class PostData + { + /// + /// Gets or sets the instance. + /// + [JsonProperty("data")] + public Post Data { get; set; } + } + + /// + /// A simple model for a Reddit post. + /// + public class Post + { + /// + /// Gets or sets the title of the post. + /// + [JsonProperty("title")] + public string Title { get; set; } + + /// + /// Gets or sets the URL to the post thumbnail, if present. + /// + [JsonProperty("thumbnail")] + public string Thumbnail { get; set; } + + /// + /// Gets the text of the post. + /// + /// + /// Here we're just hardcoding some sample text to simplify how posts are displayed. + /// Normally, not all posts have a self text post available. + /// + [JsonIgnore] + public string SelfText { get; } = string.Join(" ", Enumerable.Repeat( +@"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. +Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 20)); + } +} diff --git a/samples/MvvmSample.Core/MvvmSample.Core.csproj b/samples/MvvmSample.Core/MvvmSample.Core.csproj new file mode 100644 index 0000000..ebd20a7 --- /dev/null +++ b/samples/MvvmSample.Core/MvvmSample.Core.csproj @@ -0,0 +1,13 @@ + + + + netstandard2.0 + 8.0 + + + + + + + + diff --git a/samples/MvvmSample.Core/Services/IFileService.cs b/samples/MvvmSample.Core/Services/IFileService.cs new file mode 100644 index 0000000..da2ff59 --- /dev/null +++ b/samples/MvvmSample.Core/Services/IFileService.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.IO; +using System.Threading.Tasks; + +namespace MvvmSample.Services +{ + /// + /// The default for a service that handles files. + /// + public interface IFilesService + { + /// + /// Gets the path of the installation directory. + /// + string InstallationPath { get; } + + /// + /// Gets a readonly for a file at a specified path. + /// + /// The path of the file to retrieve. + /// The for the specified file. + Task OpenForReadAsync(string path); + } +} diff --git a/samples/MvvmSample.Core/Services/IRedditService.cs b/samples/MvvmSample.Core/Services/IRedditService.cs new file mode 100644 index 0000000..764938e --- /dev/null +++ b/samples/MvvmSample.Core/Services/IRedditService.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using MvvmSample.Models; +using Refit; + +namespace MvvmSample.Services +{ + /// + /// An interface for a simple Reddit service. + /// + public interface IRedditService + { + /// + /// Get a list of posts from a given subreddit + /// + /// The subreddit name. + [Get("/r/{subreddit}/.json")] + Task GetSubredditPostsAsync(string subreddit); + } +} diff --git a/samples/MvvmSample.Core/Services/ISettingsService.cs b/samples/MvvmSample.Core/Services/ISettingsService.cs new file mode 100644 index 0000000..688e256 --- /dev/null +++ b/samples/MvvmSample.Core/Services/ISettingsService.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.Contracts; + +namespace MvvmSample.Services +{ + /// + /// The default for the settings manager used in the app. + /// + public interface ISettingsService + { + /// + /// Assigns a value to a settings key. + /// + /// The type of the object bound to the key. + /// The key to check. + /// The value to assign to the setting key. + void SetValue(string key, T value); + + /// + /// Reads a value from the current instance and returns its casting in the right type. + /// + /// The type of the object to retrieve. + /// The key associated to the requested object. + [Pure] + T GetValue(string key); + } +} diff --git a/samples/MvvmSample.Core/ViewModels/AsyncRelayCommandPageViewModel.cs b/samples/MvvmSample.Core/ViewModels/AsyncRelayCommandPageViewModel.cs new file mode 100644 index 0000000..afa2d0a --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/AsyncRelayCommandPageViewModel.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; +using Microsoft.Toolkit.Mvvm.Input; + +namespace MvvmSample.ViewModels +{ + public class AsyncRelayCommandPageViewModel : SamplePageViewModel + { + public AsyncRelayCommandPageViewModel() + { + DownloadTextCommand = new AsyncRelayCommand(DownloadTextAsync); + } + + public IAsyncRelayCommand DownloadTextCommand { get; } + + private async Task DownloadTextAsync() + { + await Task.Delay(3000); // Simulate a web request + + return "Hello world!"; + } + } +} diff --git a/samples/MvvmSample.Core/ViewModels/IocPageViewModel.cs b/samples/MvvmSample.Core/ViewModels/IocPageViewModel.cs new file mode 100644 index 0000000..353716a --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/IocPageViewModel.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MvvmSample.ViewModels +{ + public class IocPageViewModel : SamplePageViewModel + { + } +} diff --git a/samples/MvvmSample.Core/ViewModels/MessengerPageViewModel.cs b/samples/MvvmSample.Core/ViewModels/MessengerPageViewModel.cs new file mode 100644 index 0000000..4cf0398 --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/MessengerPageViewModel.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.Toolkit.Mvvm.Messaging.Messages; + +namespace MvvmSample.ViewModels +{ + public class MessengerPageViewModel : SamplePageViewModel + { + public UserSenderViewModel SenderViewModel { get; } = new UserSenderViewModel(); + + public UserReceiverViewModel ReceiverViewModel { get; } = new UserReceiverViewModel(); + + // Simple viewmodel for a module sending a username message + public class UserSenderViewModel : ObservableRecipient + { + private string username = "Bob"; + + public string Username + { + get => username; + private set => SetProperty(ref username, value); + } + + protected override void OnActivated() + { + Messenger.Register(this, (r, m) => m.Reply(Username)); + } + + public void SendUserMessage() + { + Username = Username == "Bob" ? "Alice" : "Bob"; + + Messenger.Send(new UsernameChangedMessage(Username)); + } + } + + // Simple viewmodel for a module receiving a username message + public class UserReceiverViewModel : ObservableRecipient + { + private string username = ""; + + public string Username + { + get => username; + private set => SetProperty(ref username, value); + } + + protected override void OnActivated() + { + Messenger.Register(this, (r, m) => Username = m.Value); + } + } + + private string username; + + public string Username + { + get => username; + private set => SetProperty(ref username, value); + } + + public void RequestCurrentUsername() + { + Username = WeakReferenceMessenger.Default.Send(); + } + + public void ResetCurrentUsername() + { + Username = null; + } + + // A sample message with a username value + public sealed class UsernameChangedMessage : ValueChangedMessage + { + public UsernameChangedMessage(string value) : base(value) + { + } + } + + // A sample request message to get the current username + public sealed class CurrentUsernameRequestMessage : RequestMessage + { + } + } +} diff --git a/samples/MvvmSample.Core/ViewModels/ObservableObjectPageViewModel.cs b/samples/MvvmSample.Core/ViewModels/ObservableObjectPageViewModel.cs new file mode 100644 index 0000000..8ca26fa --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/ObservableObjectPageViewModel.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading.Tasks; + +namespace MvvmSample.ViewModels +{ + public class ObservableObjectPageViewModel : SamplePageViewModel + { + private string name; + + /// + /// Gets or sets the name to display. + /// + public string Name + { + get => name; + set => SetProperty(ref name, value); + } + + private TaskNotifier myTask; + + /// + /// Gets or sets the name to display. + /// + public Task MyTask + { + get => myTask; + private set => SetPropertyAndNotifyOnCompletion(ref myTask, value); + } + + /// + /// Simulates an asynchronous method. + /// + public void ReloadTask() + { + MyTask = Task.Delay(3000); + } + } +} diff --git a/samples/MvvmSample.Core/ViewModels/RelayCommandPageViewModel.cs b/samples/MvvmSample.Core/ViewModels/RelayCommandPageViewModel.cs new file mode 100644 index 0000000..b4fccf5 --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/RelayCommandPageViewModel.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Windows.Input; +using Microsoft.Toolkit.Mvvm.Input; + +namespace MvvmSample.ViewModels +{ + public class RelayCommandPageViewModel : SamplePageViewModel + { + public RelayCommandPageViewModel() + { + IncrementCounterCommand = new RelayCommand(IncrementCounter); + } + + /// + /// Gets the responsible for incrementing . + /// + public ICommand IncrementCounterCommand { get; } + + private int counter; + + /// + /// Gets the current value of the counter. + /// + public int Counter + { + get => counter; + private set => SetProperty(ref counter, value); + } + + /// + /// Increments . + /// + private void IncrementCounter() => Counter++; + + } +} diff --git a/samples/MvvmSample.Core/ViewModels/SamplePageViewModel.cs b/samples/MvvmSample.Core/ViewModels/SamplePageViewModel.cs new file mode 100644 index 0000000..f6da8e8 --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/SamplePageViewModel.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.DependencyInjection; +using Microsoft.Toolkit.Mvvm.Input; +using MvvmSample.Helpers; +using MvvmSample.Services; + +namespace MvvmSample.ViewModels +{ + /// + /// A base class for viewmodels for sample pages in the app. + /// + public class SamplePageViewModel : ObservableObject + { + private IReadOnlyDictionary texts; + /// + /// The instance currently in use. + /// + private readonly IFilesService FilesServices = Ioc.Default.GetRequiredService(); + + public SamplePageViewModel() + { + LoadDocsCommand = new AsyncRelayCommand(LoadDocsAsync); + } + + /// + /// Gets the responsible for loading the source markdown docs. + /// + public IAsyncRelayCommand LoadDocsCommand { get; } + + public IReadOnlyDictionary Texts { get => texts; set => SetProperty(ref texts, value); } + + /// + /// Gets the markdown for a specified paragraph from the docs page. + /// + /// The key of the paragraph to retrieve. + /// The text of the specified paragraph, or . + public string GetParagraph(string key) + { + return Texts != null && Texts.TryGetValue(key, out var value) ? value : string.Empty; + } + + /// + /// Implements the logic for . + /// + /// The name of the docs file to load. + private async Task LoadDocsAsync(string name) + { + // Skip if the loading has already started + if (!(LoadDocsCommand.ExecutionTask is null)) return; + + var path = Path.Combine("Assets", "docs", $"{name}.md"); + using var stream = await FilesServices.OpenForReadAsync(path); + using var reader = new StreamReader(stream); + var text = await reader.ReadToEndAsync(); + + Texts = MarkdownHelper.GetParagraphs(text); + + OnPropertyChanged(nameof(GetParagraph)); + } + } +} + diff --git a/samples/MvvmSample.Core/ViewModels/Widgets/PostWidgetViewModel.cs b/samples/MvvmSample.Core/ViewModels/Widgets/PostWidgetViewModel.cs new file mode 100644 index 0000000..afca023 --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/Widgets/PostWidgetViewModel.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.Messaging; +using Microsoft.Toolkit.Mvvm.Messaging.Messages; +using MvvmSample.Models; + +namespace MvvmSample.ViewModels.Widgets +{ + /// + /// A viewmodel for a post widget. + /// + public sealed class PostWidgetViewModel : ObservableRecipient, IRecipient> + { + private Post post; + + /// + /// Gets the currently selected post, if any. + /// + public Post Post + { + get => post; + private set => SetProperty(ref post, value); + } + + /// + public void Receive(PropertyChangedMessage message) + { + if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) && + message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost)) + { + Post = message.NewValue; + } + } + } +} diff --git a/samples/MvvmSample.Core/ViewModels/Widgets/SubredditWidgetViewModel.cs b/samples/MvvmSample.Core/ViewModels/Widgets/SubredditWidgetViewModel.cs new file mode 100644 index 0000000..5394b48 --- /dev/null +++ b/samples/MvvmSample.Core/ViewModels/Widgets/SubredditWidgetViewModel.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Toolkit.Mvvm.ComponentModel; +using Microsoft.Toolkit.Mvvm.DependencyInjection; +using Microsoft.Toolkit.Mvvm.Input; +using MvvmSample.Models; +using MvvmSample.Services; +using Nito.AsyncEx; + +namespace MvvmSample.ViewModels.Widgets +{ + /// + /// A viewmodel for a subreddit widget. + /// + public sealed class SubredditWidgetViewModel : ObservableRecipient + { + /// + /// Gets the instance to use. + /// + private readonly IRedditService RedditService = Ioc.Default.GetRequiredService(); + + /// + /// Gets the instance to use. + /// + private readonly ISettingsService SettingsService = Ioc.Default.GetRequiredService(); + + /// + /// An instance to avoid concurrent requests. + /// + private readonly AsyncLock LoadingLock = new AsyncLock(); + + /// + /// Creates a new instance. + /// + public SubredditWidgetViewModel() + { + LoadPostsCommand = new AsyncRelayCommand(LoadPostsAsync); + + selectedSubreddit = SettingsService.GetValue(nameof(SelectedSubreddit)) ?? Subreddits[0]; + } + + /// + /// Gets the instance responsible for loading posts. + /// + public IAsyncRelayCommand LoadPostsCommand { get; } + + /// + /// Gets the collection of loaded posts. + /// + public ObservableCollection Posts { get; } = new ObservableCollection(); + + /// + /// Gets the collection of available subreddits to pick from. + /// + public IReadOnlyList Subreddits { get; } = new[] + { + "microsoft", + "windows", + "surface", + "windowsphone", + "dotnet", + "csharp" + }; + + private string selectedSubreddit; + + /// + /// Gets or sets the currently selected subreddit. + /// + public string SelectedSubreddit + { + get => selectedSubreddit; + set + { + SetProperty(ref selectedSubreddit, value); + + SettingsService.SetValue(nameof(SelectedSubreddit), value); + } + } + + private Post selectedPost; + + /// + /// Gets or sets the currently selected subreddit. + /// + public Post SelectedPost + { + get => selectedPost; + set => SetProperty(ref selectedPost, value, true); + } + + /// + /// Loads the posts from a specified subreddit. + /// + private async Task LoadPostsAsync() + { + using (await LoadingLock.LockAsync()) + { + try + { + var response = await RedditService.GetSubredditPostsAsync(SelectedSubreddit); + + Posts.Clear(); + + foreach (var item in response.Data.Items) + { + Posts.Add(item.Data); + } + } + catch + { + // Whoops! + } + } + } + } +} diff --git a/samples/MvvmSampleUno/MvvmSample.sln b/samples/MvvmSampleUno/MvvmSample.sln new file mode 100644 index 0000000..c7d4a78 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample.sln @@ -0,0 +1,417 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30323.103 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "MvvmSample.Shared", "MvvmSample\MvvmSample.Shared\MvvmSample.Shared.shproj", "{6279C845-92F8-4333-AB99-3D213163593C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvvmSample.Droid", "MvvmSample\MvvmSample.Droid\MvvmSample.Droid.csproj", "{17579977-DC58-416C-B1C3-AAAECEEBB41D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvvmSample.iOS", "MvvmSample\MvvmSample.iOS\MvvmSample.iOS.csproj", "{F1606259-0C33-4086-A4A7-F95C73ED46AF}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvvmSample.UWP", "MvvmSample\MvvmSample.UWP\MvvmSample.UWP.csproj", "{455221B3-484E-412B-ADB1-D884CCA47383}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvvmSample.Wasm", "MvvmSample\MvvmSample.Wasm\MvvmSample.Wasm.csproj", "{79389D18-22EF-4C88-B0E1-177FCC8400FD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MvvmSample.macOS", "MvvmSample\MvvmSample.macOS\MvvmSample.macOS.csproj", "{60704335-4B3D-47D9-8684-BBEE1AC8EE88}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MvvmSample.Core", "..\MvvmSample.Core\MvvmSample.Core.csproj", "{98CCBE56-376A-409D-93E6-6424337333A7}" +EndProject +Global + GlobalSection(SharedMSBuildProjectFiles) = preSolution + MvvmSample\MvvmSample.Shared\MvvmSample.Shared.projitems*{17579977-dc58-416c-b1c3-aaaeceebb41d}*SharedItemsImports = 4 + MvvmSample\MvvmSample.Shared\MvvmSample.Shared.projitems*{455221b3-484e-412b-adb1-d884cca47383}*SharedItemsImports = 4 + MvvmSample\MvvmSample.Shared\MvvmSample.Shared.projitems*{60704335-4b3d-47d9-8684-bbee1ac8ee88}*SharedItemsImports = 4 + MvvmSample\MvvmSample.Shared\MvvmSample.Shared.projitems*{6279c845-92f8-4333-ab99-3d213163593c}*SharedItemsImports = 13 + MvvmSample\MvvmSample.Shared\MvvmSample.Shared.projitems*{79389d18-22ef-4c88-b0e1-177fcc8400fd}*SharedItemsImports = 5 + MvvmSample\MvvmSample.Shared\MvvmSample.Shared.projitems*{f1606259-0c33-4086-a4a7-f95c73ed46af}*SharedItemsImports = 4 + EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Ad-Hoc|Any CPU = Ad-Hoc|Any CPU + Ad-Hoc|ARM = Ad-Hoc|ARM + Ad-Hoc|ARM64 = Ad-Hoc|ARM64 + Ad-Hoc|iPhone = Ad-Hoc|iPhone + Ad-Hoc|iPhoneSimulator = Ad-Hoc|iPhoneSimulator + Ad-Hoc|x64 = Ad-Hoc|x64 + Ad-Hoc|x86 = Ad-Hoc|x86 + AppStore|Any CPU = AppStore|Any CPU + AppStore|ARM = AppStore|ARM + AppStore|ARM64 = AppStore|ARM64 + AppStore|iPhone = AppStore|iPhone + AppStore|iPhoneSimulator = AppStore|iPhoneSimulator + AppStore|x64 = AppStore|x64 + AppStore|x86 = AppStore|x86 + Debug|Any CPU = Debug|Any CPU + Debug|ARM = Debug|ARM + Debug|ARM64 = Debug|ARM64 + Debug|iPhone = Debug|iPhone + Debug|iPhoneSimulator = Debug|iPhoneSimulator + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|ARM = Release|ARM + Release|ARM64 = Release|ARM64 + Release|iPhone = Release|iPhone + Release|iPhoneSimulator = Release|iPhoneSimulator + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|Any CPU.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|ARM.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|ARM.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|ARM.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|ARM64.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|ARM64.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|ARM64.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|iPhone.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|x64.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|x64.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|x64.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|x86.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|x86.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Ad-Hoc|x86.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|Any CPU.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|Any CPU.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|ARM.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|ARM.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|ARM.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|ARM64.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|ARM64.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|ARM64.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|iPhone.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|iPhone.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|iPhoneSimulator.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|x64.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|x64.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|x64.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|x86.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|x86.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.AppStore|x86.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|ARM.ActiveCfg = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|ARM.Build.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|ARM.Deploy.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|ARM64.Build.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|ARM64.Deploy.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|iPhone.Build.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|iPhone.Deploy.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|iPhoneSimulator.Deploy.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|x64.ActiveCfg = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|x64.Build.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|x64.Deploy.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|x86.ActiveCfg = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|x86.Build.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Debug|x86.Deploy.0 = Debug|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|Any CPU.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|Any CPU.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|ARM.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|ARM.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|ARM.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|ARM64.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|ARM64.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|ARM64.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|iPhone.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|iPhone.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|iPhone.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|iPhoneSimulator.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|x64.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|x64.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|x64.Deploy.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|x86.ActiveCfg = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|x86.Build.0 = Release|Any CPU + {17579977-DC58-416C-B1C3-AAAECEEBB41D}.Release|x86.Deploy.0 = Release|Any CPU + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|Any CPU.ActiveCfg = Ad-Hoc|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|ARM.ActiveCfg = Ad-Hoc|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|ARM64.ActiveCfg = Ad-Hoc|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|iPhone.ActiveCfg = Ad-Hoc|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|iPhone.Build.0 = Ad-Hoc|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Ad-Hoc|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|iPhoneSimulator.Build.0 = Ad-Hoc|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|x64.ActiveCfg = Ad-Hoc|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Ad-Hoc|x86.ActiveCfg = Ad-Hoc|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|Any CPU.ActiveCfg = AppStore|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|ARM.ActiveCfg = AppStore|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|ARM64.ActiveCfg = AppStore|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|iPhone.ActiveCfg = AppStore|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|iPhone.Build.0 = AppStore|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|iPhoneSimulator.ActiveCfg = AppStore|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|iPhoneSimulator.Build.0 = AppStore|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|x64.ActiveCfg = AppStore|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.AppStore|x86.ActiveCfg = AppStore|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|Any CPU.ActiveCfg = Debug|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|ARM.ActiveCfg = Debug|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|ARM64.ActiveCfg = Debug|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|iPhone.ActiveCfg = Debug|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|iPhone.Build.0 = Debug|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|x64.ActiveCfg = Debug|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Debug|x86.ActiveCfg = Debug|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|Any CPU.ActiveCfg = Release|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|ARM.ActiveCfg = Release|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|ARM64.ActiveCfg = Release|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|iPhone.ActiveCfg = Release|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|iPhone.Build.0 = Release|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|x64.ActiveCfg = Release|iPhone + {F1606259-0C33-4086-A4A7-F95C73ED46AF}.Release|x86.ActiveCfg = Release|iPhone + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|Any CPU.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|Any CPU.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|Any CPU.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|ARM.ActiveCfg = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|ARM.Build.0 = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|ARM.Deploy.0 = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|ARM64.ActiveCfg = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|ARM64.Build.0 = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|ARM64.Deploy.0 = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|iPhone.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|iPhone.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|iPhone.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|iPhoneSimulator.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|x64.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|x64.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|x64.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|x86.ActiveCfg = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|x86.Build.0 = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Ad-Hoc|x86.Deploy.0 = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|Any CPU.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|Any CPU.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|Any CPU.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|ARM.ActiveCfg = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|ARM.Build.0 = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|ARM.Deploy.0 = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|ARM64.ActiveCfg = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|ARM64.Build.0 = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|ARM64.Deploy.0 = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|iPhone.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|iPhone.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|iPhone.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|iPhoneSimulator.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|iPhoneSimulator.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|iPhoneSimulator.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|x64.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|x64.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|x64.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|x86.ActiveCfg = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|x86.Build.0 = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.AppStore|x86.Deploy.0 = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|Any CPU.ActiveCfg = Debug|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|Any CPU.Build.0 = Debug|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|Any CPU.Deploy.0 = Debug|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|ARM.ActiveCfg = Debug|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|ARM.Build.0 = Debug|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|ARM.Deploy.0 = Debug|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|ARM64.Build.0 = Debug|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|iPhone.ActiveCfg = Debug|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|iPhoneSimulator.ActiveCfg = Debug|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|x64.ActiveCfg = Debug|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|x64.Build.0 = Debug|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|x64.Deploy.0 = Debug|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|x86.ActiveCfg = Debug|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|x86.Build.0 = Debug|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Debug|x86.Deploy.0 = Debug|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|Any CPU.ActiveCfg = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|ARM.ActiveCfg = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|ARM.Build.0 = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|ARM.Deploy.0 = Release|ARM + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|ARM64.ActiveCfg = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|ARM64.Build.0 = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|ARM64.Deploy.0 = Release|ARM64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|iPhone.ActiveCfg = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|iPhoneSimulator.ActiveCfg = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|x64.ActiveCfg = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|x64.Build.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|x64.Deploy.0 = Release|x64 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|x86.ActiveCfg = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|x86.Build.0 = Release|x86 + {455221B3-484E-412B-ADB1-D884CCA47383}.Release|x86.Deploy.0 = Release|x86 + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|ARM.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|ARM64.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|ARM64.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|iPhone.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|x64.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|x64.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|x86.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.AppStore|x86.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|ARM.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|ARM.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|ARM64.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|iPhone.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|x64.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|x64.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|x86.ActiveCfg = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Debug|x86.Build.0 = Debug|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|Any CPU.Build.0 = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|ARM.ActiveCfg = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|ARM.Build.0 = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|ARM64.ActiveCfg = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|ARM64.Build.0 = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|iPhone.ActiveCfg = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|iPhone.Build.0 = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|x64.ActiveCfg = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|x64.Build.0 = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|x86.ActiveCfg = Release|Any CPU + {79389D18-22EF-4C88-B0E1-177FCC8400FD}.Release|x86.Build.0 = Release|Any CPU + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|Any CPU.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|Any CPU.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|ARM.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|ARM.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|ARM64.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|ARM64.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|iPhone.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|iPhone.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|x64.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|x64.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|x86.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Ad-Hoc|x86.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|Any CPU.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|Any CPU.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|ARM.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|ARM.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|ARM64.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|ARM64.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|iPhone.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|iPhone.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|x64.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|x64.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|x86.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.AppStore|x86.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|Any CPU.ActiveCfg = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|ARM.ActiveCfg = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|ARM64.ActiveCfg = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|iPhone.ActiveCfg = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|iPhoneSimulator.ActiveCfg = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|iPhoneSimulator.Build.0 = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|x64.ActiveCfg = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Debug|x86.ActiveCfg = Debug|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|Any CPU.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|ARM.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|ARM64.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|iPhone.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|iPhoneSimulator.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|iPhoneSimulator.Build.0 = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|x64.ActiveCfg = Release|iPhoneSimulator + {60704335-4B3D-47D9-8684-BBEE1AC8EE88}.Release|x86.ActiveCfg = Release|iPhoneSimulator + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|ARM.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|ARM.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|ARM64.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|ARM64.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|x64.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|x64.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|ARM.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|ARM.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|ARM64.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|ARM64.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|iPhone.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|x64.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|x64.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|x86.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.AppStore|x86.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|ARM.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|ARM.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|ARM64.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|ARM64.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|iPhone.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|x64.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|x64.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|x86.ActiveCfg = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Debug|x86.Build.0 = Debug|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|Any CPU.Build.0 = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|ARM.ActiveCfg = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|ARM.Build.0 = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|ARM64.ActiveCfg = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|ARM64.Build.0 = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|iPhone.ActiveCfg = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|iPhone.Build.0 = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|x64.ActiveCfg = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|x64.Build.0 = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|x86.ActiveCfg = Release|Any CPU + {98CCBE56-376A-409D-93E6-6424337333A7}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B8683C54-9538-47DD-B824-53DA7518535A} + EndGlobalSection +EndGlobal diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Assets/AboutAssets.txt b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Assets/AboutAssets.txt new file mode 100644 index 0000000..ee39886 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Assets/AboutAssets.txt @@ -0,0 +1,19 @@ +Any raw assets you want to be deployed with your application can be placed in +this directory (and child directories) and given a Build Action of "AndroidAsset". + +These files will be deployed with you package and will be accessible using Android's +AssetManager, like this: + +public class ReadAsset : Activity +{ + protected override void OnCreate (Bundle bundle) + { + base.OnCreate (bundle); + + InputStream input = Assets.Open ("my_asset.txt"); + } +} + +Additionally, some Android functions will automatically load asset files: + +Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf"); \ No newline at end of file diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Assets/Fonts/winjs-symbols.ttf b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Assets/Fonts/winjs-symbols.ttf new file mode 100644 index 0000000..118f5c4 Binary files /dev/null and b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Assets/Fonts/winjs-symbols.ttf differ diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Main.cs b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Main.cs new file mode 100644 index 0000000..78a8792 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Main.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +using Android.App; +using Android.Content; +using Android.OS; +using Android.Runtime; +using Android.Views; +using Android.Widget; +using Com.Nostra13.Universalimageloader.Core; +using Windows.UI.Xaml.Media; + +namespace MvvmSample.Droid +{ + [global::Android.App.ApplicationAttribute( + Label = "@string/ApplicationName", + LargeHeap = true, + HardwareAccelerated = true, + Theme = "@style/AppTheme" + )] + public class Application : Windows.UI.Xaml.NativeApplication + { + public Application(IntPtr javaReference, JniHandleOwnership transfer) + : base(() => new App(), javaReference, transfer) + { + ConfigureUniversalImageLoader(); + } + + private void ConfigureUniversalImageLoader() + { + // Create global configuration and initialize ImageLoader with this config + ImageLoaderConfiguration config = new ImageLoaderConfiguration + .Builder(Context) + .Build(); + + ImageLoader.Instance.Init(config); + + ImageSource.DefaultImageLoader = ImageLoader.Instance.LoadImageAsync; + } + } +} diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/MainActivity.cs b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/MainActivity.cs new file mode 100644 index 0000000..d76b307 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/MainActivity.cs @@ -0,0 +1,18 @@ +using Android.App; +using Android.Widget; +using Android.OS; +using Android.Content.PM; +using Android.Views; + +namespace MvvmSample.Droid +{ + [Activity( + MainLauncher = true, + ConfigurationChanges = Uno.UI.ActivityHelper.AllConfigChanges, + WindowSoftInputMode = SoftInput.AdjustPan | SoftInput.StateHidden + )] + public class MainActivity : Windows.UI.Xaml.ApplicationActivity + { + } +} + diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/MvvmSample.Droid.csproj b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/MvvmSample.Droid.csproj new file mode 100644 index 0000000..b0ce337 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/MvvmSample.Droid.csproj @@ -0,0 +1,126 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {17579977-DC58-416C-B1C3-AAAECEEBB41D} + {EFBA0AD7-5A72-4C68-AF49-83D382785DCF};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Library + Properties + MvvmSample.Droid + MvvmSample.Droid + 512 + true + Resources\Resource.Designer.cs + + false + Off + False + v11.0 + Properties\AndroidManifest.xml + True + ..\MvvmSample.Shared\Strings + 8.0 + + + true + portable + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + None + true + + + portable + true + true + true + bin\Release\ + TRACE + prompt + 4 + False + SdkOnly + true + true + true + true + true + + + + + + + + + + + + + 5.0.1 + + + 7.0.0-preview4 + + + + 5.1.0 + + + 5.2.4 + + + + 6.1.0-build.191.gc988bdd4ff + + + 6.1.0-build.191.gc988bdd4ff + + + 3.4.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {98ccbe56-376a-409d-93e6-6424337333a7} + MvvmSample.Core + + + + + + + \ No newline at end of file diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Properties/AndroidManifest.xml b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Properties/AndroidManifest.xml new file mode 100644 index 0000000..5e47331 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Properties/AssemblyInfo.cs b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..bedf68a --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Android.App; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("MvvmSample.Droid")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("MvvmSample.Droid")] +[assembly: AssemblyCopyright("Copyright © 2020")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/AboutResources.txt b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/AboutResources.txt new file mode 100644 index 0000000..c2bca97 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.axml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.axml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "R" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the R class would expose: + +public class R { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main +to reference the layout/main.axml file, or R.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/drawable/Icon.png b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/drawable/Icon.png new file mode 100644 index 0000000..8074c4c Binary files /dev/null and b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/drawable/Icon.png differ diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/values/Strings.xml b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/values/Strings.xml new file mode 100644 index 0000000..00af15c --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/values/Strings.xml @@ -0,0 +1,5 @@ + + + Hello World, Click Me! + MvvmSample + diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/values/Styles.xml b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/values/Styles.xml new file mode 100644 index 0000000..d668a8e --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Droid/Resources/values/Styles.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/App.xaml b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/App.xaml new file mode 100644 index 0000000..d41616c --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/App.xaml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/App.xaml.cs b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/App.xaml.cs new file mode 100644 index 0000000..10315ba --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/App.xaml.cs @@ -0,0 +1,116 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Toolkit.Mvvm.DependencyInjection; +using MvvmSample; +using MvvmSample.Helpers; +using MvvmSample.Services; +using Refit; +using Windows.ApplicationModel.Activation; +using Windows.ApplicationModel.Core; +using Windows.UI.Xaml; + +namespace MvvmSample +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + sealed partial class App : Application + { + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + ConfigureFilters(global::Uno.Extensions.LogExtensionPoint.AmbientLoggerFactory); + + this.InitializeComponent(); + } + + /// + protected override void OnLaunched(LaunchActivatedEventArgs e) + { + // Ensure the UI is initialized + if (Windows.UI.Xaml.Window.Current.Content is null) + { +#if WINDOWS_UWP + // Load the WinUI resource dictionaries on UWP + Resources.MergedDictionaries.Add(new Microsoft.UI.Xaml.Controls.XamlControlsResources()); +#endif + + Windows.UI.Xaml.Window.Current.Content = new Shell(); + + TitleBarHelper.StyleTitleBar(); + TitleBarHelper.ExpandViewIntoTitleBar(); + + // Register services + Ioc.Default.ConfigureServices(new ServiceCollection() + .AddSingleton() + .AddSingleton() + .AddSingleton(RestService.For("https://www.reddit.com/")) + .BuildServiceProvider()); + } + + // Enable the prelaunch if needed, and activate the window + if (e.PrelaunchActivated == false) + { + CoreApplication.EnablePrelaunch(true); + + Windows.UI.Xaml.Window.Current.Activate(); + } + } + + /// + /// Configures global logging + /// + /// + static void ConfigureFilters(ILoggerFactory factory) + { + factory + .WithFilter(new FilterLoggerSettings + { + { "Uno", LogLevel.Warning }, + { "Windows", LogLevel.Warning }, + + // Debug JS interop + // { "Uno.Foundation.WebAssemblyRuntime", LogLevel.Debug }, + + // Generic Xaml events + // { "Windows.UI.Xaml", LogLevel.Debug }, + // { "Windows.UI.Xaml.VisualStateGroup", LogLevel.Debug }, + // { "Windows.UI.Xaml.StateTriggerBase", LogLevel.Debug }, + // { "Windows.UI.Xaml.UIElement", LogLevel.Debug }, + + // Layouter specific messages + // { "Windows.UI.Xaml.Controls", LogLevel.Debug }, + // { "Windows.UI.Xaml.Controls.Layouter", LogLevel.Debug }, + // { "Windows.UI.Xaml.Controls.Panel", LogLevel.Debug }, + // { "Windows.Storage", LogLevel.Debug }, + + // Binding related messages + // { "Windows.UI.Xaml.Data", LogLevel.Debug }, + + // DependencyObject memory references tracking + // { "ReferenceHolder", LogLevel.Debug }, + + // ListView-related messages + // { "Windows.UI.Xaml.Controls.ListViewBase", LogLevel.Debug }, + // { "Windows.UI.Xaml.Controls.ListView", LogLevel.Debug }, + // { "Windows.UI.Xaml.Controls.GridView", LogLevel.Debug }, + // { "Windows.UI.Xaml.Controls.VirtualizingPanelLayout", LogLevel.Debug }, + // { "Windows.UI.Xaml.Controls.NativeListViewBase", LogLevel.Debug }, + // { "Windows.UI.Xaml.Controls.ListViewBaseSource", LogLevel.Debug }, //iOS + // { "Windows.UI.Xaml.Controls.ListViewBaseInternalContainer", LogLevel.Debug }, //iOS + // { "Windows.UI.Xaml.Controls.NativeListViewBaseAdapter", LogLevel.Debug }, //Android + // { "Windows.UI.Xaml.Controls.BufferViewCache", LogLevel.Debug }, //Android + // { "Windows.UI.Xaml.Controls.VirtualizingPanelGenerator", LogLevel.Debug }, //WASM + } + ) +#if DEBUG + .AddConsole(LogLevel.Debug); +#else + .AddConsole(LogLevel.Information); +#endif + } + } +} diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/SharedAssets.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/SharedAssets.md new file mode 100644 index 0000000..8d2d37a --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/SharedAssets.md @@ -0,0 +1,34 @@ +See documentation about assets here : https://github.com/unoplatform/uno/blob/master/doc/articles/features/working-with-assets.md + +# Here is a cheat sheet: + +1. Add the image file to the `Assets` directory of a shared project. +2. Set the build action to `Content`. +3. (Recommended) Provide an asset for various scales/dpi + +## Examples + +``` +\Assets\Images\logo.scale-100.png +\Assets\Images\logo.scale-200.png +\Assets\Images\logo.scale-400.png + +\Assets\Images\scale-100\logo.png +\Assets\Images\scale-200\logo.png +\Assets\Images\scale\400\logo.png +``` + +## Table of scales + +| Scale | UWP | iOS | Android | +|-------|:-----------:|:--------:|:-------:| +| `100` | scale-100 | @1x | mdpi | +| `125` | scale-125 | N/A | N/A | +| `150` | scale-150 | N/A | hdpi | +| `200` | scale-200 | @2x | xhdpi | +| `300` | scale-300 | @3x | xxhdpi | +| `400` | scale-400 | N/A | xxxhdpi | + + + + diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/AsyncRelayCommand.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/AsyncRelayCommand.md new file mode 100644 index 0000000..6ba0df5 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/AsyncRelayCommand.md @@ -0,0 +1,42 @@ +--- +title: AsyncRelayCommand +author: Sergio0694 +description: An asynchronous command whose sole purpose is to relay its functionality to other objects by invoking delegates +keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, componentmodel, property changed, notification, binding, command, delegate, net core, net standard +dev_langs: + - csharp +--- + +# AsyncRelayCommand and AsyncRelayCommand<T> + +The [AsyncRelayCommand](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.AsyncRelayCommand) and [AsyncRelayCommand](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.input.AsyncRelayCommand-1) are `ICommand` implementations that extend the functionalities offered by `RelayCommand`, with support for asynchronous operations. + +## How they work + +`AsyncRelayCommand` and `AsyncRelayCommand` have the following main features: + +- They extend the functionalities of the non-asynchronous commands included in the library, with support for `Task`-returning delegates. +- They expose an `ExecutionTask` property that can be used to monitor the progress of a pending operation, and an `IsRunning` that can be used to check when an operation completes. This is particularly useful to bind a command to UI elements such as loading indicators. +- They implement the `IAsyncRelayCommand` and `IAsyncRelayCommand` interfaces, which means that viewmodel can easily expose commands using these to reduce the tight coupling between types. For instance, this makes it easier to replace a command with a custom implementation exposing the same public API surface, if needed. + +## Working with asynchronous commands + +Let's imagine a scenario similar to the one described in the `RelayCommand` sample, but a command executing an asynchronous operation. + +Upon clicking the `Button`, the command is invoked, and the `ExecutionTask` updated. When the operation completes, the property raises a notification which is reflected in the UI. In this case, both the task status and the current result of the task are displayed. Note that to show the result of the task, it is necessary to use the `TaskExtensions.GetResultOrDefault` method - this provides access to the result of a task that has not yet completed without blocking the thread (and possibly causing a deadlock). + +## Sample Code + +There are more examples in the [unit tests](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/UnitTests/UnitTests.Shared/Mvvm). + +## Requirements + +| Device family | Universal, 10.0.16299.0 or higher | +| --- | --- | +| Namespace | Microsoft.Toolkit.Mvvm | +| NuGet package | [Microsoft.Toolkit.Mvvm](https://www.nuget.org/packages/Microsoft.Toolkit.Mvvm/) | + +## API + +* [AsyncRelayCommand source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand.cs) +* [AsyncRelayCommand<T> source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Input/AsyncRelayCommand{T}.cs) diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Introduction.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Introduction.md new file mode 100644 index 0000000..abec4de --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Introduction.md @@ -0,0 +1,69 @@ +--- +title: Introduction to the MVVM package +author: Sergio0694 +description: An overview of how to get started with MVVM package and to the APIs it contains +keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, get started, visual studio, MVVM, net core, net standard +dev_langs: + - csharp + - vb +--- + +# Introduction to the MVVM package + +The `Microsoft.Toolkit.Mvvm` package is a modern, fast, and modular MVVM library. It is part of the Windows Community Toolkit and is built around the following principles: + +- **Platform and Runtime Independent** - **.NET Standard 2.0** 🚀 (UI Framework Agnostic) +- **Simple to pick-up and use** - No strict requirements on Application structure or coding-paradigms (outside of 'MVVM'ness), i.e., flexible usage. +- **À la carte** - Freedom to choose which components to use. +- **Reference Implementation** - Lean and performant, providing implementations for interfaces that are included in the Base Class Library, but lack concrete types to use them directly. + +This package targets .NET Standard so it can be used on any app platform: UWP, WinForms, WPF, Xamarin, Uno, and more; and on any runtime: .NET Native, .NET Core, .NET Framework, or Mono. It runs on all of them. The API surface is identical in all cases, making it perfect for building shared libraries. + +To install the package from within Visual Studio: + +1. In Solution Explorer, right-click on the project and select **Manage NuGet Packages**. Search for **Microsoft.Toolkit.Mvvm** and install it. + +2. Add a using or Imports directive to use the new APIs: + +```csharp +using Microsoft.Toolkit.Mvvm; +``` + +```vb +Imports Microsoft.Toolkit.Mvvm +``` + +## When should I use this package? + +Use this package for access to a collection of standard, self-contained, lightweight types that provide a starting implementation for building modern apps using the MVVM pattern. These types alone are usually enough for many users to build apps without needing additional external references. + +The included types are: + +- **Microsoft.Toolkit.Mvvm.ComponentModel** + - `ObservableObject` + - `ObservableRecipient` +- **Microsoft.Toolkit.Mvvm.DependencyInjection** + - `Ioc` +- **Microsoft.Toolkit.Mvvm.Input** + - `RelayCommand` + - `RelayCommand` + - `AsyncRelayCommand` + - `AsyncRelayCommand` + - `IRelayCommand` + - `IRelayCommand` + - `IAsyncRelayCommand` + - `IAsyncRelayCommand` +- **Microsoft.Toolkit.Mvvm.Messaging** + - `Messenger` + - `IMessenger` +- **Microsoft.Toolkit.Mvvm.Messaging.Messages** + - `PropertyChangedMessage` + - `RequestMessage` + - `AsyncRequestMessage` + - `CollectionRequestMessage` + - `AsyncCollectionRequestMessage` + - `ValueChangedMessage` + +This package aims to offer as much flexibility as possible, so developers are free to choose which components to use. All types are loosely-coupled, so that it's only necessary to include what you use. There is no requirement to go "all-in" with a specific series of all-encompassing APIs, nor is there a set of mandatory patterns that need to be followed when building apps using these helpers. Combine these building blocks in a way that best fits your needs. + +Code samples are available in the other docs pages for the MVVM package, and in the [unit tests](https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/UnitTests/UnitTests.Shared/Mvvm) for the project. \ No newline at end of file diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Ioc.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Ioc.md new file mode 100644 index 0000000..b05d35f --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Ioc.md @@ -0,0 +1,89 @@ +--- +title: Ioc +author: Sergio0694 +description: A type that facilitates the use of the IServiceProvider type +keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, service, dependency injection, net core, net standard +dev_langs: + - csharp +--- + +# Ioc ([Inversion of control](https://en.wikipedia.org/wiki/Inversion_of_control)) + +The [Ioc](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.DependencyInjection.Ioc) class is a type that facilitates the use of the `IServiceProvider` type. It's powered by the `Microsoft.Extensions.DependencyInjection` package, which provides a fully featured and powerful DI set of APIs, and acts as an easy to setup and use `IServiceProvider`. + +## Configure and resolve services + +The main entry point is the `ConfigureServices` method, which can be used like so: + +```csharp +// Register the services at startup +Ioc.Default.ConfigureServices(services => +{ + services.AddSingleton(); + services.AddSingleton(); + // Other services here... +}); + +// Retrieve a service instance when needed +IFilesService fileService = Ioc.Default.GetService(); +``` + +The `Ioc.Default` property offers a thread-safe `IServiceProvider` instance that can be used anywhere in the application to resolve services. The `ConfigureService` method handles the initialization of that service. It is also possible to create different `Ioc` instances and to initialize each with different services. + +## Constructor injection + +One powerful feature that is available is "constructor injection", which means that the DI service provider is able to automatically resolve indirect dependencies between registered services when creating instances of the type being requested. Consider the following service: + +```csharp +public class ConsoleLogger : ILogger +{ + private readonly IFileService FileService; + private readonly IConsoleService ConsoleService; + + public ConsoleLogger( + IFileService fileService, + IConsoleService consoleService) + { + FileService = fileService; + ConsoleService = consoleService; + } + + // Methods for the IFileLogger interface here... +} +``` + +Here we have a `ConsoleLogger` implementing the `ILogger` interface, and requiring `IFileService` and `IConsoleService` instances. Constructor injection means the DI service provider will "automagically" gather all the necessary services, like so: + +```csharp +// Register the services at startup +Ioc.Default.ConfigureServices(services => +{ + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); +}); + +// Retrieve a logger service with constructor injection +ILogger consoleLogger = Ioc.Default.GetService(); +``` + +The DI service provider will automatically check whether all the necessary services are registered, then it will retrieve them and invoke the constructor for the registered `ILogger` concrete type, to get the instance to return - all done automatically! + +## More docs + +For more info about `Microsoft.Extensions.DependencyInjection`, see [here](https://docs.microsoft.com/aspnet/core/fundamentals/dependency-injection). + +## Sample Code + +There are more examples in the [unit tests](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/UnitTests/UnitTests.Shared/Mvvm). + +## Requirements + +| Device family | Universal, 10.0.16299.0 or higher | +| --- | --- | +| Namespace | Microsoft.Toolkit.Mvvm | +| NuGet package | [Microsoft.Toolkit.Mvvm](https://www.nuget.org/packages/Microsoft.Toolkit.Mvvm/) | + +## API + +* [ObservableObject source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs) diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Messenger.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Messenger.md new file mode 100644 index 0000000..47b4660 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/Messenger.md @@ -0,0 +1,117 @@ +--- +title: Messenger +author: Sergio0694 +description: A type that can be used to exchange messages between different objects +keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, service, messenger, messaging, net core, net standard +dev_langs: + - csharp +--- + +# Messenger + +The [Messenger](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.Messaging.Messenger) class (with the accompanying [IMessenger](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.Messaging.IMessenger) interface) can be used to exchange messages between different objects. This can be useful to decouple different modules of an application without having to keep strong references to types being referenced. It is also possible to send messages to specific channels, uniquely identified by a token, and to have different messengers in different sections of an application. + +## How it works + +The `Messenger` type is responsible for maintaining links between recipients (receivers of messages) and their registered message types, with relative message handlers. Any object can be registered as a recipient for a given message type using a message handler, which will be invoked whenever the `Messenger` instance is used to send a message of that type. It is also possible to send messages through specific communication channels (each identified by a unique token), so that multiple modules can exchange messages of the same type without causing conflicts. Messages sent without a token use the default shared channel. + +There are two ways to perform message registration: either through the `IRecipient` interface, or using an `Action` delegate acting as message handler. The first lets you register all the handlers with a single call to the `RegisterAll` extension, which automatically registers the recipients of all the declared message handlers, while the latter is useful when you need more flexibility or when you want to use a simple lambda expression as a message handler. + +Similar to the `Ioc` class, `Messenger` exposes a `Default` property that offers a thread-safe implementation built-in into the package. It is also possible to create multiple `Messenger` instances if needed, for instance if a different one is injected with a DI service provider into a different module of the app (for instance, multiple windows running in the same process). + +## Sending and receiving messages + +Consider the following: + +```csharp +// Create a message +public class LoggedInUserChangedMessage : ValueChangedMessage +{ + public LoggedInUserChangedMessage(User user) : base(user) + { + } +} + +// Register a message in some module +Messenger.Default.Register(this, m => +{ + // Handle the message here +}); + +// Send a message from some other module +Messenger.Default.Send(new LoggedInUserChangedMessage(user)); +``` + +Let's imagine this message type being used in a simple messaging application, which displays a header with the user name and profile image of the currently logged user, a panel with a list of conversations, and another panel with messages from the current conversation, if one is selected. Let's say these three sections are supported by the `HeaderViewModel`, `ConversationsListViewModel` and `ConversationViewModel` types respectively. In this scenario, the `LoggedInUserChangedMessage` message might be sent by the `HeaderViewModel` after a login operation has completed, and both those other viewmodels might register handlers for it. For instance, `ConversationsListViewModel` will load the list of conversations for the new user, and `ConversationViewModel` will just close the current conversation, if one is present. + +The `Messenger` class takes care of delivering messages to all the registered recipients. Note that a recipient can subscribe to messages of a specific type. Note that inherited message types are not registered in the default `Messenger` implementation. + +When a recipient is not needed anymore, you should unregister it so that it will stop receiving messages. You can unregister either by message type, by registration token, or by recipient: + +```csharp +// Unregisters the recipient from a message type +Messenger.Default.Unregister(this); + +// Unregister the recipient from all messages, across all channels +Messenger.Default.UnregisterAll(this); +``` + +> [!WARNING] +> The `Messenger` implementation uses strong references to track the registered recipients. This is done for performance reasons, and it means that each registered recipient should manually be unregistered to avoid memory leaks. That is, as long as a recipient is registered, the `Messenger` instance in use will keep an active reference to it, which will prevent the garbage collector from being able to collect that instance. You can either handle this manually, or you can inherit from [ObservableRecipient](ObservableRecipient.md), which by default automatically takes care of removing all the message registrations for recipient when it is deactivated (see docs on `ObservableRecipient` for more info about this). + +## Using request messages + +Another useful feature of messenger instances is that they can also be used to request values from a module to another. In order to do so, the package includes a base `RequestMessage` class, which can be used like so: + +```csharp +// Create a message +public class LoggedInUserRequestMessage : RequestMessage +{ +} + +// Register the receiver in a module +Messenger.Default.Register(this, m => +{ + m.Reply(CurrentUser); // Assume this is a private member +}); + +// Request the value from another module +User user = Messenger.Default.Send(); +``` + +The `RequestMessage` class includes an implicit converter that makes the conversion from a `LoggedInUserRequestMessage` to its contained `User` object possible. This will also check that a response has been received for the message, and throw an exception if that's not the case. It is also possible to send request messages without this mandatory response guarantee: just store the returned message in a local variable, and then manually check whether a response value is available or not. Doing so will not trigger the automatic exception if a response is not received when the `Send` method returns. + +The same namespace also includes base requests message for other scenarios: `AsyncRequestMessage`, `CollectionRequestMessage` and `AsyncCollectionRequestMessage`. +Here's how you can use an async request message: + +```csharp +// Create a message +public class LoggedInUserRequestMessage : AsyncRequestMessage +{ +} + +// Register the receiver in a module +Messenger.Default.Register(this, m => +{ + m.Reply(GetCurrentUserAsync()); // We're replying with a Task +}); + +// Request the value from another module (we can directly await on the request) +User user = await Messenger.Default.Send(); +``` + +## Sample Code + +There are more examples in the [unit tests](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/UnitTests/UnitTests.Shared/Mvvm). + +## Requirements + +| Device family | Universal, 10.0.16299.0 or higher | +| --- | --- | +| Namespace | Microsoft.Toolkit.Mvvm | +| NuGet package | [Microsoft.Toolkit.Mvvm](https://www.nuget.org/packages/Microsoft.Toolkit.Mvvm/) | + +## API + +* [Messenger source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Messaging/Messenger.cs) +* [IMessenger source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/Messaging/IMessenger.cs) diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/ObservableObject.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/ObservableObject.md new file mode 100644 index 0000000..0ec6db6 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/ObservableObject.md @@ -0,0 +1,100 @@ +--- +title: ObservableObject +author: Sergio0694 +description: A base class for objects of which the properties must be observable +keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, componentmodel, property changed, notification, binding, net core, net standard +dev_langs: + - csharp +--- + +# ObservableObject + +The [ObservableObject](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.componentmodel.ObservableObject) is a base class for objects that are observable by implementing the [INotifyPropertyChanged](https://docs.microsoft.com/dotnet/api/system.componentmodel.inotifypropertychanged) and [INotifyPropertyChanging](https://docs.microsoft.com/dotnet/api/system.componentmodel.inotifypropertychanging) interfaces. It can be used as a starting point for all kinds of objects that need to support property change notifications. + +## How it works + +`ObservableObject` has the following main features: + +- It provides a base implementation for `INotifyPropertyChanged` and `INotifyPropertyChanging`, exposing the `PropertyChanged` and `PropertyChanging` events. +- It provides a series of `SetProperty` methods that can be used to easily set property values from types inheriting from `ObservableObject`, and to automatically raise the appropriate events. +- It provides the `SetPropertyAndNotifyOnCompletion` method, which is analogous to `SetProperty` but with the ability to set `Task` properties and raise the notification events automatically when the assigned tasks are completed. +- It exposes the `OnPropertyChanged` and `OnPropertyChanging` methods, which can be overridden in derived types to customize how the notification events are raised. + +## Simple property + +Here's an example of how to implement notification support to a custom property: + +```csharp +public class User : ObservableObject +{ + private string name; + + public string Name + { + get => name; + set => SetProperty(ref name, value); + } +} +``` + +The provided `SetProperty(ref T, T, string)` method checks the current value of the property, and updates it if different, and then also raises the relevant events automatically. The property name is automatically captured through the use of the `[CallerMemberName]` attribute, so there's no need to manually specify which property is being updated. + +## Wrapping a non-observable model + +A common scenario, for instance, when working with database items, is to create a wrapping "bindable" model that relays properties of the database model, and raises the property changed notifications when needed. This is also needed when wanting to inject notification support to models, that don't implement the `INotifyPropertyChanged` interface. `ObservableObject` provides a dedicated method to make this process simpler. For the following example, `User` is a model directly mapping a database table, without inheriting from `ObservableObject`: + +```csharp +public class ObservableUser : ObservableObject +{ + private readonly User user; + + public ObservableUser(User user) => this.user = user; + + public string Name + { + get => user.Name; + set => SetProperty(() => user.Name, value); + } +} +``` + +The `SetProperty(Expression>, T, string)` method makes creating these wrapping properties extremely simple, as it takes care of both retrieving and setting the target properties while providing an extremely compact API. + +## Handling `Task` properties + +If a property is a `Task` it's necessary to also raise the notification event once the task completes, so that bindings are updated at the right time. eg. to display a loading indicator or other status info on the operation represented by the task. `ObservableObject` has an API for this scenario: + +```csharp +public class MyModel : ObservableObject +{ + private Task requestTask; + + public Task RequestTask + { + get => requestTask; + set => SetPropertyAndNotifyOnCompletion(ref requestTask, () => requestTask, value); + } + + public void RequestValue() + { + RequestTask = WebService.LoadMyValueAsync(); + } +} +``` + +Here the `SetPropertyAndNotifyOnCompletion(ref TTask, Expression>, TTask, string)` method will take care of updating the target field, monitoring the new task, if present, and raising the notification event when that task completes. This way, it's possible to just bind to a task property and to be notified when its status changes. + +## Sample Code + +There are more examples in the [unit tests](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/UnitTests/UnitTests.Shared/Mvvm). + +## Requirements + +| Device family | Universal, 10.0.16299.0 or higher | +| --- | --- | +| Namespace | Microsoft.Toolkit.Mvvm | +| NuGet package | [Microsoft.Toolkit.Mvvm](https://www.nuget.org/packages/Microsoft.Toolkit.Mvvm/) | + +## API + +* [ObservableObject source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableObject.cs) diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/ObservableRecipient.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/ObservableRecipient.md new file mode 100644 index 0000000..1dabae2 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/ObservableRecipient.md @@ -0,0 +1,71 @@ +--- +title: ObservableRecipient +author: Sergio0694 +description: A base class for observable objects that also acts as recipients for messages +keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, componentmodel, property changed, notification, binding, messenger, messaging, net core, net standard +dev_langs: + - csharp +--- + +# ObservableRecipient + +The [`ObservableRecipient`](https://docs.microsoft.com/dotnet/api/microsoft.toolkit.mvvm.componentmodel.ObservableRecipient) type is a base class for observable objects that also acts as recipients for messages. This class is an extension of `ObservableObject` which also provides built-in support to use the `IMessenger` type. + +## How it works + +The `ObservableRecipient` type is meant to be used as a base for viewmodels that also use the `IMessenger` features, as it provides built-in support for it. In particular: + +- It has both a parameterless constructor and one that takes an `IMessenger` instance, to be used with dependency injection. It also exposes a `Messenger` property that can be used to send and receive messages in the viewmodel. If the parameterless constructor is used, the `Messenger.Default` instance will be assigned to the `Messenger` property. +- It exposes an `IsActive` property to activate/deactivate the viewmodel. In this context, to "activate" means that a given viewmodel is marked as being in use, such that eg. it will start listening for registered messages, perform other setup operations, etc. There are two related methods, `OnActivated` and `OnDeactivated`, that are invoked when the property changes value. By default, `OnDeactivated` automatically unregisters the current instance from all registered messages. For best results and to avoid memory leaks, it's recommended to use `OnActivated` to register to messages, and to use `OnDeactivated` to do cleanup operations. This pattern allows a viewmodel to be enabled/disabled multiple times, while being safe to collect without the risk of memory leaks every time it's deactivated. By default, `OnActived` will automatically register all the message handlers defined through the `IRecipient` interface. +- It exposes a `Broadcast(T, T, string)` method which sends a `PropertyChangedMessage` message through the `IMessenger` instance available from the `Messenger` property. This can be used to easily broadcast changes in the properties of a viewmodel without having to manually retrieve a `Messenger` instance to use. This method is used by the overload of the various `SetProperty` methods, which have an additional `bool broadcast` property to indicate whether or not to also send a message. + +Here's an example of a viewmodel that receives `LoggedInUserRequestMessage` messages when active: + +```csharp +public class MyViewModel : ObservableRecipient, IRecipient +{ + public void Receive(LoggedInUserRequestMessage message) + { + // Handle the message here + } +} +``` + +In the above, `OnActivated` automatically registers the instance as a recipient for `LoggedInUserRequestMessage` messages, using that method as the action to invoke. Using the `IRecipient` interface is not mandatory, and the registration can also be done manually (even using just an inline lambda expression): + +```csharp +public class MyViewModel : ObservableRecipient +{ + protected override void OnActivated() + { + // Using a method group... + Messenger.Register(this, Receive); + + // ...or a lambda expression + Messenger.Register(this, m => + { + // Handle the message here + }); + } + + private void Receive(LoggedInUserRequestMessage message) + { + // Handle the message here + } +} +``` + +## Sample Code + +There are more examples in the [unit tests](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/UnitTests/UnitTests.Shared/Mvvm). + +## Requirements + +| Device family | Universal, 10.0.16299.0 or higher | +| --- | --- | +| Namespace | Microsoft.Toolkit.Mvvm | +| NuGet package | [Microsoft.Toolkit.Mvvm](https://www.nuget.org/packages/Microsoft.Toolkit.Mvvm/) | + +## API + +* [ObservableRecipient source code](https://github.com/Microsoft/WindowsCommunityToolkit//blob/master/Microsoft.Toolkit.Mvvm/ComponentModel/ObservableRecipient.cs) diff --git a/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/PuttingThingsTogether.md b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/PuttingThingsTogether.md new file mode 100644 index 0000000..30a5be3 --- /dev/null +++ b/samples/MvvmSampleUno/MvvmSample/MvvmSample.Shared/Assets/docs/PuttingThingsTogether.md @@ -0,0 +1,423 @@ +--- +title: PuttingThingsTogether +author: Sergio0694 +description: An overview of how to combine different features of the MVVM Toolkit into a practical example +keywords: windows 10, uwp, windows community toolkit, uwp community toolkit, uwp toolkit, mvvm, service, messenger, messaging, net core, net standard +dev_langs: + - csharp +--- + +# Putting things together + +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. + +## What do we want to build + +Let's start by outlining exactly what we want to build: + +- 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. +- 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. +- We want the subreddit widget to also offer a refresh button to reload the current subreddit. +- 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. + +## Setting up the viewmodels + +Let's start with the viewmodel that will power the subreddit widget and let's go over the tools we need: + +- **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. +- **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`). In this case, we have: + - `ObservableCollection 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. + - `IReadOnlyList 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. + - `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. + - `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. +- **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. + +Here's the viewmodel so far: + +```csharp +public sealed class SubredditWidgetViewModel : ObservableRecipient +{ + /// + /// Creates a new instance. + /// + public SubredditWidgetViewModel() + { + LoadPostsCommand = new AsyncRelayCommand(LoadPostsAsync); + } + + /// + /// Gets the instance responsible for loading posts. + /// + public IAsyncRelayCommand LoadPostsCommand { get; } + + /// + /// Gets the collection of loaded posts. + /// + public ObservableCollection Posts { get; } = new ObservableCollection(); + + /// + /// Gets the collection of available subreddits to pick from. + /// + public IReadOnlyList Subreddits { get; } = new[] + { + "microsoft", + "windows", + "surface", + "windowsphone", + "dotnet", + "csharp" + }; + + private string selectedSubreddit; + + /// + /// Gets or sets the currently selected subreddit. + /// + public string SelectedSubreddit + { + get => selectedSubreddit; + set => SetProperty(ref selectedSubreddit, value); + } + + private object selectedPost; + + /// + /// Gets or sets the currently selected subreddit. + /// + public object SelectedPost + { + get => selectedPost; + set => SetProperty(ref selectedPost, value, true); + } + + /// + /// Loads the posts from a specified subreddit. + /// + private async Task LoadPostsAsync() + { + // TODO... + } +} +``` + +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: + +```csharp +public sealed class PostWidgetViewModel : ObservableRecipient, IRecipient> +{ + private object post; + + /// + /// Gets the currently selected post, if any. + /// + public object Post + { + get => post; + private set => SetProperty(ref post, value); + } + + /// + public void Receive(PropertyChangedMessage message) + { + if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) && + message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost)) + { + Post = message.NewValue; + } + } +} +``` + +In this case, we're using the `IRecipient` 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: + +```csharp +public sealed class PostWidgetViewModel : ObservableRecipient +{ + protected override void OnActivated() + { + // We use a method group here, but a lambda expression is also valid + Messenger.Register>(this, Receive); + } + + /// + public void Receive(PropertyChangedMessage message) + { + if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) && + message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost)) + { + Post = message.NewValue; + } + } +} +``` + +We now have a draft of our viewmodels ready, and we can start looking into the services we need. + +## Building the settings service + +> **NOTE:** the sample is built using the service locator pattern, but this is not the only possible pattern to use to manage service. The MVVM Toolkit also fully supports the dependency injection pattern, and you can choose the one you prefer depending on the architecture of your application, the available development time or personal preference. + +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. + +Here's a simple interface for a settings service: + +```csharp +public interface ISettingsService +{ + /// + /// Assigns a value to a settings key. + /// + /// The type of the object bound to the key. + /// The key to check. + /// The value to assign to the setting key. + void SetValue(string key, T value); + + /// + /// Reads a value from the current instance and returns its casting in the right type. + /// + /// The type of the object to retrieve. + /// The key associated to the requested object. + [Pure] + T GetValue(string key); +} +``` + +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: + +```csharp +/// +/// Gets the instance to use. +/// +private readonly ISettingsService SettingsService = Ioc.Default.GetRequiredService(); + +/// +/// Creates a new instance. +/// +public SubredditWidgetViewModel() +{ + selectedSubreddit = SettingsService.GetValue(nameof(SelectedSubreddit)) ?? Subreddits[0]; +} + +private string selectedSubreddit; + +/// +/// Gets or sets the currently selected subreddit. +/// +public string SelectedSubreddit +{ + get => selectedSubreddit; + set + { + SetProperty(ref selectedSubreddit, value); + + SettingsService.SetValue(nameof(SelectedSubreddit), value); + } +} +``` + +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. + +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: + +```csharp +public sealed class SettingsService : ISettingsService +{ + /// + /// The with the settings targeted by the current instance. + /// + private readonly IPropertySet SettingsStorage = ApplicationData.Current.LocalSettings.Values; + + /// + public void SetValue(string key, T value) + { + if (!SettingsStorage.ContainsKey(key)) SettingsStorage.Add(key, value); + else SettingsStorage[key] = value; + } + + /// + public T GetValue(string key) + { + if (SettingsStorage.TryGetValue(key, out object value)) + { + return (T)value; + } + + return default; + } +} +``` + +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: + +```csharp +Ioc.Default.ConfigureServices(services => +{ + services.AddSingleton(); +}); +``` + +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()` 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! + +## Building the Reddit service + +The last component of the backend that we're missing is a service that is able to use the Reddit REST APIs to fetch the posts from the subreddits we're interested in. To build it, we're going to use [refit](https://github.com/reactiveui/refit), which is a library to easily build type-safe services to interact with REST APIs. As before, we need to define the interface with all the APIs that our service will implement, like so: + +```csharp +public interface IRedditService +{ + /// + /// Get a list of posts from a given subreddit + /// + /// The subreddit name. + [Get("/r/{subreddit}/.json")] + Task GetSubredditPostsAsync(string subreddit); +} +``` + +That `PostsQueryResponse` is a model we wrote that maps the JSON response for that API. The exact structure of that class is not important - suffice to say that it contains a collection of `Post` items, which are simple models representing our posts, that like like this: + +```csharp +public class Post +{ + /// + /// Gets or sets the title of the post. + /// + public string Title { get; set; } + + /// + /// Gets or sets the URL to the post thumbnail, if present. + /// + public string Thumbnail { get; set; } + + /// + /// Gets the text of the post. + /// + public string SelfText { get; } +} +``` + +Once we have our service and our models, can plug them into our viewmodels to complete our backend. While doing so, we can also replace those `object` placeholders with the `Post` type we've defined: + +```csharp +public sealed class SubredditWidgetViewModel : ObservableRecipient +{ + /// + /// Gets the instance to use. + /// + private readonly IRedditService RedditService = Ioc.Default.GetRequiredService(); + + /// + /// Loads the posts from a specified subreddit. + /// + private async Task LoadPostsAsync() + { + var response = await RedditService.GetSubredditPostsAsync(SelectedSubreddit); + + Posts.Clear(); + + foreach (var item in response.Data.Items) + { + Posts.Add(item.Data); + } + } +} +``` + +We have added a new `IRedditService` field to store our service, just like we did for the settings service, and we implemented our `LoadPostsAsync` method, which was previously empty. + +The last missing piece now is just to inject the actual service into our service provider. The big difference in this case is that by using `refit` we don't actually need to implement the service at all! The library will automatically create a type implementing the service for us, behind the scenes. So we only need to get an `IRedditService` instance and inject it directly, like so: + +```csharp +Ioc.Default.ConfigureServices(services => +{ + services.AddSingleton(); + services.AddSingleton(RestService.For("https://www.reddit.com/")); +}); +``` + +And that's all we need to do! We now have all our backend ready to use, including two custom services that we created specifically for this app! 🎉 + +## Building the UI + +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. + +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. + +**Subreddit selector:** + +```xml + + + + + + + +``` + +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. + +**Refresh button:** + +```xml +