Xamarin逆引きTips

Xamarin逆引きTips

MvvmCrossでカスタムコンバーターを作成するには?

2015年5月27日

MvvmCrossでのiOS/Androidアプリ開発において、バインディングする値を変換できるカスタムコンバーターの使い方を説明する。

  • このエントリーをはてなブックマークに追加

 MvvmCrossのiOS/Androidアプリ開発では、カスタムコンバーターを用いることでバインディングする値を変換できる。今回は、そのカスタムコンバーターを作成し、iOS/Android各プラットフォームから、それを利用する方法を解説する*1

  • *1 なお、本TipsはMac OS X(10.10.3)、Xamarin Studio(5.9.1)、MvvmCross(3.5.0)で動作を確認している(編集部注: Windows上のVisual Studioでも同様の手順で、本稿の内容が実現できることは確認している)。

コンバーターとは

 Value Converter(以下、コンバーター)とは、MvvmCrossの機能の一つで、プロパティバインディング時に、その値を変換する仕組みである。ViewModelプロパティからViewプロパティへの変換と、ViewプロパティからViewModelプロパティへの変換の双方向に対応している。

 コンバーターは、「ViewModelの論理的な値」と「Viewへ表示したい値」に差があるときに活躍する。例えば、ViewModelにコンテンツの投稿時刻をDateTime型のプロパティとして保持しておき、Viewではstringプロパティとして“12:01”のような“時:分”形式の文字列を受け取る場合などである。また、コンバーターを差し替えることで、ViewModelを変更することなく、“5/17 12:01”(月/日 時:分)などの表示へ変更することもできる。

 コンバーターを利用するプロジェクトは、基本的に図1のようになる。

図1 コンバーターの所属するプロジェクト

 図1の通り、コンバーターはCoreプロジェクトに実装することも、TouchプロジェクトとDroidプロジェクト、それぞれで実装することもできる。同じデータであっても、プラットフォームごとに異なる表示をさせたり、プラットフォームごとの画像リソースを表示したりするといった対応が可能となる。

 ViewModelの値を変換してViewへ渡せる仕組みであるため、コンバーターは画面作成時に活躍するが、パフォーマンスがシビアな局面では注意が必要だ。例えばiOSのUITableViewやAndroidのListViewなどのリスト形式のViewは、リストアイテムとなるViewが再利用されるため、スクロールするたびにバインディングが発生する。コンバーターはバインディングの都度、実行されるため、リストスクロールの負荷が高くなることがある。

実装方針

 それではコンバーターのサンプルを作成してみよう。Coreプロジェクトへ実装する(プラットフォーム間で共通の)コンバーターと、プラットフォームごとに実装するコンバーターを順に実装する。

 はじめに、入力された文字数をカウントするTextCountコンバーターを、Coreプロジェクトに実装する。

 次に、入力された文字数が10文字以上であれば色付きで文字数を表示するCountColorコンバーターを、プラットフォームごとのプロジェクトで実装する。

Coreプロジェクトコンバーターの実装

プロジェクトの作成

 「Tips:MvvmCrossのプロジェクトをセットアップするには?」の手順に従い、MvvmCrossプロジェクトを作成する。ソリューション名は「CrossConverterSample」と設定する。

Coreプロジェクトの実装

 FirstViewModelクラスは、プロジェクト作成時にあらかじめ実装されているHelloプロパティを使用するため、変更の必要はない。

 さっそくコンバータークラスを作成する。CrossConverterSample.CoreプロジェクトのルートディレクトリへConvertersディレクトリを作成し、その下へMvxTextCountConverter.csファイルを作成する(図2)。

図2 MvxTextCountConverter.csの構成

 MvxTextCountConverterクラスは次のように実装する。

C#
using System;
using Cirrious.CrossCore.Converters;

namespace CrossConverterSample.Core.Converters
{
  public class MvxTextCountConverter : MvxValueConverter<string, string>
  {
    protected override string Convert (string value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      int count = value.Length;
      return string.Format ("{0} count", count);
    }
  }
}
プラットフォーム間で共通のコンバーター(MvxTextCountConverter.cs)

 コンバータークラスはMvvmCrossに用意されているIMvxValueConverterインターフェースを実装する。Convertメソッドにより、ViewModelからViewへ値が渡されるときのコンバート処理を実装し、ConvertBackメソッドにより、ViewからViewModelへ値が渡されるときのコンバート処理を実装する。

 ただし、IMvxValueConverterインターフェースではConvertメソッドとConvertBackメソッドの引数はobject型であり、メソッド内でキャストが必要となる。バインドされる型が限定されている場合には、MvxValueConverterクラスを型クラス指定ありで継承することで引数の型を限定できる。今回は、string型からstring型へのコンバーターとして実装している。

 MvxTextCountConverterはViewModelからViewへの一方向のバインディングであるため、Convertメソッドのみ実装している。このコンバーターは渡されたプロパティの文字数を"{0} count"のフォーマットで返す。

コンバータークラス名について

 MvvmCrossはアプリ起動時に、IMvxValueConverterを継承したクラスをコンバータークラスとして列挙する。このとき、コンバータークラス名は次のようなものとなる。

  • TextCount
  • TextCountValueConverter
  • TextCountConverter
  • MvxTextCountValueConverter
  • MvxTextCountConverter

 これらのクラスは全て「TextCount」の名前でコンバーターとして登録される。

 コンバータークラスはCoreプロジェクトのものとプラットフォームごとのプロジェクトのものとで同じ名前空間で識別される。もし同一の名前のコンバータークラスが複数存在した場合には、最後に登録されたコンバータークラスが有効となる。

Touchプロジェクトの実装

 Touchプロジェクトではレイアウトを編集し、コンバーターを使用したバインディングを定義する。

C#
using Cirrious.MvvmCross.Binding.BindingContext;
using Cirrious.MvvmCross.Touch.Views;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;

namespace CrossConverterSample.Touch.Views
{
  [Register("FirstView")]
  public class FirstView : MvxViewController
  {
    public override void ViewDidLoad()
    {
      View = new UIView { BackgroundColor = UIColor.White };
      base.ViewDidLoad();

      // ios7 layout
      if (RespondsToSelector(new Selector("edgesForExtendedLayout")))
      {
         EdgesForExtendedLayout = UIRectEdge.None;
      }
   
      var label = new UILabel(new CGRect(10, 10, 300, 40));
      Add(label);
      var textField = new UITextField(new CGRect(10, 50, 300, 40));
      Add(textField);
      // 1
      var labelCount = new UILabel(new CGRect(10, 90, 300, 40));
      Add (labelCount);

      var set = this.CreateBindingSet<FirstView, Core.ViewModels.FirstViewModel>();
      set.Bind(label).To(vm => vm.Hello);
      set.Bind(textField).To(vm => vm.Hello);
      set.Bind(labelCount).To(vm => vm.Hello).WithConversion("TextCount");  // 2
      set.Apply();
    }
  }
}
Touchプロジェクトのレイアウトを作成する(FirstView.cs)

 1で文字数を表示するラベルを画面へ追加する。

 2labelCountラベルへ、HelloプロパティをTextCountコンバーターを使ってバインドする。WithConversionメソッドでコンバーターを指定する。

 Helloプロパティが更新されたとき、CoreプロジェクトのMvxTextCountConverterクラスが値を受け取り、その文字数をlabelCountラベルへ表示する。

Droidプロジェクトの実装

 Droidプロジェクトも同様にレイアウトを編集し、コンバーターを使用してバインディングを定義する。[CrossConverterSample.Droid]プロジェクトの[Resources]-[layout]ディレクトリにある[FirstView.axml]ファイルを開き、その[Source]を次のように修正する。

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:local="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text Hello" />
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text Hello" />
  <!-- 1 -->
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text TextCount(Hello)" />
</LinearLayout>
Droidプロジェクトのレイアウトを作成する(FirstView.axml)

 1で文字数を表示するTextViewを作成し、TextCountコンバーターを設定してHelloプロパティをバインディングしている。Android Layout XML(.axml)ファイルではMvxBind属性でTextCount(Hello)のようにコンバーターとその引数となるプロパティを指定する。

アプリケーションの実行

 ここでアプリケーションを実行すると、次のように表示される。テキストを編集すると、その文字数をカウントして画面へ表示する。

図3 文字数をカウントするコンバーターのアプリ実行結果(iOS/Android)

プラットフォームプロジェクトごとのコンバーターの実装

 次に、プラットフォームごとに異なるコンバーターを実装してみよう。

 今回はCoreプロジェクトを修正する必要はなく、TouchプロジェクトとDroidプロジェクトのみ追加で実装する。

Touchプロジェクトの実装

 まず、Touchプロジェクト用のコンバータークラスを作成する。

 CrossConverterSample.TouchプロジェクトのルートディレクトリにConvertersディレクトリを作成し、その下へMvxCountColorConverter.csファイルを作成する(図4)。

図4 MvxCountColorConverter.csの構成

 MvxCountColorConverterクラスは次のように実装する。

C#
using System;
using Cirrious.CrossCore.Converters;
using UIKit;

namespace CrossConverterSample.Touch.Converters
{
  public class MvxCountColorConverter : MvxValueConverter<string, UIColor>
  {
    protected override UIColor Convert (string value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      int count = value.Length;
      if (10 <= count) {
        return UIColor.Red;
      }
      return UIColor.White;
    }
  }
}
Touchプロジェクト用のコンバーター実装(MvxCountColorConverter.cs)

 クラス名の命名やMvxValueConverterクラスの継承など、実装方法はTextCountコンバーターとほとんど変わりはない。CountColorコンバーターは与えられたプロパティの文字数が10以上のときUIColor.Redを返し、10未満であればUIColor.Whiteを返す。

 CountColorコンバーターは変換後にUIColorを返している。UIColorクラスはTouchプロジェクトからのみ参照できるクラスであるため、MvxCountColorConverterクラスをCoreプロジェクトのコンバーターとして実装することはできない。

 次にTouchプロジェクトのバインディングを修正する。[CrossConverterSample.Touch]プロジェクト-[Views]ディレクトリ-[FirstView.cs]ファイルを次のように修正する。

C#
using Cirrious.MvvmCross.Binding.BindingContext;
using Cirrious.MvvmCross.Touch.Views;
using CoreGraphics;
using Foundation;
using ObjCRuntime;
using UIKit;

namespace CrossConverterSample.Touch.Views
{
  [Register("FirstView")]
  public class FirstView : MvxViewController
  {
    public override void ViewDidLoad()
    {
      ……省略(コントロルのレイアウトは先ほどと同じ)……

      var set = this.CreateBindingSet<FirstView, Core.ViewModels.FirstViewModel>();
      set.Bind(label).To(vm => vm.Hello);
      set.Bind(textField).To(vm => vm.Hello);
      set.Bind(labelCount).To(vm => vm.Hello).WithConversion("TextCount");
      set.Bind(labelCount).For(v => v.BackgroundColor).To(vm => vm.Hello).WithConversion("CountColor"); // 1この一行を追加
      set.Apply();
    }
  }
}
Touchプロジェクト用のバインディング設定(FirstView.cs)

 1の一行を追加する。labelCountラベルのBackgroundColorプロパティへ、CountColorコンバーターを使用してHelloプロパティをバインディングしている。

 CountColorコンバーターはHelloプロパティの文字数をカウントしてUIColorを返すため、文字数によってlabelCoutラベルの背景色が変化する。

Droidプロジェクトの実装

 次に、Droidプロジェクトのコンバーターを作成しよう。

 CrossConverterSample.DroidプロジェクトのルートディレクトリへConvertersディレクトリを作成し、その下へMvxCountColorConverter.csファイルを作成する(図5)。

図5 MvxCountColorConverter.csの構成

 MvxCountColorConverterクラスは次のように実装する。

C#
using System;
using Cirrious.CrossCore.Converters;
using Android.Graphics;
using Android.Graphics.Drawables;

namespace CrossConverterSample.Droid.Converters
{
  public class MvxCountColorConverter : MvxValueConverter<string, Drawable>
  {
    protected override Drawable Convert (string value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      int count = value.Length;
      if (10 <= count) {
        return new ColorDrawable (Color.Red);
      }
      return new ColorDrawable (Color.Black);
    }
  }
}
Droidプロジェクト用のコンバーター実装(MvxCountColorConverter.cs)

 実装方法もロジックも、TouchプロジェクトのMvxCountColorConverterクラスと同様であるが、変換後の値としてColorDrawableインスタンスを返す点が異なる。DroidプロジェクトでバインディングするViewのプロパティはTextViewクラスのBackgroudプロパティであり、BackgroundプロパティはDrawable型であるため、これにそろえた返り値となる。

 DroidプロジェクトのCountColor実装は、渡されたプロパティの文字数が10以上であればColor.RedColorDrawableインスタンスを返し、10未満であればColor.BlackColorDrawableインスタンスを返す動作となる。

 CountColorコンバーターはTouchプロジェクトのコンバーターと名前が被っているが、TouchプロジェクトとDroidプロジェクトは依存関係がなく、どちらのプラットフォーム向けのビルドをした場合でも二つのコンバータークラスは衝突しない。

 次に、Droidプロジェクトのバインディングを修正する。[CrossConverterSample.Droid]プロジェクトの[Resources]-[layout]ディレクトリにある[FirstView.axml]ファイルを次のように修正する。

XML
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:local="http://schemas.android.com/apk/res-auto"
  android:orientation="vertical"
  android:layout_width="match_parent"
  android:layout_height="match_parent">
  <EditText
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text Hello" />
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text Hello" />
  <!-- 1 -->
  <TextView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textSize="40dp"
    local:MvxBind="Text TextCount(Hello);Background CountColor(Hello)" />
</LinearLayout>
Droidプロジェクト用のバインディング設定(FirstView.axml)

 1MvxBind属性の値を修正する。TextViewBackgroundプロパティへ、CountColorコンバーターを使いHelloプロパティをバインディングしている。この修正により、入力された文字数によって文字数表示用のTextViewの背景色が変化する。

アプリの実行

 これで全ての実装が完了した。アプリを実行すると以下のように表示される。テキストを編集すると文字数がカウントされ、文字数が10以上のとき、文字数表示の背景色が赤色となる。コンバーターをプラットフォームごとに実装したため、iOSアプリとAndroidアプリとでは10文字未満の場合に異なった背景色設定されている。

図6 背景色変化ありのアプリ動作(iOS/Android)

【コラム】MvvmCrossで提供されているコンバーター

 MvvmCrossには、MvvmCrossプラグイン形式で複数のコンバーターが提供されている。いずれも使いやすいコンバーターであるため、確認しておこう。

 MvxVisibilityValueConverterクラスとMvxInvertedVisibilityValueConverterクラスは、プロパティの値によってViewの表示/非表示の状態を切り替えるコンバーターである。以下の例に示すように、「Visibility」ターゲットを指定することで、iOSアプリではUViewクラスのHiddenプロパティを、AndroidアプリではViewクラスのVisibilityプロパティを設定する動作となる。

C#
set.Bind(field)
   .For("Visibility")    // 「Visibility」ターゲットを指定
   .To(vm => vm.VMProperty)
   .WithConversion("Visibility");
Visibilityコンバーターを用いたバインディング例

 MvxVisibilityValueConverterクラスは以下の表に示すように柔軟に判定を行う。MvxInvertedVisibilityValueConverterクラスは以下の条件を反転させたコンバーターである。

プロパティの型Visibleの条件
string nullではない、かつ、文字数が0文字ではない
bool trueである
intまたはdouble 0より大きい
その他の型 nullでないとき
MvxVisibilityValueConverterの判定条件

 MvxVisibilityValueConverterクラスとMvxInvertedVisibilityValueConverterクラスは利用したいプラットフォームプロジェクトへ、NuGetリポジトリからMvvmCross Visibility Pluginをインストールすることで利用できる。

 MvxNativeColorValueConverterクラスはMvxColorクラスをプラットフォームごとの色表現(iOSならUIColor、Androidならint値)へ変換するコンバーターである。MvxColorクラスはCoreプロジェクトで共通にプラットフォームの色を扱うためのクラスとして用意されている。

 MvxRGBAValueConverterクラスはRGBの文字列による色表現を、それぞれのプラットフォームの色表現へ変換するコンバーターである。MvxRGBAValueConverterクラスは次の文字列表現をパース可能である。

  • “RGB”または“#RGB”形式
  • “RRGGBB”または“#RRGGBB”形式
  • “RRGGBBAA”または“#RRGGBBAA”形式

 MvxRGBIntColorConverterクラスはint型による色表現を、それぞれのプラットフォームの色表現へ変換するコンバーターである。

 MvxNativeColorValueConverterクラス、MvxRGBAValueConverterクラス、MvxRGBIntColorConverterクラスは利用したいプラットフォームプロジェクトへ、NuGetリポジトリからMvvmCross Color Pluginをインストールすることで利用できる。

※以下では、本稿の前後を合わせて5回分(第49回~第53回)のみ表示しています。
 連載の全タイトルを参照するには、[この記事の連載 INDEX]を参照してください。

49. MvvmCrossでAndroidの画面の再生成に対応するには?

Androidアプリでは別アプリ移動時に画面が破棄され、アプリ再表示時に画面が復元される場合がある。この画面の再生成を、MvxViewModelのライフサイクルメソッドにより行う方法を説明する。

50. Xamarin.Formsでローカルデータベースを使用するには?

アプリを終了して再起動したときに、ユーザーデータを復活させたい場合、ローカルやクラウドにデータを保存することになる。その一つの方法として、SQLite.Netを使ってローカルDBに保存する方法を説明する。

51. 【現在、表示中】≫ MvvmCrossでカスタムコンバーターを作成するには?

MvvmCrossでのiOS/Androidアプリ開発において、バインディングする値を変換できるカスタムコンバーターの使い方を説明する。

52. Xamarin.FormsでTwitterクライアントを作成するには?

TwitterのAPIを扱えるライブラリであるCoreTweetを使用して、Twitterデータを検索するアプリを作成。CoreTweetの導入と、検索したテキストの表示までを紹介する。

53. MvvmCrossでWebBrowserプラグインを使用するには?

WebBrowserプラグインを追加・利用する例を通じて、MvvmCrossでのiOS/Androidアプリ開発におけるMvvmCrossプラグインの基本的な使い方を説明する。

イベント情報(メディアスポンサーです)

Twitterでつぶやこう!


Build Insider賛同企業・団体

Build Insiderは、以下の企業・団体の支援を受けて活動しています(募集概要)。

ゴールドレベル

  • グレープシティ株式会社
  • 日本マイクロソフト株式会社