これまでに、CommunityToolkit.Mvvm
パッケージで使用できるすべてのさまざまなコンポーネントの概要を説明してきたので、それらのすべてを結合して 1 つのより大きな例を構築する実際の例について調べることができます。 ここでは、選択された数のサブレディットのための非常にシンプルで、かつミニマルな Reddit ブラウザーを構築したいと思います。
構築しようとしているもの
最初に、構築しようとしているものの概要について正確に説明します。
- 次の 2 つの "ウィジェット" で構成された最小限の Reddit ブラウザー。1 つはサブレディットからの投稿を示し、もう 1 つは現在選択されている投稿を示します。 この 2 つのウィジェットは、互いへの強い参照のない、自己完結型である必要があります。
- ユーザーが使用可能なオプションの一覧からサブレディットを選択できるようにし、その選択されているサブレディットを設定として保存して、それをサンプルの次回の読み込み時に読み込むようにしたいと考えています。
- サブレディット ウィジェットでは、現在のサブレディットを再度読み込むための更新ボタンも提供できるようにしたいと思います。
- このサンプルの目的として、可能性のあるすべての投稿の種類を処理できる必要はありません。 簡略化するために、読み込まれたすべての投稿にサンプル テキストを割り当て、それを直接表示するだけです。
ビューモデルの設定
では、サブレディット ウィジェットを機能させるビューモデルから始め、必要なツールについて見ていきましょう。
- コマンド: ビューモデルに、選択されているサブレディットから投稿の現在の一覧を再度読み込むように要求できるビューが必要です。
AsyncRelayCommand
型を使用すると、Reddit から投稿をフェッチするプライベート メソッドをラップできます。 ここでは、使用している正確なコマンドの種類への強い参照を回避するために、IAsyncRelayCommand
インターフェイスを使用してコマンドを公開しています。 これにより、場合によっては将来、使用されているその特定の種類に依存する UI コンポーネントについて心配することなく、コマンドの種類を変更することも可能になります。 - プロパティ: UI にいくつかの値を公開する必要があります。これは、完全に置き換えようとしている値である場合は監視可能なプロパティ、またはそれ自体が監視可能であるプロパティ (
ObservableCollection<T>
など) のどちらかで行うことができます。 この場合は、次のものがあります。ObservableCollection<object> Posts
。これは、読み込まれた投稿の監視可能な一覧です。 まだ投稿を表すモデルを作成していないため、ここでは、object
をプレースホルダーとしてのみ使用しています。 これは、後で置き換えることができます。IReadOnlyList<string> Subreddits
。これは、ユーザーが選択できるようにするサブレディットの名前が含まれている読み取り専用の一覧です。 このプロパティは更新されることがないため、監視可能である必要もありません。string SelectedSubreddit
。これは、現在選択されているサブレディットです。 このプロパティは、サンプルが読み込まれたときに選択されている最後のサブレディットを示すためと、ユーザーが選択を変更したときに UI から直接操作されるための両方に使用されるため、UI にバインドする必要があります。 ここでは、ObservableObject
クラスのSetProperty
メソッドを使用しています。object SelectedPost
。これは、現在選択されている投稿です。 この場合は、ObservableRecipient
クラスのSetProperty
メソッドを使用して、このプロパティが変更されたときに通知もブロードキャストすることを示しています。 これは、投稿ウィジェットに現在の投稿の選択が変更されたことを通知できるようにするために実行されます。
- メソッド: 非同期コマンドによってラップされ、選択されているサブレディットから投稿を読み込むためのロジックを含むプライベート
LoadPostsAsync
メソッドだけが必要です。
ここまでのビューモデルを次に示します。
public sealed class SubredditWidgetViewModel : ObservableRecipient
{
/// <summary>
/// Creates a new <see cref="SubredditWidgetViewModel"/> instance.
/// </summary>
public SubredditWidgetViewModel()
{
LoadPostsCommand = new AsyncRelayCommand(LoadPostsAsync);
}
/// <summary>
/// Gets the <see cref="IAsyncRelayCommand"/> instance responsible for loading posts.
/// </summary>
public IAsyncRelayCommand LoadPostsCommand { get; }
/// <summary>
/// Gets the collection of loaded posts.
/// </summary>
public ObservableCollection<object> Posts { get; } = new ObservableCollection<object>();
/// <summary>
/// Gets the collection of available subreddits to pick from.
/// </summary>
public IReadOnlyList<string> Subreddits { get; } = new[]
{
"microsoft",
"windows",
"surface",
"windowsphone",
"dotnet",
"csharp"
};
private string selectedSubreddit;
/// <summary>
/// Gets or sets the currently selected subreddit.
/// </summary>
public string SelectedSubreddit
{
get => selectedSubreddit;
set => SetProperty(ref selectedSubreddit, value);
}
private object selectedPost;
/// <summary>
/// Gets or sets the currently selected subreddit.
/// </summary>
public object SelectedPost
{
get => selectedPost;
set => SetProperty(ref selectedPost, value, true);
}
/// <summary>
/// Loads the posts from a specified subreddit.
/// </summary>
private async Task LoadPostsAsync()
{
// TODO...
}
}
次に、投稿ウィジェットのビューモデルのために何が必要かを見てみましょう。 これは、実際に必要なのは現在選択されている投稿で Post
プロパティを公開することと、サブレディット ウィジェットからブロードキャスト メッセージを受信して Post
プロパティを更新することだけであるため、はるかに単純なビューモデルになります。 これは次のようになります。
public sealed class PostWidgetViewModel : ObservableRecipient, IRecipient<PropertyChangedMessage<object>>
{
private object post;
/// <summary>
/// Gets the currently selected post, if any.
/// </summary>
public object Post
{
get => post;
private set => SetProperty(ref post, value);
}
/// <inheritdoc/>
public void Receive(PropertyChangedMessage<object> message)
{
if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) &&
message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost))
{
Post = message.NewValue;
}
}
}
この場合は、IRecipient<TMessage>
インターフェイスを使用して、ビューモデルで受信するメッセージを宣言しています。 宣言されたメッセージ用のハンドラーは、IsActive
プロパティが true
に設定されているときに ObservableRecipient
クラスによって自動的に追加されます。 このアプローチの使用は必須ではなく、次のように、各メッセージ ハンドラーを手動で登録することもできることに注意してください。
public sealed class PostWidgetViewModel : ObservableRecipient
{
protected override void OnActivated()
{
// We use a method group here, but a lambda expression is also valid
Messenger.Register<PostWidgetViewModel, PropertyChangedMessage<object>>(this, (r, m) => r.Receive(m));
}
/// <inheritdoc/>
public void Receive(PropertyChangedMessage<object> message)
{
if (message.Sender.GetType() == typeof(SubredditWidgetViewModel) &&
message.PropertyName == nameof(SubredditWidgetViewModel.SelectedPost))
{
Post = message.NewValue;
}
}
}
これで、ビューモデルのドラフトが準備できたたので、必要なサービスの調査を開始できます。
設定サービスの構築
Note
このサンプルは、ビューモデルのサービスを処理するための推奨されるアプローチである依存関係の挿入パターンを使用して構築されます。 サービス ロケーター パターンなどの他のパターンを使用することもできますが、MVVM Toolkit には、それを有効にするための組み込み API が用意されていません。
一部のプロパティを保存して永続化されるようにしたいので、ビューモデルでアプリケーション設定を操作できるようにする方法が必要です。 ただし、プラットフォーム固有の API をビューモデルで直接使用すると、すべてのビューモデルが移植可能な .NET Standard プロジェクトに含まれなくなるため、これを行うことはできません。 この問題は、Microsoft.Extensions.DependencyInjection
ライブラリ内のサービスと API を使用してアプリケーションの IServiceProvider
インスタンスを設定することによって解決できます。 考え方としては、必要なすべての API サーフェスを表すインターフェイスを記述してから、このインターフェイスをすべてのアプリケーション ターゲットに実装するプラットフォーム固有の型を実装します。 ビューモデルは、このインターフェイスしか操作しないため、どのプラットフォーム固有の型への強い参照もまったくなくなります。
設定サービスのための簡単なインターフェイスを次に示します。
public interface ISettingsService
{
/// <summary>
/// Assigns a value to a settings key.
/// </summary>
/// <typeparam name="T">The type of the object bound to the key.</typeparam>
/// <param name="key">The key to check.</param>
/// <param name="value">The value to assign to the setting key.</param>
void SetValue<T>(string key, T value);
/// <summary>
/// Reads a value from the current <see cref="IServiceProvider"/> instance and returns its casting in the right type.
/// </summary>
/// <typeparam name="T">The type of the object to retrieve.</typeparam>
/// <param name="key">The key associated to the requested object.</param>
[Pure]
T GetValue<T>(string key);
}
このインターフェイスを実装するプラットフォーム固有の型は、実際に設定をシリアル化し、それらをディスクに格納してから読み取り戻すために必要なすべてのロジックを処理することを想定できます。 これで、SelectedSubreddit
プロパティを永続的にするために、このサービスを SubredditWidgetViewModel
で使用できるようになりました。
/// <summary>
/// Gets the <see cref="ISettingsService"/> instance to use.
/// </summary>
private readonly ISettingsService SettingsService;
/// <summary>
/// Creates a new <see cref="SubredditWidgetViewModel"/> instance.
/// </summary>
public SubredditWidgetViewModel(ISettingsService settingsService)
{
SettingsService = settingsService;
selectedSubreddit = settingsService.GetValue<string>(nameof(SelectedSubreddit)) ?? Subreddits[0];
}
private string selectedSubreddit;
/// <summary>
/// Gets or sets the currently selected subreddit.
/// </summary>
public string SelectedSubreddit
{
get => selectedSubreddit;
set
{
SetProperty(ref selectedSubreddit, value);
SettingsService.SetValue(nameof(SelectedSubreddit), value);
}
}
ここでは、前に説明したように、依存関係の挿入とコンストラクターの挿入を使用しています。 単に設定サービスを格納する ISettingsService SettingsService
フィールド (これは、ビューモデル コンストラクターでパラメーターとして受け取っています) を宣言した後、前の値か、または最初の使用可能なサブレディットだけのどちらかを使用して、コンストラクターで SelectedSubreddit
プロパティを初期化しています。 次に、SelectedSubreddit
セッターも変更して、そこでも設定サービスを使用して新しい値をディスクに保存するようにしました。
完了。 次に必要なのは、今度はいずれかのアプリ プロジェクトの内部に直接、このサービスのプラットフォーム固有のバージョンを記述することだけです。 そのサービスが UWP でどのようになるかを次に示します。
public sealed class SettingsService : ISettingsService
{
/// <summary>
/// The <see cref="IPropertySet"/> with the settings targeted by the current instance.
/// </summary>
private readonly IPropertySet SettingsStorage = ApplicationData.Current.LocalSettings.Values;
/// <inheritdoc/>
public void SetValue<T>(string key, T value)
{
if (!SettingsStorage.ContainsKey(key)) SettingsStorage.Add(key, value);
else SettingsStorage[key] = value;
}
/// <inheritdoc/>
public T GetValue<T>(string key)
{
if (SettingsStorage.TryGetValue(key, out object value))
{
return (T)value;
}
return default;
}
}
パズルの最後のピースは、このプラットフォーム固有のサービスをサービス プロバイダー インスタンスに挿入することです。 これは、起動時に次のように行うことができます。
/// <summary>
/// Gets the <see cref="IServiceProvider"/> instance to resolve application services.
/// </summary>
public IServiceProvider Services { get; }
/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddTransient<PostWidgetViewModel>();
return services.BuildServiceProvider();
}
これにより、SettingsService
のシングルトン インスタンスが ISettingsService
を実装する型として登録されます。 また、一時的なサービスとして PostWidgetViewModel
も登録しています。つまり、インスタンスを取得するたびに、それが新しいサービスになります (複数の独立した投稿ウィジェットが必要な場合は、これが役立つことを想像できます)。 つまり、使用されているアプリが UWP アプリである間に ISettingsService
インスタンスを解決するたびに、そのアプリが SettingsService
インスタンスを受信し、それがバックグラウンドで UWP API を使用して設定を操作します。 完璧です。
Reddit サービスの構築
欠けているバックエンドの最後のコンポーネントは、Reddit REST API を使用して、関心のあるサブレディットから投稿をフェッチできるサービスです。 これを構築するために、REST API を操作するためのタイプ セーフなサービスを簡単に構築するライブラリである refit を使用します。 前と同様に、次のように、サービスで実装するすべての API を備えたインターフェイスを定義する必要があります。
public interface IRedditService
{
/// <summary>
/// Get a list of posts from a given subreddit
/// </summary>
/// <param name="subreddit">The subreddit name.</param>
[Get("/r/{subreddit}/.json")]
Task<PostsQueryResponse> GetSubredditPostsAsync(string subreddit);
}
この PostsQueryResponse
は、その API の JSON 応答をマップする記述済みのモデルです。 そのクラスの正確な構造は重要ではありません。次のように、投稿を表す単純なモデルである Post
項目のコレクションが含まれていると言えば十分です。
public class Post
{
/// <summary>
/// Gets or sets the title of the post.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets the URL to the post thumbnail, if present.
/// </summary>
public string Thumbnail { get; set; }
/// <summary>
/// Gets the text of the post.
/// </summary>
public string SelfText { get; }
}
サービスとモデルが揃ったら、それらをビューモデルに接続してバックエンドを完成させることができます。 また、それを行っている間に、これらの object
プレースホルダーを前に定義した Post
型に置き換えることもできます。
public sealed class SubredditWidgetViewModel : ObservableRecipient
{
/// <summary>
/// Gets the <see cref="IRedditService"/> instance to use.
/// </summary>
private readonly IRedditService RedditService = Ioc.Default.GetRequiredService<IRedditService>();
/// <summary>
/// Loads the posts from a specified subreddit.
/// </summary>
private async Task LoadPostsAsync()
{
var response = await RedditService.GetSubredditPostsAsync(SelectedSubreddit);
Posts.Clear();
foreach (var item in response.Data.Items)
{
Posts.Add(item.Data);
}
}
}
設定サービスの場合と同様に、サービスを格納するための新しい IRedditService
フィールドを追加し、以前は空であった LoadPostsAsync
メソッドを実装しました。
これで、最後の欠けているピースは、実際のサービスをサービス プロバイダーに挿入することだけになりました。 この場合の大きな違いは、実際にサービスの実装にはまったく必要のない refit
を使用していることです。 このライブラリでは、サービスをバックグラウンドで実装する型が自動的に作成されます。 そのため、次のように IRedditService
インスタンスを取得し、それを直接挿入するだけで済みます。
/// <summary>
/// Configures the services for the application.
/// </summary>
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
services.AddSingleton<ISettingsService, SettingsService>();
services.AddSingleton(RestService.For<IRedditService>("https://www.reddit.com/"));
services.AddTransient<PostWidgetViewModel>();
return services.BuildServiceProvider();
}
行う必要がある操作はすべて完了しました。 これで、このアプリ専用に作成した 2 つのカスタム サービスを含め、すべてのバックエンドがすぐに使用できる状態になりました。 🎉
UI の構築
これで、すべてのバックエンドが完成したので、ウィジェットの UI を記述できます。 MVVM パターンを使用すると、ここまで UI 関連のコードを何も記述しなくても、最初はビジネス ロジックだけにどのように集中できるかに注意してください。 ここでは、簡潔にするためにビューモデルを操作していないすべての UI コードを削除し、それぞれの異なるコントロールを 1 つずつ実行します。 完全なソース コードは、サンプル アプリに含まれています。
さまざまなコントロールを実行する前に、アプリケーション内のすべての異なるビュー (PostWidgetView
など) のビューモデルを解決する方法を次に示します。
public PostWidgetView()
{
this.InitializeComponent();
this.DataContext = App.Current.Services.GetService<PostWidgetViewModel>();
}
public PostWidgetViewModel ViewModel => (PostWidgetViewModel)DataContext;
IServiceProvider
インスタンスを使用して必要な PostWidgetViewModel
オブジェクトを解決しており、それがその後、データ コンテキスト プロパティに割り当てられます。 また、単純にデータ コンテキストを正しいビューモデルの種類にキャストする、厳密に型指定された ViewModel
プロパティも作成しています。これは、XAML コードで x:Bind
を有効にするために必要です。
では、サブレディットを選択するための ComboBox
、フィードを更新するための Button
、投稿を表示するための ListView
、フィードがいつ読み込まれているかを示すための ProgressBar
を備えたサブレディット ウィジェットから始めましょう。 ViewModel
プロパティは、前に説明したビューモデルのインスタンスを表すと仮定しています。これは、XAML で、または直接コード ビハインドで宣言できます。
サブレディット セレクター:
<ComboBox
ItemsSource="{x:Bind ViewModel.Subreddits}"
SelectedItem="{x:Bind ViewModel.SelectedSubreddit, Mode=TwoWay}">
<interactivity:Interaction.Behaviors>
<core:EventTriggerBehavior EventName="SelectionChanged">
<core:InvokeCommandAction Command="{x:Bind ViewModel.LoadPostsCommand}"/>
</core:EventTriggerBehavior>
</interactivity:Interaction.Behaviors>
</ComboBox>
ここでは、ソースを Subreddits
プロパティに、選択された項目を SelectedSubreddit
プロパティにバインドしています。 Subreddits
プロパティは、コレクション自体が変更通知を送信するため 1 回しかバインドされないのに対して、SelectedSubreddit
プロパティは、取得する値を設定から読み込むことと、ユーザーが選択を変更したときにビューモデル内のプロパティを更新することの両方を可能にする必要があるため TwoWay
モードでバインドされることに注意してください。 さらに、選択が変更されるたびにコマンドを呼び出すために XAML の動作を使用しています。
更新ボタン:
<Button Command="{x:Bind ViewModel.LoadPostsCommand}"/>
このコンポーネントはきわめて単純です。単に、カスタム コマンドをボタンの Command
プロパティにバインドして、ユーザーがクリックするたびにこのコマンドが呼び出されるようにしています。
投稿の一覧:
<ListView
ItemsSource="{x:Bind ViewModel.Posts}"
SelectedItem="{x:Bind ViewModel.SelectedPost, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate x:DataType="models:Post">
<Grid>
<TextBlock Text="{x:Bind Title}"/>
<controls:ImageEx Source="{x:Bind Thumbnail}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
ここには、ソースと選択をビューモデル プロパティにバインドする ListView
のほか、使用可能な各投稿を表示するために使用されるテンプレートもあります。 テンプレートで x:Bind
を有効にするために x:DataType
を使用しており、投稿の Title
および Thumbnail
プロパティに直接バインドする 2 つのコントロールがあります。
読み込みバー:
<ProgressBar Visibility="{x:Bind ViewModel.LoadPostsCommand.IsRunning, Mode=OneWay}"/>
ここでは、IAsyncRelayCommand
インターフェイスの一部である IsRunning
プロパティにバインドしています。 AsyncRelayCommand
型は、そのコマンドの非同期操作が開始または完了するたびに、そのプロパティに関する通知の生成を処理します。
最後の欠けているピースは、投稿ウィジェットの UI です。 前と同様に、簡潔にするために、ビューモデルを操作するために必要ではなかった UI 関連のコードをすべて削除しました。 完全なソース コードは、サンプル アプリで入手できます。
<Grid>
<!--Header-->
<Grid>
<TextBlock Text="{x:Bind ViewModel.Post.Title, Mode=OneWay}"/>
<controls:ImageEx Source="{x:Bind ViewModel.Post.Thumbnail, Mode=OneWay}"/>
</Grid>
<!--Content-->
<ScrollViewer>
<TextBlock Text="{x:Bind ViewModel.Post.SelfText, Mode=OneWay}"/>
</ScrollViewer>
</Grid>
ここにはヘッダーだけがあり、Text
および Source
プロパティを Post
モデル内の対応するプロパティにバインドする TextBlock
と ImageEx
コントロール、選択された投稿の (サンプル) コンテンツを表示するために使用される ScrollViewer
内の単純な TextBlock
が含まれています。
サンプル アプリケーション
サンプル アプリケーションは、ここで参照できます。
準備ができました 🚀
これで、すべてのビューモデル、必要なサービス、ウィジェットの UI が構築され、シンプルな Reddit ブラウザーが完成しました。 これは単に、MVVM パターンに従い、MVVM Toolkit の API を使用してアプリを構築する方法の例を示すことを目的としていました。
前に説明したように、これは参考にすぎず、ニーズに合わせてこの構造を変更したり、ライブラリからコンポーネントのサブセットだけを選択したりすることが自由にできます。 採用するアプローチには関係なく、MVVM Toolkit では、MVVM パターンの適切なサポートを可能にするために必要なすべての作業を手動で行うことについて心配しなくても、ビジネス ロジックに集中できるため、新しいアプリケーションを起動するときに即座に運用開始するための堅牢な基礎が提供されます。
MVVM Toolkit