読者です 読者をやめる 読者になる 読者になる

Xamarin 日本語情報

Xamarin(ザマリン) の代理店だったエクセルソフト田淵のブログです。主に Xamarin に関するエントリーをアップしていきます。(なるべく正しい有益な情報を掲載していきたいと考えていますが、このブログのエントリーは所属組織の公式見解ではありませんのでご注意ください)

Xamarin.Forms で IValueConverter を使うには

Xamarin Xamarin.Forms

こんにちは。エクセルソフトの田淵です。

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

フェンリルさんによる Xamarin逆引きTips -MvvmCross 編- でコンバーターの記事が載っていたので、Xamarin.Forms の IValueConverter に再チャレンジしてみました。

2015/6/22 追記
また、リクエストがありましたので、View to view Binding する方法と、一般的であろう ViewModel 的な Binding する方法の両方を XamlC# で用意しました。

サンプルプロジェクト

GitHub ギッハブ

IValueConverter 本体

こんな感じです。ConvertBack は今回は使用しませんので未実装です。

string の文字数を返す Converter

using System.Globalization;

class StringToLengthConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null)
            return 0;

        return value.ToString().Length;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

string を大文字/小文字に変換する Converter

using System.Globalization;

class StringCaseConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // 例えば ConverterParameter=true とすると、parameter に string "true" が入ります。
        if (value == null)
            return 0;

        // parameter に "True" が指定されると ToUpper します。
        // TODO: parameter に switch の bool 値を参照させる
        if (System.Convert.ToBoolean(parameter))
        {
            return value.ToString().ToUpper();
        }
        else
        {
            return value.ToString().ToLower();
        }
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Editor のテキストを Binding して Convert

例えば Editor のテキストを Label に Binding するだけなら、わざわざ ViewModel 的なものを呼び出さないでも、Xamarin のドキュメントにあるように View-to-View Bindings で直接参照することができます。

View to view Binding で Convert

今回のように同じ View 内の情報を Binding や Convert する場合は、View to view Binding が便利です。

Xaml の場合

<ContentPage.Resources>
  <ResourceDictionary>
    <cv:StringCaseConverter x:Key="scConverter" />
    <cv:StringToLengthConverter x:Key="s2lConverter" />
  </ResourceDictionary>
</ContentPage.Resources>

<StackLayout>
  <Editor x:Name="editor" Text="test" />
  <!-- ConverterParameter は "True" で UpperCase、"False" で LowerCase になります。
       今は固定してしまっていますが、Switch とかから IsToggled を参照したいですね。 -->
  <Label BindingContext="{x:Reference Name=editor}"
         Text="{Binding Text, 
                Converter={StaticResource scConverter},
                ConverterParameter=True}" />
  <Label BindingContext="{x:Reference Name=editor}"
         Text="{Binding Text, 
                Converter={StaticResource s2lConverter},
                StringFormat='{0} letters'}" />
</StackLayout>

のように BindingContext="{x:Reference Name=editor}" とすることで Editor の x:Name の値を直接参照できます。

Converter/ViewModel をフォルダで分けている場合は、NameSpace も別々になりますので、NameSpace の設定だけ xmlns:cv="clr-namespace:XF_IValueConverterSample.Converters;assembly=XF_IValueConverterSample" のようにしてあげて、<ResourceDictionary> で Converter を指定します。
(Xamarin のサンプル は NameSpace が local になっていますが、using と同じ感じだと思いますので cv (Converter) や vm (ViewModel) などで複数指定できるみたいです。)

指定した Converter は Binding XXX, Converter={StaticResource Key名} で呼び出せます。

scConverter では if (System.Convert.ToBoolean(parameter)) で True の時に value.ToString().ToUpper() に、Flase の時に value.ToString().ToLower() になるようにしました。s2lConverter では value.ToString().LengthBuild Insider と同じく文字数を取りました。

C# の場合

var editor = new Editor { Text = "test" };

var sclabel = new Label { Text = "" };
sclabel.BindingContext = editor;
sclabel.SetBinding(Label.TextProperty, 
    new Binding("Text", 
        converter: new Converters.StringCaseConverter(),
        converterParameter: "True"));

var sllabel = new Label { Text = "" };
sllabel.BindingContext = editor;
sllabel.SetBinding(Label.TextProperty,
    new Binding("Text",
        converter: new Converters.StringToLengthConverter(),
        stringFormat: "{0} letters"));

Content = new StackLayout
{
    Children = {
        editor,
        sclabel,
        sllabel
    }
};

C# の場合は各コントロールに .BindingContext で Binding 対象を、.SetBinding で Bind する内容を記述します。

Binding ですが、Binding(string, BindingMode, IValueConverter, object, string, object) となっており、 ConverterParameterPath などを設定できます。

詳細は Xamarin API Documentation をご覧ください。

単純なコンバートであればコードビハインドや ViewModel なしでも出来て便利ですね。

ViewModel の Binding で Convert

次は一般的な ViewModel を経由した Binding と Convert です。

Xaml の場合

xmlns:cv="clr-namespace:XF_IValueConverterSample.Converters;assembly=XF_IValueConverterSample"
xmlns:vm="clr-namespace:XF_IValueConverterSample.ViewModel;assembly=XF_IValueConverterSample"

<ContentPage.BindingContext>
  <vm:CommonViewModel />
</ContentPage.BindingContext>

<ContentPage.Resources>
  <ResourceDictionary>
    <cv:StringCaseConverter x:Key="scConverter" />
    <cv:StringToLengthConverter x:Key="s2lConverter" />
  </ResourceDictionary>
</ContentPage.Resources>

で ViewModel と Converter を指定します。同じフォルダにファイルがある場合は NameSpace も同じですので、local で良いかと思います。

<StackLayout>
  <Editor Text="{Binding Message}" />
  <Label Text="{Binding Message}" />
  <Label Text="{Binding Message, 
                Converter={StaticResource scConverter},
                ConverterParameter=True}" />
  <Label Text="{Binding Message, 
                Converter={StaticResource s2lConverter},
                StringFormat='{0} letters'}" />
</StackLayout>

レイアウト部分はこれだけです。

ViewModel は INotifyPropertyChanged を継承した ViewModel を用意して

string _message;

public string Message
{
    get { return _message; }
    set
    {
        if (_message != value)
        {
            _message = value;
            OnPropertyChanged("Message");
        }
    }
}

OnPropertyChanged メソッドと一緒に入れてしまいましたが、OnPropertyChanged は BaseViewModel とかに定義して BaseViewModel を継承する方が一般的なやり方なのかもしれません。(素人ですみません><)

Converter の部分は使いまわしですので、説明は割愛します。

C# の場合

BindingContext = new ViewModel.CommonViewModel();

var editor = new Editor { Text = "" };
editor.SetBinding(Editor.TextProperty, "Message");

var sclabel = new Label { Text = "" };
sclabel.SetBinding(Label.TextProperty,
    new Binding("Message",
        converter: new Converters.StringCaseConverter(),
        converterParameter: "True"));

var sllabel = new Label { Text = "" };
sllabel.SetBinding(Label.TextProperty,
    new Binding("Message",
        converter: new Converters.StringToLengthConverter(),
        stringFormat: "{0} letters"));

Content = new StackLayout
{
    Children = {
        editor,
        sclabel,
        sllabel
    }
};

BindingContext は一度で指定できました。後はそれぞれで .SetBinding するだけです。こちらも converter, converterparameter などは一緒なので割愛します。

結果は同じでこんな感じです。

f:id:ytabuchi:20150618174732p:plain:w150f:id:ytabuchi:20150618174736p:plain:w150f:id:ytabuchi:20150618174738p:plain:w150

メモ:
Slider の値を ViewModel の OnPropertyChanged 経由で Label に反映することは出来ましたが、当初 Editor の Text が Binding で出来なかった問題ですが、Xaml で Editor の Text を Text="{Binding Message}" した場合、Converter に渡る Value が Null なので、Converter 側でしっかり Null チェックをしていれば大丈夫だった。ということでした。

if (value == null)
    return 0;

Null チェック大事ですね。

おまけ:ListView の Binding を Convert

先日の de:code のプロジェクトでは間に合わなかった ListView の Binding したやつを Convert してみました。

<ListView ItemsSource="{Binding}">
  <ListView.ItemTemplate>
    <DataTemplate>
      <TextCell Text="{Binding word, 
                       Converter={StaticResource scConverter},
                       ConverterParameter=True}"
                Detail="{Binding word, 
                         Converter={StaticResource s2lConverter},
                         StringFormat='{0} letters'}" />
    </DataTemplate>
  </ListView.ItemTemplate>
</ListView>
var listdata = new List<Words> {
    new Words { word = "the" },
    new Words { word = "quick" },
    new Words { word = "brown" },
    new Words { word = "fox" },
    new Words { word = "jumps" },
    new Words { word = "over" },
    new Words { word = "the" },
    new Words { word = "lazy" },
    new Words { word = "dog" }
};

this.BindingContext = listdata;

string[] をそのまま ItemsSource に渡すことも出来るのですが、それだと {Binding xxx} の xxx をどう書くのか良く分からなかったので面倒ですが List にしました。すべて小文字ですが、Editor と同様に scConverter で大文字にし、s2lConverter で文字数を取得しています。

f:id:ytabuchi:20150618175909p:plain:w150f:id:ytabuchi:20150618175914p:plain:w150f:id:ytabuchi:20150618175917p:plain:w150

IValueConverter のまとめ

簡単な変換であれば一時格納用のクラスを作ったりせずに手軽に値を変換できます。

ContentPage 内のデータを参照するだけであれば、View to view の Binding も手軽ですし、一般的な ViewModel 的なのを使った Binding も (分かってしまえば) 簡単です。

是非使ってみてください。

参照記事

Part 4. Data Binding Basics - Xamarin

How do I use IValueConverters? - Xamarin Forums

Xamarin.Forms Kickstarter

Xamarin 気になった方は

是非 ダウンロード(直接) / ダウンロード(弊社経由) して触ってみてください。 学習用リソースJXUG リンクページ に参考資料を纏めてますので併せてどうぞ。

Xamarin の情報が欲しい方はこのブログも購読いただいたりすると嬉しいです。

以上です。