始めに
Xamlを使うプロジェクトでMVVMを扱うフレームワークっていくつかあると思います・・・
- Prism・・・老舗?多機能だけどモジュール化とか以外と面倒。
- ReactiveProperty・・・フレームワークっていうよりプロパティの変更検知に絞ってある感じ?
- ReactiveUI・・・知らんし、情報も少なげ。
という訳でReactiveUIを試してみました。
環境
- WPF & .NET Framework 4.6
- ReactiveUI 8.3
- System.Reactive 4.0
Getting Started
ドキュメントのGetting Startedに(自分の知る限り)日本では流行っていない写真投稿型のSNS「Ficker」のAPIを利用した簡単なサンプルがあるのでそれを写経します。
サンプルのアプリケーションはフォーム上のテキストボックスに検索したい文字列を入力すると、APIをコールして検索結果の投稿写真と詳細をリストボックに表示する感じになっています。
セットアップ
まずは「新しいプロジェクト」→「Windowsデスクトップ」→「WPFアプリ」で新規にWPFフォームアプリケーションを作成します(プロジェクト名はFickerBrowserにしました)。
次にNuGetなんですが、ドキュメントでは「ReactiveUI」だけ入れるように書いてありますが、「System.Reactive.Interfaces」「System.Reactive.Linq」「System.Reactive.PlatformServices」が足りないので追加で入れてます。
書きます
ドキュメントとは順番が前後しますが、MVVMのMから書きます。
FickerのAPIの結果をあてるModelです。結果をあてるだけなのでほぼ空ですが、ViewModel側にあるAPIの結果をModelに落とす部分は役割的にこっちにあってもいい気がします。
namespace FickerBrowser
{
public class FickerPhoto
{
public string Title { get; set; }
public string Description { get; set; }
public string Url { get; set; }
}
}
次に今回のメインとなるViewModelです。こやつのプロパティやコマンドがViewにバインドされます(今回はコマンドはバインドされません)。書き方的にはよくあるMVVMパターンの書き方と大差ありません。
重要なのはコンストラクタにあるプロパティ類の観測部分です(詳細はコード内のコメントを参照してください)。
API周りは今回の趣旨とズレるので解説は省略+HttpUtility
って古いくさいのでWebUtility
に変更。
namespace FickerBrowser
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Xml.Linq;
using ReactiveUI;
public class AppViewModel : ReactiveObject
{
// ↓ こいつらはクラスメンバとして隠匿される
private string searchTerm;
private ObservableAsPropertyHelper<List<FickerPhoto>> searchResults;
private ObservableAsPropertyHelper<Visibility> spinnerVisibility;
// ↑ こいつらはクラスメンバとして隠匿される
// 基本的にコンストラクタでイベントやプロパティの変更処理を定義するのが慣わし・・・なのか?
public AppViewModel()
{
// コマンドに実装を結びつける
this.ExecuteSearch = ReactiveCommand.CreateFromTask<string, List<FickerPhoto>>(searchTerm => GetSearchResultsFromFicker(searchTerm));
// SearchTermの観測
// Throttleは入力完了を待つための遅延だよね?
// DistinctUntilChangedは”Returns an observable sequence that contains only distinct contiguous elements.”とのこと
// 最終的にExecuteSearchを呼ぶ
this.WhenAnyValue(x => x.SearchTerm)
.Throttle(TimeSpan.FromMilliseconds(800), RxApp.MainThreadScheduler)
.Select(x => x?.Trim())
.DistinctUntilChanged()
.Where(x => !string.IsNullOrWhiteSpace(x))
.InvokeCommand(this.ExecuteSearch);
// スピナーはExecuteSearchが実行されている間(IsExecuting)、プロパティを変化させる
// ExecuteSearchを観測しているとも言える
this.spinnerVisibility = this.ExecuteSearch.IsExecuting
.Select(x => x ? Visibility.Visible : Visibility.Collapsed)
.ToProperty(this, x => x.SpinnerVisibility, Visibility.Hidden);
// 検索結果にExecuteSearchの結果(List<FickerPhoto>)を渡す
// こちらもまたExecuteSearchを観測しているとも言える
this.searchResults = this.ExecuteSearch.ToProperty(this, x => x.SearchResults, new List<FickerPhoto>());
// ExecuteSearchのエラーはこうやってハンドルするらしい
// 解放処理(Unsbscribe)はいらんのだろうか?
this.ExecuteSearch.ThrownExceptions.Subscribe(ex => {/* Handle errors here */});
}
/// <summary>
/// 検索フレーズ
/// </summary>
public string SearchTerm
{
get
{
return this.searchTerm;
}
set
{
// 変更の通知が起きる = Reactive
this.RaiseAndSetIfChanged(ref this.searchTerm, value);
}
}
/// <summary>
/// 検索結果
/// </summary>
public List<FickerPhoto> SearchResults => this.searchResults.Value;
/// <summary>
/// スピナー("..."というTextBlock)の表示プロパティ
/// </summary>
public Visibility SpinnerVisibility => this.spinnerVisibility.Value;
/// <summary>
/// 検索実行コマンド
/// </summary>
public ReactiveCommand<string, List<FickerPhoto>> ExecuteSearch { get; protected set; }
/// <summary>
/// Ficker検索(非同期)
/// </summary>
/// <param name="searchTerm">検索フレーズ</param>
/// <returns>検索結果</returns>
private static async Task<List<FickerPhoto>> GetSearchResultsFromFicker(string searchTerm)
{
var doc = await Task.Run(() => XDocument.Load(string.Format(
CultureInfo.InvariantCulture,
"http://api.flickr.com/services/feeds/photos_public.gne?tags={0}&format=rss_200",
WebUtility.UrlEncode(searchTerm))));
if (doc.Root == null)
{
return null;
}
var titles = doc.Root.Descendants("{http://search.yahoo.com/mrss/}title")
.Select(x => x.Value);
var tagRegex = new Regex("<[^>]+>", RegexOptions.IgnoreCase);
var descriptions = doc.Root.Descendants("{http://search.yahoo.com/mrss/}description")
.Select(x => tagRegex.Replace(WebUtility.HtmlDecode(x.Value), string.Empty));
var items = titles.Zip(descriptions, (t, d) => new FickerPhoto()
{
Title = t,
Description = d,
}).ToArray();
var urls = doc.Root.Descendants("{http://search.yahoo.com/mrss/}thumbnail")
.Select(x => x.Attributes("url").First().Value);
var ret = items.Zip(urls, (item, url) =>
{
item.Url = url;
return item;
}).ToList();
return ret;
}
}
}
次にViewです。
まずはクラス側。ビューモデルを初期化してビューのコンテキストに紐付けします。
ちゃんと調べてないけどPrismのような自動バインディングは無いっぽい?
namespace FickerBrowser
{
using System.Windows;
public partial class MainWindow : Window
{
public MainWindow()
{
this.ViewModel = new AppViewModel();
this.InitializeComponent();
this.DataContext = this.ViewModel;
}
public AppViewModel ViewModel { get; private set; }
}
}
次にXaml本体。Source
やらText
にあるBinding
がViewModelにバインドしてる部分です。
・・・いつまで経ってもXamlを好きになれん。特にTemplate定義。
<Window x:Class="FickerBrowser.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<DataTemplate x:Key="PhotoDataTemplate">
<Grid MaxHeight="100">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Source="{Binding Url, IsAsync=True}" Margin="6" MaxWidth="128" HorizontalAlignment="Center" VerticalAlignment="Center" />
<StackPanel Grid.Column="1" Margin="6">
<TextBlock FontSize="14" FontWeight="Bold" Text="{Binding Title}" />
<TextBlock FontStyle="Italic" Text="{Binding Description}" TextWrapping="WrapWithOverflow" Margin="6" />
</StackPanel>
</Grid>
</DataTemplate>
</Window.Resources>
<Grid Margin="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock FontSize="16" FontWeight="Bold" VerticalAlignment="Center">Search For:</TextBlock>
<TextBox Grid.Column="1" Margin="6,0,0,0" Text="{Binding SearchTerm, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Column="2" Margin="6,0,0,0" FontSize="16" FontWeight="Bold" Text="..." Visibility="{Binding SpinnerVisibility}" />
<ListBox Grid.ColumnSpan="3" Grid.Row="1" Margin="0,6,0,0" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ItemsSource="{Binding SearchResults}" ItemTemplate="{DynamicResource PhotoDataTemplate}" />
</Grid>
</Window>
といった具合で割と簡単にMVVMの肝になるプロパティの監視と通知が実装できます。
終わりに
複雑になりがちなPrismよりはシンプルになりそうだけど、このサンプルだけじゃReactivePropertyで十分じゃん感じなのでもう少しちゃんとドキュメント読んで試してみたいと思いましたとさ(Chromeでドキュメントのツリーが壊れるの自分だけだろうか・・・)。