はじめまして。
アプリケーション共同開発部 東京開発課の太田川と申します。
11月に入社し、はじめてのお仕事がこのブログ更新になりました!前職では少しだけ Xamarin.Forms をやっていました。
Xamarin.Forms は iOS / Android / Windows Phone の UI が作成できるフレームワークとして有名ですが、違う一面も持っています。
それが MVVM です。MVVM は良く出来たデザインパターンでもありますが、そのロジックを View か ViewModel か Model かのどのレイヤーで実装するかは MVVM を始めるにあたりすごく悩ましいことです(よく Fat ViewModel など言います)。
そのときの知見として Xamarin.Forms ならではの機能があります。私が実装した一例と設計した理由も最後に入れて、紹介いたします。
Behaviors
同じ動作をするコントロールを実装したい場合に、Behavior は大変有効です。
Behavior はコントロールに振る舞いを付与することにより、効果的に見た目を変化させることができます。Xamarin.Forms では コードビハインドやサブクラス化によって同じ機能が実装できますが、Behavior には複雑な依存性がないので再利用や複数付与などができます。
下の例は ブラウザで開くコントロールを Behavior で実装しています。
using System; using Xamarin.Forms; namespace DevBlog { public class OpenURLBehavior : BehaviorBase<View> //BehaviorBaseは、Xamarin公式のサンプルです { public static readonly BindableProperty TargetProperty = BindableProperty.Create("Target", typeof(object), typeof(OpenURLBehavior)); public object TargetParameter { get { return GetValue(TargetProperty); } set { SetValue(TargetProperty, value); } } protected override void OnAttachedTo(View bindable) { base.OnAttachedTo(bindable); var button = bindable as Button; if (button != null) { button.Clicked += bindable_Clicked; } else { var gesture = new TapGestureRecognizer(); gesture.Tapped += bindable_Clicked; bindable.GestureRecognizers.Add(gesture); } } protected override void OnDetachingFrom(View bindable) { base.OnDetachingFrom(bindable); var button = bindable as Button; if (button != null) { bindable.Clicked -= bindable_Clicked; } else { bindable.GestureRecognizers.Clear(); } } void bindable_Clicked(object sender, EventArgs e) { Uri uri; var param = this.TargetParameter?.ToString(); if (Uri.TryCreate(param, UriKind.RelativeOrAbsolute, out uri)) { Device.OpenUri(uri); } } } }
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:DevBlog;assembly=DevBlog" x:Class="DevBlog.DevBlogPage"> <StackLayout> <Button Text="ブラウザで開く" VerticalOptions="CenterAndExpand"> <Button.Behaviors> <local:OpenURLBehavior Target="{Binding Target}" /> </Button.Behaviors> </Button> <Label Text="ブラウザで開く" VerticalOptions="CenterAndExpand" HorizontalOptions="CenterAndExpand"> <Label.Behaviors> <local:OpenURLBehavior Target="{Binding Target}" /> </Label.Behaviors> </Label> </StackLayout> </ContentPage>
iOS サンプル動画
Android サンプル動画
Bindable が実装されている Behavior は存在しないので公式のサンプルを参考にし、実装しています。
このサンプルでは、Command を実装し ViewModel から Device.OpenUri メソッドを呼び出すこともできます。View を指定しているのは、再利用性を高めるためです。サンプルのように同じ振る舞いを View クラスから拡張されている Button や Label から実行できています。他に Image や BoxView にも使用することが出来ます。
Behavior にはこのサンプル以外に EventToCommandBehavior という実装方法があります。今回の趣旨とは違いますので割愛しますが、イベントを付与しコントロールにはない Command を実装することが出来ます。実装は少し複雑ですが、これも再利用するうえで重要な方法になりますので一度試してみてください。
IValueConverter
MVVM で実装すると、View のため データを見やすく変更することが必要になります。
例えば 価格などの金額を表記する場合、int price = 1000 というデータを表示しようと考えると 1,000円と表記するため文字列化する必要があります。これを実装すると ViewModel に 価格のプロパティ( int ) と表示のプロパティ( string ) の2つが必要になります。しかしこれは、同じ意味のデータが2つあるとも考えられます。
そこで2つを持たず、1,000円と表示するために IValueConverter があります。
IValueConverter は ViewModel から渡された値を表示する前に変更する事が可能です。先程の価格の仕様を考えてみると下記のサンプルで実装できます。
using System; using Xamarin.Forms; public class PriceTriggerConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { int num; var v = value?.ToString(); // 文字列 if (int.TryParse(v, out num)) // 数字チェック { if (culture.TwoLetterISOLanguageName.Equals("ja")) // 言語チェック { return string.Format("{0:#,0}円", num); } else { return string.Format("{0:#,0}yen", num); } } return value; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotImplementedException(); //実装の必要なし } }
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:DevBlog;assembly=DevBlog" x:Class="DevBlog.DevBlogPage"> <ContentPage.Resources> <ResourceDictionary> <local:PriceTriggerConverter x:Key="priceTriggerConverter" /> </ResourceDictionary> </ContentPage.Resources> <Label Text="{Binding Price, Converter={StaticResource priceTriggerConverter}}" VerticalOptions="CenterAndExpand" HorizontalOptions="Center" /> </ContentPage>
iOS 英語 サンプル画像
Android 日本語 サンプル画像
※ViewModel は特別な実装が必要ないので割愛します。
IValueConverter の Convert には CultureInfo が引数として渡されるため、今回のサンプルのようなローカライズも可能です。日時の表示や距離の表示等でフォーマット指定するような場面で活用できます。
更に IValueConverter を使用することにより、StringFormat の処理を ViewModel から切り離しています。よって単体テストがしやすい設計にもなっています。
ConvertBack は表示を考えるだけならば実装の必要がありません。双方向のバインディングを考えるときは、このメソッドで逆処理をしてください。
さいごに
2つの機能と実装例を紹介しました。
最初に述べたとおり Xamarin.Forms で実装する際に、どのレイヤーで実装するかというのは悩ましい問題です。
私自身 Fat ViewModel になってしまったことが何度もあります。Fat ViewModel が悪だというつもりはありませんが、一つのクラスが大きくなりすぎるのは望ましくありません。いつもどの部分がネックになっているかを考え、今回の実装例のように修正しています。
私は、Xamarin.Forms の機能はとてもよくできており、活用すればきれいな MVVM になると考えています。今回の実装例以外にもうまく活用できる実装はあります。なので、私自身も Fat ViewModel にならないよう頑張っていきます。
フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!
フェンリル採用チームの Twitter アカウントです。応募前のお問い合わせや、ちょっとした相談ごとなどお気軽にどうぞ!
フェンリルの Facebook ページでは、最新トピックをお知らせしています。よろしければいいね!してください!
Sleipnir の Facebook ページでは、ユーザーの方たちとのコミュニケーションや最新情報の投稿などを行なっています。よろしければいいね!してください!