しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

Xamarin.Forms の View にバインド可能プロパティを追加する

少し前に Xamarin.Forms でもユーザーコントロール的な View が作成できることがわかったので、今度はこうしたカスタム View に BindableProperty を追加してみようと思います

本家 XAML依存関係プロパティのようなものみたいですね

まずは ContentView で下記のような XAML を記述します

<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:XamarinSample.ViewModels;assembly=XamarinSample"
             x:Class="XamarinSample.Views.ArcStepper"
             Value="{Binding TimerValue}">

  <ContentPage.BindingContext>
    <vm:TimerViewModel/>
  </ContentPage.BindingContext>
  
  <StackLayout 
    Orientation="Vertical" 
    VerticalOptions="Center" 
    HorizontalOptions="Center"
    Spacing="10">
    
    <Label
      x:Name="label"
      Font="30"
      HorizontalOptions="Center"/>

    <Stepper ValueChanged="OnValueChanged" HorizontalOptions="Center"/>
  
  </StackLayout>
</ContentView>

そしてコードビハインドを次のように記述

/// <summary>
/// 円弧型 Stepper
/// </summary>
public partial class ArcStepper : ContentView
{
    /// <summary>
    /// Value Bindable プロパティ
    /// </summary>
    public static readonly BindableProperty ValueProperty = BindableProperty.Create(
        "Value", // プロパティ名
        typeof (TimeSpan), // プロパティの型
        typeof (ArcStepper), // プロパティを持つ View の型
        TimeSpan.FromMinutes(1), // 初期値
        BindingMode.TwoWay, // バインド方向
        null, // バリデーションメソッド
        ArcStepper.OnValuePropertyChanged, // 変更後イベントハンドラ
        null, // 変更時イベントハンドラ
        null); // BindableProperty.CoerceValueDelegate Xamarin 公式にも説明なしなので用途不明

    /// <summary>
    /// Value CLR プロパティ
    /// </summary>
    public TimeSpan Value
    {
        get { return (TimeSpan) this.GetValue(ValueProperty); }
        set { this.SetValue(ValueProperty, value); }
    }

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public ArcStepper()
    {
        this.InitializeComponent();

        // 初期値を画面に反映させる
        OnValuePropertyChanged(this, null, this.Value);
    }

    /// <summary>
    /// Value 変更後イベントハンドラ
    /// </summary>
    /// <param name="bindable">BindableObject</param>
    /// <param name="oldValue">古い値</param>
    /// <param name="newValue">新しい値</param>
    private static void OnValuePropertyChanged(BindableObject bindable, object oldValue, object newValue)
    {
        var view = bindable as ArcStepper;
        if (view == null || !(newValue is TimeSpan))
        {
            return;
        }
        view.label.Text = string.Format("{0:h\\:mm\\:ss}", (TimeSpan)newValue);
    }

    /// <summary>
    /// Stepper による値変更イベントハンドラ
    /// </summary>
    /// <param name="sender">イベント発行者</param>
    /// <param name="e">イベント引数</param>
    private void OnValueChanged(object sender, ValueChangedEventArgs e)
    {
        if (e.NewValue - e.OldValue > 0)
        {
            // 1分加算
            this.Value = this.Value.Add(TimeSpan.FromMinutes(1));
        }
        else
        {
            // 1分減算
            this.Value = this.Value.Add(-TimeSpan.FromMinutes(1));
        }
    }
}

BindableProperty は Create メソッドで生成します

CoerceValueDelegate の用途は不明ですが他はひと通りのことができそうな引数がそろっている感じですね

上記のコードを実行すると・・・

f:id:matatabi_ux:20140923190529p:plain

ちゃんと表示されました(丸いライトグレイの円はシミュレータが表示してくれるタップ位置の目印みたいです)

デフォルト値は 1:00 ですが TimerViewModel.TimerValue に 5:00 が設定されているのできちんと反映されていますね

さらに Stepper の + ボタンをタップすると

f:id:matatabi_ux:20140923190742p:plain

f:id:matatabi_ux:20140923191314p:plain

画面も ViewModel もちゃんと 1:00 加算されました

バインド成功!