libたくさんあって、どれをリンクすればいいのかわからないよぉ…ってなったのでメモ
opencv_videoをリンクすればよかった。 ということで以下の行を追加しました。
#pragma comment(lib, "opencv_video2410.lib")
libたくさんあって、どれをリンクすればいいのかわからないよぉ…ってなったのでメモ
opencv_videoをリンクすればよかった。 ということで以下の行を追加しました。
#pragma comment(lib, "opencv_video2410.lib")
明日使うのでメモ。
#include "cv.h"#include "highgui.h"#include <iostream>#include <vector>#ifdef _DEBUG#pragma comment(lib, "opencv_imgproc2410d.lib")#pragma comment(lib, "opencv_core2410d.lib")#pragma comment(lib, "opencv_highgui2410d.lib")#pragma comment(lib, "opencv_video2410d.lib")//Release Mode#else#pragma comment(lib, "opencv_imgproc2410.lib")#pragma comment(lib, "opencv_core2410.lib")#pragma comment(lib, "opencv_highgui2410.lib")#pragma comment(lib, "opencv_video2410.lib")#endifint main(int argc, char** argv) { cv::VideoCapture cap(0); constint cycle = 1000; cv::Mat prevFrame; cv::Size frameSize = prevFrame.size(); cap >> prevFrame; cv::waitKey(cycle); while (1) { cv::Mat frame; cap >> frame; cv::Mat prevFrameGray; cv::Mat currFrameGray; cv::cvtColor(prevFrame, prevFrameGray, CV_RGB2GRAY); cv::cvtColor(frame, currFrameGray, CV_RGB2GRAY); // 特徴点抽出 std::vector<cv::Point2f> prevCorners; std::vector<cv::Point2f> currCorners; cv::goodFeaturesToTrack(prevFrameGray, prevCorners, 20, 0.05, 5.0); cv::goodFeaturesToTrack(currFrameGray, currCorners, 20, 0.05, 5.0); cv::cornerSubPix(prevFrameGray, prevCorners, cv::Size(21, 21), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::COUNT | cv::TermCriteria::EPS, 30, 0.01)); cv::cornerSubPix(currFrameGray, currCorners, cv::Size(21, 21), cv::Size(-1, -1), cv::TermCriteria(cv::TermCriteria::COUNT | cv::TermCriteria::EPS, 30, 0.01)); std::vector<uchar> featuresFound; std::vector<float> featuresErrors; cv::calcOpticalFlowPyrLK( prevFrameGray, currFrameGray, prevCorners, currCorners, featuresFound, featuresErrors); for (int i = 0; i < featuresFound.size(); i++) { cv::Point p1 = cv::Point((int) prevCorners[i].x, (int) prevCorners[i].y); cv::Point p2 = cv::Point((int) currCorners[i].x, (int) currCorners[i].y); cv::line(frame, p1, p2, cv::Scalar(0, 0, 255), 2); } cv::imshow("preview", frame); prevFrame = frame; if (cv::waitKey(cycle) == 27) { break; } } return0; }
特にReactivePropertyでサポートはしてないので自前でやるっきゃないです!ということでこういうクラスを書いてみました。
ReadOnlyReactiveCollection
publicstaticclass ReadOnlyCollectionExtensions { /// <summary>/// ReadOnlyReactiveCollectionをIListAdapterに変換する/// </summary>/// <typeparam name="T"></typeparam>/// <paramname="self"></param>/// <paramname="createRowView">行のデータを表示するためのViewを作る処理</param>/// <paramname="setRowData">行にデータを設定する処理</param>/// <returns></returns>publicstatic IListAdapter ToAdapter<T>(this ReadOnlyReactiveCollection<T> self, Func<View> createRowView, Action<T, View> setRowData) { returnnew ReadOnlyReactiveCollectionAdapter<T>(self, createRowView, setRowData); } } /// <summary>/// ReadOnlyReactiveCollection用のAdapterクラス/// </summary>/// <typeparam name="T"></typeparam>class ReadOnlyReactiveCollectionAdapter<T> : BaseAdapter<T> { // もとになるコレクションprivate ReadOnlyReactiveCollection<T> source; // 行のデータを表示するためのViewを作る処理private Func<View> createRowView; // 行にデータを設定する処理private Action<T, View> setRowData; public ReadOnlyReactiveCollectionAdapter( ReadOnlyReactiveCollection<T> source, Func<View> createRowView, Action<T, View> setRowData) { this.source = source; this.createRowView = createRowView; this.setRowData = setRowData; } publicoverride T this[int position] { get { return source[position]; } } publicoverrideint Count { get { returnthis.source.Count; } } publicoverridelong GetItemId(int position) { return position; } publicoverride View GetView(int position, View convertView, ViewGroup parent) { if (convertView == null) { convertView = this.createRowView(); } this.setRowData(this[position], convertView); return convertView; } }
使い方は簡単です。以下のようなコマンドを実行するとコレクションにデータを追加するだけのViewModelがあったとします。
publicclass MainActivityViewModel { private ObservableCollection<string> source = new ObservableCollection<string> { "a", "b", "c" }; public ReadOnlyReactiveCollection<string> Items { get; private set; } public ReactiveCommand AddItemCommand { get; private set; } public MainActivityViewModel() { this.Items = source.ToReadOnlyReactiveCollection(); this.AddItemCommand = new ReactiveCommand(); this.AddItemCommand.Subscribe(_ => { this.source.Add("item" + DateTime.Now); }); } }
そして、ボタンとListViewを置いた画面でさくっと紐づけ。
[Activity(Label = "App1", MainLauncher = true, Icon = "@drawable/icon")] publicclass MainActivity : Activity { private MainActivityViewModel viewModel = new MainActivityViewModel(); protectedoverridevoid OnCreate(Bundle bundle) { base.OnCreate(bundle); // Set our view from the "main" layout resource SetContentView(Resource.Layout.Main); this.FindViewById<Button>(Resource.Id.button1).Click += viewModel.AddItemCommand.ToEventHandler(); var listView = this.FindViewById<ListView>(Resource.Id.listView1); listView.Adapter = viewModel.Items.ToAdapter( () => LayoutInflater.FromContext(this).Inflate(Resource.Layout.layout1, null), (x, v) => v.FindViewById<TextView>(Resource.Id.textView1).Text = x); this.viewModel.Items.CollectionChangedAsObservable().Subscribe(_ => listView.InvalidateViews()); } }
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="fill_parent"android:layout_height="fill_parent"><Buttonandroid:text="Button"android:layout_width="match_parent"android:layout_height="wrap_content"android:id="@+id/button1" /><ListViewandroid:minWidth="25px"android:minHeight="25px"android:layout_width="match_parent"android:layout_height="match_parent"android:id="@+id/listView1" /></LinearLayout>
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal"android:layout_width="fill_parent"android:layout_height="fill_parent"><ImageViewandroid:src="@android:drawable/ic_menu_gallery"android:layout_width="wrap_content"android:layout_height="match_parent"android:id="@+id/imageView1" /><TextViewandroid:text="Medium Text"android:textAppearance="?android:attr/textAppearanceMedium"android:layout_width="wrap_content"android:layout_height="match_parent"android:id="@+id/textView1" /></LinearLayout>
先日以下のような記事を書きました。
Xamarin.Android + ReactivePropertyでListViewを使う - かずきのBlog@hatena
別にReactivePropertyのコレクションじゃなくて普通のコレクションを対象にしてかけるじゃないということで、仕立て直してnugetに放流しました。毎回作りそうなコードだったので。
NuGet Gallery | Runceel.XamarinAndroid 1.0.0
IList>T<とObservableCollection>T<とReadOnlyReactiveCollection>T<にToAdapterという拡張メソッドを定義しています。ObservableCollectionとReadOnlyObservableCollectionから生成されるAdapterはコレクションに変更があったときに自動でNotifyDataSetChangedが呼び出されます。
ToAdapter拡張メソッドの引数は以下のような感じになってます。
こんな感じで使います。
this.list = new ObservableCollection<string>(); this.ListAdapter = this.list.ToAdapter( (_, __) => LayoutInflater.FromContext(this).Inflate(Android.Resource.Layout.SimpleListItem1, null), (_, x, v) => v.FindViewById<TextView>(Android.Resource.Id.Text1).Text = x);
人生初のGitHubでのPullRequestが来たので取り込みました。まぁ、これはコードとかプロジェクトの依存関係がクリーンになったので大歓迎でした。
本命は、以下の通りです。
今までは、Xamarin.FormsでPCL作った時にデフォルトのプロファイルではReactivePropertyが追加できませんでした。プロファイルを変更することで対応されてました。
Xamarin.Forms と ReactiveProperty で快適MVVM生活 - Qiita
これを、プロファイルを変更しなくても、さくっと追加できるようにしました。なにをしたってnugetのパッケージの定義をきちんとしてなかったのが原因でした…。
これでXamarin.Formsでも簡単にReactivePropertyが使えるようになります。
よくわからないタイトルになってしまいましたが、Xamarin.Forms使おうとして出てきた問題です。
Xamarin.Formsは、現時点ではXAMLのデザイナとか提供されてないので、長い目で見るとXAMLで書いておいたほうがいいのは確かなんですが、勉強するためにインテリセンスがないのはちょいと辛い。なので、勉強するときはC#で軽く組んでおいて感覚をつかんでから、デザイナが出てきたらXAMLに乗り換えるといったことを目論んでいます。
そんなときに、Gridにボタンを置いたり、Bindingを設定したオブジェクトを配置したりといったことをしようとすると、以下のように一度変数に入れて書いたりしてました。めんどい。
// 初期化と Button b = new Button { Text = "Hello" }; Grid.SetRow(b, 1); Grid.SetColumn(b, 1); b.SetBinding(Button.CommandProperty, "Hoge"); Content = new Grid { RowDefinitions = { new RowDefinition { Height = GridLength.Auto }, ... }, Children = { b // 配置が分離してしまうよぉぉぉ } };
変数ができてしまうのが気に食わなかったり、めんどくさいと思ってました。
こんな風にかけることに気付きました。
Content = new Grid { RowDefinitions = { ...省略... }, Children = { new Func<Button>(() => { // 配置と初期化処理が同じところにかけるし無駄に変数が増えない! var b = new Button { Text = "Hello" }; Grid.SetRow(b, 1); Grid.SetColumn(b, 1); b.SetBinding(Button.CommandProperty, "Hoge"); return b; }).Invoke() } };
よりXAMLに近いイメージで書けそう。
ReactivePropertyは、XAMLで使うのがしっくりきますが、Codeplex.Reactive.Extensions名前空間には、それ以外にも使える便利なメソッドが詰まってます。その中でも汎用的なINotifyPropertyChangedとINotifyCollectionChangedを監視するメソッドを紹介します。
ObservePropertyという拡張メソッドがあります。これはExpressionTreeで監視対象のプロパティを指定することで、そのプロパティを監視するIObservable>T<を作成できます。
例を以下に示します。
このようなPersonクラスがあるとして…
publicclass Person : INotifyPropertyChanged { publicevent PropertyChangedEventHandler PropertyChanged; privatevoid OnPropertyChanged([CallerMemberName]string propertyName = null) { var h = this.PropertyChanged; if (h != null) { h(this, new PropertyChangedEventArgs(propertyName)); } } privatestring name; publicstring Name { get { returnthis.name; } set { this.name = value; this.OnPropertyChanged(); } } }
Nameプロパティを監視するコードはこうなります。
var p = new Person(); p.ObserveProperty(x => x.Name) .Subscribe(x => Console.WriteLine("変更されました {0}", x)); p.Name = "tanaka"; p.Name = "kimura";
実行すると以下のようになります。
変更されました 変更されました tanaka 変更されました kimura
最初のSubscribeした時点での値がいらなかったらSkip(1)すればいいです。
INotifyCollectionChangedのを監視するのは、CollectionChangedAsObservable拡張メソッドです。
これは、CollectionChangedイベントが発行されるたびに値を通知するIObservable>NotifyCollectionChangedEventArgs<を返します。使い方は以下のようになります。
var c = new ObservableCollection<Person>(); c.CollectionChangedAsObservable() .Subscribe(x => Console.WriteLine("コレクションに変更がありました")); c.Add(new Person { Name = "tanaka" }); c.Add(new Person { Name = "kimura" });
実行するとこんな結果になります。
コレクションに変更がありました コレクションに変更がありました
ObservableCollection>T<限定になりますが、タイプセーフな監視メソッドも用意されています。
ObserveXXXXChangedメソッドでXXXXにはAddやRemoveやReplaceやMoveやResetなどがあります。これは、NotifyCollectionChangedEventArgsを用途ごとに使いやすいように加工したものになっています。
ReactiveProperty全部つかうのが重たかったら、該当メソッドだけ切り出して使うのもありかもしれないですね。OSSですし。
Prism.Mvvmは非常にシンプルなMVVMをサポートするライブラリです。Xamarin.Formsでも使わない手はない!ということで使ってみました。
Windows Phoneのプロジェクトを消します。日本で出てないし、Prism.Mvvmサポートしてないプラットフォームなので泣く泣く…。この時点で私はAndroidしか試せないのでiOSも消してしまいました。
PCLのプロパティを開いてWindows Phoneのチェックを外しておきます。
Prism.Mvvmで検索してPCLとAndroidのプロジェクト両方にPrism.Mvvmを追加します。
Viewsという名前のフォルダを作ってForms Xaml Pageを作成します。
Prism.MvvmのVとVMの紐づけ機能を使うには、ViewがIViewインターフェースを実装してないといけないので、これを実装します。DataContextというプロパティが必要になるのでBindingContextをラップする形で実装します。ついでに、コンストラクタにViewModelLocationProviderのAutoWireViewModelChangedを呼び出しておいて、紐づけ機能を有効にしておきます。
using Microsoft.Practices.Prism.Mvvm; using Xamarin.Forms; namespace App8.Views { publicpartialclass MainPage : ContentPage, IView { public MainPage() { InitializeComponent(); ViewModelLocationProvider.AutoWireViewModelChanged(this); } publicobject DataContext { get { returnthis.BindingContext; } set { this.BindingContext = value; } } } }
ViewModelsフォルダを作って、そこにViewのクラス名+ViewModelという命名規約でViewModelを作成します。この例ではMainPageViewModelですね。とりあえず、適当に。
using Microsoft.Practices.Prism.Mvvm; using System.Windows.Input; using Xamarin.Forms; namespace App8.ViewModels { publicclass MainPageViewModel : BindableBase { privatestring mainText; publicstring MainText { get { returnthis.mainText; } set { this.SetProperty(refthis.mainText, value); } } public ICommand HelloCommand { get; private set; } public MainPageViewModel() { this.MainText = "Hello world"; this.HelloCommand = new Command(() => this.MainText = "こんにちは世界"); } } }
さて、Viewに戻ってViewModelに紐づくXAMLを書いていきます。といっても、デフォルトでMainTextにバインドされてるLabelが用意されてるので、ここではStackLayoutとButtonを追加しただけです。Xamarin Studioだと若干のインテリセンスが効くので精神衛生にいいですが、Visual Studioで書くと発狂しそうになります。ここらへん、そのうち出てくるであろうデザイナに期待したいところですね。
<?xml version="1.0" encoding="utf-8"?><ContentPage xmlns="http://xamarin.com/schemas/2014/forms"xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"x:Class="App8.Views.MainPage"><StackLayout><Label Text="{Binding MainText}"VerticalOptions="Center"HorizontalOptions="Center" /><Button Text="Hello"Command="{Binding HelloCommand}" /></StackLayout></ContentPage>
App.csのMainPageを、先ほど作成したMainPageクラスに差し替えます。
public App() { // The root page of your application MainPage = new MainPage(); }
実行すると以下のようになります。
Prism.MvvmのVとVMの連携は、UnityによるDIと連携するとうれしいと個人的に思ってます。UnityをプロジェクトにNuGetから追加します。
ついでに、Prism.PubSubEventsも追加しておきます。Prism.PubSubEventsのクラスをVMにDIしてみたいと思います。
App.csのコンストラクタでUnityの初期化とViewModelのインスタンス化の処理の書き換えを行います。
public App() { var c = new UnityContainer(); // EventAggregatorをシングルトンで管理してもらう c.RegisterType<IEventAggregator, EventAggregator>(new ContainerControlledLifetimeManager()); // ViewModelをUnityから取得するようにする ViewModelLocationProvider.SetDefaultViewModelFactory(t => c.Resolve(t)); MainPage = new MainPage(); }
ViewModelのコンストラクタなどでIEventAggregatorを受け取るようにするとUnityが勝手にインスタンスを設定してくれます。
public MainPageViewModel(IEventAggregator eventAggregator)
Modelのルート要素をDIしてもらったり、設定情報を保持するクラスをDIしてもらったりとか色々便利です。オブジェクトのインスタンスの管理をお任せできるので個人的に気に入ってます。
Prism.Mvvm + UnityはXamarin.Formsでも使える(ただしWindows Phoneは除く)
ということで、コードレシピにアップしました。疲れた疲れた。
Xamarin.FormsでPrism.MvvmとReactivePropertyを使ったサンプル in C# for Visual Studio 2013
ソファーでうたたねしてて目覚めたらこんな記事が!
これはきた!!!新規作成したプロジェクトで試したらインテリセンスが効いたけど、既存プロジェクトでは効かなかったから、寝て起きたら、そこらへん調査してみよう。寝て起きたら既存プロジェクトでも効くようになってないかなぁ。
Xamarin.FormsのNavigationPageのアイコンの変え方(Android) - かずきのBlog@hatena
デフォルトのXamarinのアイコンじゃなくて独自アイコンにしようと思ってNavigationPage内のページにNavigationPage.TitleIcon添付プロパティをセットしたんです。
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:App10.ViewModels;assembly=App10" x:Class="App10.Views.MainPage" NavigationPage.TitleIcon="bird.png"> <StackLayout> <Label Text="okokokokook" /> </StackLayout> </ContentPage>
Androidで実行するとアイコンが左じゃなくて真ん中の変な位置に…。
左にこないのはバグなのか、やりかたが間違ってるのか悩む…。
以前に、こんな記事を書きました。
Xamarin.FormsのNavigationPageのアイコンの変え方が知りたい - かずきのBlog@hatena
この記事では、アイコンの表示位置がおかしくなるということを言ってたのですが、私が用意したアイコンが大きかったのが原因でしたorz
72x72のサイズのアイコンを用意するとちゃんとなりました。ということで改めて。
Resources/drawableにデフォルトで用意されてるIcon.pngと同じサイズの画像を用意します。ここではbird.pngという画像を72x72ピクセルで用意しました。ビルドアクションは、AndroidResourceにします。
App.csを書き換えてNavigationPageを使うようにします。
今回用意したアイコンが黒で絵が描いてあるものだったのでバーの色を緑にしてみました。
public App() { // The root page of your application MainPage = new NavigationPage(new MainPage()) { BarBackgroundColor = Color.Green, }; }
BarTextColorプロパティが効かない気がするのですが、また別の話。
XAMLで(XAMLじゃなくてもいいんですが)MainPageを用意します。MainPageにNavigationPage.TitleIcon添付プロパティ(添付プロパティって呼び名あってるのかな)を設定します。先ほど用意したbird.pngを設定しておきます。
<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewModels="clr-namespace:App10.ViewModels;assembly=App10" x:Class="App10.Views.MainPage" NavigationPage.TitleIcon="bird.png" Title="こんにちは"> <ContentPage.ToolbarItems> <ToolbarItem Text="Hello world" /> </ContentPage.ToolbarItems> <StackLayout> <Label Text="okokokokook" /> <Button Text="Hello" Clicked="Button_Clicked" /> </StackLayout> </ContentPage>```
画面の中身は適当で重要なのはTitleIconをセットしてるところです。
実行すると以下のように鳥のアイコンがきちんと表示されます。めでたしめでたし。
assemblyにApplicationAttributeをつけて、Themeプロパティを設定するらしい。
Lolipopのマテリアルの白にしたいなら、こんな感じ。
[assembly: Application(Icon = "@drawable/bird", Theme = "@android:style/Theme.Material.Light")]
ここらへんのテーマの一覧ってどこにあるんだろう…。軽く探した感じだとわからなかったorz
Prism.Mvvmのほうが好みなんですが、こいつがSilverlight for Windows Phone 8をサポートしないので、SL for WP8もサポートしてる(すごいよね…)MVVM Light Toolkitを試してみました。
SimpleIoCというDIコンテナがついてるけど、個人的にUnity推しなので、Unity使うようにしてViewModelLocatorを作りました。こんな感じで。
using App13.ViewModels; using Microsoft.Practices.ServiceLocation; using Microsoft.Practices.Unity; namespace App13 { publicstaticclass ViewModelLocator { static ViewModelLocator() { var c = new UnityContainer(); ServiceLocator.SetLocatorProvider(new ServiceLocatorProvider(() => new UnityServiceLocator(c))); } publicstatic MainViewModel Main { get { return ServiceLocator.Current.GetInstance<MainViewModel>(); } } } }
コマンド押したらダイアログが出るイメージです。
using Codeplex.Reactive; using GalaSoft.MvvmLight; using GalaSoft.MvvmLight.Messaging; using System; namespace App13.ViewModels { publicclass MainViewModel : ViewModelBase { public ReactiveProperty<string> Label { get; private set; } public ReactiveCommand AlertCommand { get; private set; } public MainViewModel() { this.Label = new ReactiveProperty<string>("Hello world"); this.AlertCommand = new ReactiveCommand(); this.AlertCommand.Subscribe(_ => this.MessengerInstance.Send(new NotificationMessage("こんにちは!"))); } } }
これでいいのか自信がないけどつくってみました。
using GalaSoft.MvvmLight.Messaging; using Xamarin.Forms; namespace App13.Services { publicclass DialogBehavior : Behavior<Page> { private Page page; protectedoverridevoid OnAttachedTo(Page bindable) { base.OnAttachedTo(bindable); this.page = bindable; Messenger.Default.Register<NotificationMessage>(this, this.Alert); } publicvoid Alert(NotificationMessage message) { this.page.DisplayAlert("メッセージ", message.Notification, "OK"); } protectedoverridevoid OnDetachingFrom(Page bindable) { base.OnDetachingFrom(bindable); Messenger.Default.Unregister<NotificationMessage>(this); } } }
XAMLでさくっとね。
<?xml version="1.0" encoding="utf-8"?><ContentPage xmlns="http://xamarin.com/schemas/2014/forms"xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"xmlns:local="clr-namespace:App13;assembly=App13"xmlns:services="clr-namespace:App13.Services;assembly=App13"x:Class="App13.Views.MainPage"BindingContext="{x:Static local:ViewModelLocator.Main}"><ContentPage.Padding><OnPlatform x:TypeArguments="Thickness"iOS="0, 20, 0, 0" /></ContentPage.Padding><ContentPage.Behaviors><services:DialogBehavior /></ContentPage.Behaviors><StackLayout><Label Text="{Binding Label.Value}" /><Button Text="こんにちは"Command="{Binding AlertCommand}" /></StackLayout></ContentPage>
App.csのMainPageの設定を上で作ったページに書き換えます。これは忘れがち。
using App13.Views; using Xamarin.Forms; namespace App13 { publicclass App : Application { public App() { // The root page of your application MainPage = new MainPage(); } protectedoverridevoid OnStart() { // Handle when your app starts } protectedoverridevoid OnSleep() { // Handle when your app sleeps } protectedoverridevoid OnResume() { // Handle when your app resumes } } }
実行してボタンを押すとダイアログが出た!
とりあえず。
以下のようなVMがあったとして
using Codeplex.Reactive; using System; using System.Reactive.Linq; namespace App15 { publicclass MainPageViewModel { public ReactiveProperty<string> Input { get; private set; } public ReactiveProperty<string> Output { get; private set; } public ReactiveCommand ClearCommand { get; private set; } public MainPageViewModel() { this.Input = new ReactiveProperty<string>(); this.Output = Observable.Merge( this.Input.Where(x => string.IsNullOrEmpty(x)).Select(_ => ""), this.Input.Where(x => !string.IsNullOrEmpty(x)).Select(x => x.ToUpper())) .ToReactiveProperty(); this.ClearCommand = new ReactiveCommand(); this.ClearCommand.Subscribe(_ => this.Input.Value = ""); } } }
こんな感じでActivityとつなぐことができます。
using System; using Android.App; using Android.Content; using Android.Runtime; using Android.Views; using Android.Widget; using Android.OS; using Codeplex.Reactive.Extensions; using ReactiveProperty.XamarinAndroid; using ReactiveProperty.XamarinAndroid.Extensions; namespace App15 { [Activity(Label = "App15", MainLauncher = true, Icon = "@drawable/icon")] publicclass MainActivity : Activity { privatereadonly MainPageViewModel viewModel = new MainPageViewModel(); protectedoverridevoid OnCreate(Bundle bundle) { base.OnCreate(bundle); // Set our view from the "main" layout resource SetContentView(Resource.Layout.Main); // TwoWay bindingthis.SetBinding( // 対象のID Resource.Id.EditTextInput, // 対象のプロパティ (EditText x) => x.Text, // ソースのReactivePropertythis.viewModel.Input, // 更新タイミングのIO<Unit> (EditText x) => x.TextChangedAsObservable().ToUnit()); // OneWay bindingthis.SetBinding( // 対象のID Resource.Id.TextViewOutput, // 対象のプロパティ (TextView x) => x.Text, // ソースのReactivePropertythis.viewModel.Output); // Command bindingthis.FindViewById<Button>(Resource.Id.ButtonClear) .ClickAsObservable() // IO<T>の発火でコマンドをキックする .SetCommand(this.viewModel.ClearCommand); } } }
コレクション系は、前につくったライブラリのソースをごそっと持ってきたので、各種コレクションに対してToAdapterでIListAdapterがゲットできます。
Xamarin AndroidでIListやObservableCollectionをListViewに表示する - かずきのBlog@hatena
です。さくっと作った割には少し気に入ってるので手を入れていってみようかな。
NuGetに放流してあります。
A simple master detail application with ReactiveProperty. in C# for Visual Studio 2013
これくらいでいけるなら、個人的には許容範囲かもってお思えるものができたので満足満足。
MVVMでアプリ組んでるとModelとViewModelで似た構造のクラスを作って、値の移し替えを行うことがあります。AutoMapperとか使ってもいいのですが、ReactivePropertyを使うことでも楽をすることができます。
以下のようなModelクラスがあるとします。(BindableBaseクラスはPrismのINotifyPropertyChangedを実装したクラスです)
publicclass Person : BindableBase { privatestring name; publicstring Name { get { returnthis.name; } set { this.SetProperty(refthis.name, value); } } privateint age; publicint Age { get { returnthis.age; } set { this.SetProperty(refthis.age, value); } } }
上記のクラスは2プロパティしかないですが、こんな感じでINotifyPropertyChangedの変更通知に対応したプロパティをたくさんもっているクラスがModelの中にはあります。
ModelからViewModelへの値の移し替えは、Codeplex.Reactive.Extensions名前空間に定義されているObserveProperty拡張メソッドを使うことで簡単に定義できます。
publicclass PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model .ObserveProperty(x => x.Name) // IObservable<string>に変換 .ToReactiveProperty(); // ReactiveProperty<string>に変換this.Age = model .ObserveProperty(x => x.Age) // IObservable<int>に変換 .Select(x => x.ToString()) // IObservable<string>に変換 .ToReactiveProperty(); // ReactiveProperty<string>に変換 } }
これで、Personクラスのプロパティが書き換わると、ViewModelのプロパティが書き換わる処理が定義できます。
var p = new Person { Name = "tanaka" }; var vm = new PersonViewModel(p); Console.WriteLine(vm.Name.Value); // tanaka p.Name = "kimura"; Console.WriteLine(vm.Name.Value); // kimura p.Age = 10; Console.WriteLine(vm.Age.Value); // 10
Model → ViewModel間の一方向データバインディングと見ることもできます。
単純なケースでは、MとVM間のプロパティを双方向でバインドするToReactivePropertyAsSynchronizedメソッドを提供しています。
publicclass PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model .ToReactivePropertyAsSynchronized(x => x.Name); // ReactiveProperty<string>に変換this.Age = model .ToReactivePropertyAsSynchronized( x => x.Age, convert: x => x.ToString(), // M -> VMの変換処理 convertBack: x => // VM -> Mの変換処理 { try { returnint.Parse(x); } catch { return -1; // error } }); } }
var p = new Person { Name = "tanaka", Age = 10 }; var vm = new PersonViewModel(p); Console.WriteLine("{0} {1}", vm.Name.Value, vm.Age.Value); // tanaka 10 vm.Name.Value = "kimura"; vm.Age.Value = "30"; Console.WriteLine("{0} {1}", p.Name, p.Age); // kimura 30 vm.Age.Value = "xxx"; // error!! Console.WriteLine("{0} {1}", p.Name, p.Age); // kimura -1
ToReactivePropertyAsSynchronized拡張メソッドは、シンプルな双方向のバインディングをサポートしていますが、エラーがあったらModelのデータを書き換えたくないという要望には対応していません。そこは、少しめんどくさいですが、自前で対応するしか無いです。
まず、順を追ってReactivePropertyの値の検証から説明します。ReactivePropertyのSetValidateNotifyErrorメソッドでReactivePropertyの値の検証が出来ます。(そのほかにも色々提供していますが一番単純な奴で)エラーがある場合はエラーメッセージを返して、エラーがない場合はnullを返すようにします。
publicclass PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model .ObserveProperty(x => x.Name) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // 空文字の時はエラーにするstring.IsNullOrWhiteSpace(x) ? "Name is required" : null); this.Age = model .ObserveProperty(x => x.Age) // IObservable<int>に変換して .Select(x => x.ToString()) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // int型に変換できない場合はエラーにする { int result; // no usereturnint.TryParse(x, out result) ? null : "Error"; }); } }
プロパティのエラーの有無は、HasErrorsプロパティで確認できます。
var p = new Person { Name = "tanaka", Age = 10 }; var vm = new PersonViewModel(p); Console.WriteLine(vm.Name.HasErrors); // False vm.Name.Value = ""; // Error! Console.WriteLine(vm.Name.HasErrors); // True Console.WriteLine(vm.Age.HasErrors); // False vm.Age.Value = "xxx"; // Error! Console.WriteLine(vm.Age.HasErrors); // True
エラーメッセージは、WPFの場合はValidation.Errors添付プロパティのErrorContentで取得できるのでXAMLで完結できます。WPF以外のプラットフォームでは、特に検証エラーのサポートが無いので多少めんどくさい手順を踏むことにになります。手順は別記事に譲ります。
VMのプロパティにエラーが無いとき、つまりReactivePropertyのHasErrorsプロパティがFalseの時に、VM → Mに値を移せばいいということになります。
publicclass PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model // M -> VM .ObserveProperty(x => x.Name) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // 空文字の時はエラーにするstring.IsNullOrWhiteSpace(x) ? "Name is required" : null); this.Name // VM -> M .Where(_ => !this.Name.HasErrors) // エラーが無いときは .Subscribe(x => model.Name = x); // 値を書き戻すthis.Age = model // M -> VM .ObserveProperty(x => x.Age) // IObservable<int>に変換して .Select(x => x.ToString()) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // int型に変換できない場合はエラーにする { int result; // no usereturnint.TryParse(x, out result) ? null : ""; }); this.Age // VM -> M .Where(_ => !this.Age.HasErrors) // エラーが無いときは .Select(x => int.Parse(x)) // int型に変換して .Subscribe(x => model.Age = x); // 書き戻す } }
これで、バリデーションエラーのないときだけVM → Mへ値を書き戻す処理のあるViewModelが出来ました。
var p = new Person { Name = "tanaka", Age = 10 }; var vm = new PersonViewModel(p); Console.WriteLine(p.Name); // tanaka vm.Name.Value = "kimura"; Console.WriteLine(p.Name); // kimura vm.Name.Value = ""; // Error! Console.WriteLine(p.Name); // kimura
MとVMの値の連携がRxで簡単に出来るようになりました。ただ、通常は、単一のオブジェクトだけではなく、コレクションに入ったModelをVMのコレクションに変換するといった処理がよくあります。
ReactivePropertyでは、これを単純化するためにReadOnlyReactiveCollectionクラスを提供しています。これは、ObservableCollectionから簡単に作ることができます。
var m = new ObservableCollection<Person>(); var vm = m.ToReadOnlyReactiveCollection(x => new PersonViewModel(x)); // M -> VMの変換処理を渡すと後はよろしくしてくれる m.Add(new Person { Name = "tanaka", Age = 30 }); Console.WriteLine(vm[0].Name.Value); // tanaka m[0].Name = "kimura"; Console.WriteLine(vm[0].Name.Value); // kimura m.Add(new Person { Name = "nakata", Age = 40 }); Console.WriteLine(vm[1].Name.Value); // nakata
ReactiveProperty オーバービュー - かずきのBlog@hatena
ReactiveProperty/README-ja.md at master · runceel/ReactiveProperty · GitHub
先日書いた記事で、めんどくさいと思ってた部分を簡単にかけるようにしました。
MVVMでめんどくさいと思ってる部分を、個人的にどうやって緩和してるか - かずきのBlog@hatena
データのバリデーションンを伴うMとVMのプロパティの同期は以下のように書く必要がありました。
publicclass PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model // M -> VM .ObserveProperty(x => x.Name) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // 空文字の時はエラーにするstring.IsNullOrWhiteSpace(x) ? "Name is required" : null); this.Name // VM -> M .Where(_ => !this.Name.HasErrors) // エラーが無いときは .Subscribe(x => model.Name = x); // 値を書き戻すthis.Age = model // M -> VM .ObserveProperty(x => x.Age) // IObservable<int>に変換して .Select(x => x.ToString()) // IObservable<string>に変換して .ToReactiveProperty() // ReactiveProperty<string>に変換して .SetValidateNotifyError(x => // int型に変換できない場合はエラーにする { int result; // no usereturnint.TryParse(x, out result) ? null : ""; }); this.Age // VM -> M .Where(_ => !this.Age.HasErrors) // エラーが無いときは .Select(x => int.Parse(x)) // int型に変換して .Subscribe(x => model.Age = x); // 書き戻す } }
これを、ToReactivePropertyAsSynchronizedメソッドにバリデーションエラーのときは値を無視するようにするオプションを追加して以下のようにかけるようにしました。
publicclass PersonViewModel { public ReactiveProperty<string> Name { get; private set; } public ReactiveProperty<string> Age { get; private set; } public PersonViewModel(Person model) { this.Name = model .ToReactivePropertyAsSynchronized( x => x.Name, ignoreValidationErrorValue:true) .SetValidateNotifyError(x => string.IsNullOrEmpty(x) ? "error": null); this.Age = model .ToReactivePropertyAsSynchronized( x => x.Age, convert: x => x.ToString(), convertBack: x => int.Parse(x), ignoreValidationErrorValue:true) .SetValidateNotifyError(x => { int result; // no usereturnint.TryParse(x, out result) ? null : "error"; }); } }
今まではバリデーションエラーが、あろうが無かろうが、Mへ値が渡されてたのですが(しかも、convertBackに変な値がわたって例外がでると死ぬ)バリデーションエラーのときは値を流さないようにしました。個人的に気に入ってる。
動作は以下のようになります。
var model = new Person { Name = "tanaka", Age = 30 }; var vm = new PersonViewModel(model); Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 30 vm.Age.Value = "50"; // valid value Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 50 vm.Age.Value = "XX"; // invalid value Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 50 vm.Age.Value = "30"; // valid value Console.WriteLine("{0} {1}", model.Name, model.Age); // tanaka 30
NuGet Gallery | ReactiveProperty 2.0.0-pre1
既存プロジェクトのアップグレードの際には以下の名前空間の置換を行ってください。
Xamarin.AndroidのViewに対するSetBinding拡張メソッドと、Viewを継承したクラスのイベントをIObservableに変換する拡張メソッドと、IObservableにコマンドをセットするSetCommand拡張メソッドが主な機能です。
詳細は、以下のページを参照してください。
ReactiveProperty/README-Android.md at vNext · runceel/ReactiveProperty · GitHub
以下の3つのMVVMライブラリと
ReactiveProperty(v2系使ってます)のサンプルをコードレシピに書きました。どれも、ほぼ同じ挙動をするサンプルなので比較用にでもどうぞ。
所感としては、WPFならLivetが一番機能が豊富で次点でPrismで一番機能が少ないのがMVVM Light toolkitです。対応プラットフォームが多いのは、MVVM Light toolkit、Prism、Livetの順番です。
機能特化か、汎用をとるかケースバイケースで自分にあったものを選ぶのがいいと思います。