ぴーさんログ

だいたいXamarin.Formsのブログ

Xamarin.FormsでAttachedProperty

AttachedPropertyMicrosoftXAMLプラットフォームから移植された概念の一つで、MSDNの日本語版では「添付プロパティ」と訳されています。その性質は「任意のオブジェクトにプロパティを生やすことができる」というものです。

分かりやすい具体例は Grid.RowGrid.Column ですね。これらはGridの子コントロールが本来持っていなかった「どこに配置されるべきか」を表すプロパティを追加しています。

余談 WPFのBehaviors、TriggersはAttachedPropertyで実装されていましたが、Xamarin.FormsではVisualElementクラスのプロパティとなっています。(後発だから?)

AttachedPropertyの使い方のサンプルとして、EntryコントロールのTextChangedイベント発火時にCommandに実行するためのAttachedPropertyを作ってみましょう。(AttachedPropertyを使ったトリックとしては割と定番らしい?)

実はAttachedPropertyという型は存在せず、実際の型はBindablePropertyとなっており、BindableProperty.CreateAttachedメソッドで作成します。別クラスに添付する都合上、getter/setterはstaticメソッドとして定義する必要があります。

public class EntryBehavior
{
    // AttachedProperty定義
    public static readonly BindableProperty TextChangedCommandProperty =
        BindableProperty.CreateAttached<EntryBehavior, ICommand>(
            bindable => GetTextChangedCommand(bindable), /* static getter */
            null, /* デフォルト値 */
            BindingMode.OneWay, /* デフォルトBindingMode */
            null, /* ValidateValueデリゲート */
            OnTextChangedCommandPropertyChanged, /* PropertyChangedデリゲート */
            null, /* PropertyChangingデリゲート */
            null /* CreateDefaultValueデリゲート */
        );
    
    // AttachedProperty用のgetter、setter
    public static ICommand GetTextChangedCommand(BindableObject bindable)
    {
        return (ICommand)bindable.GetValue(EntryBehavior.TextChangedCommandProperty);
    }
    public static void SetTextChangedCommand(BindableObject bindable, Command value)
    {
        bindable.SetValue(EntryBehavior.TextChangedCommandProperty, value);
    }

    // Entry以外に使われた場合は何もしない
    private static void OnTextChangedCommandPropertyChanged(BindableObject bindable, ICommand oldValue, ICommand newValue)
    {
        Entry entry = bindable as Entry;
        if(entry == null)
            return;

        if(newValue != null)
        {
            entry.TextChanged += OnTextChanged;
        }
        else
        {
            entry.TextChanged -= OnTextChanged;
        }
    }

    // Entry.TextChangedに登録するイベントハンドラでCommandを実行する
    private static void OnTextChanged(object sender, TextChangedEventArgs e)
    {
        ICommand command = GetTextChangedCommand(sender as BindableObject);
        if(command != null)
        {
            if(command.CanExecute(e))
            {
                command.Execute(e);
            }
        }
    }
}

サンプル用のViewModel

INotifyPropertyChangedを実装した割とオーソドックスなViewModel。

public class ViewModel : INotifyPropertyChanged
{
    private string entryText;
    public string EntryText
    {
        get { return entryText; }
        set
        {
            if(entryText != value)
            {
                entryText = value;
                RaisePropertyChanged(nameof(EntryText));
            }
        }
    }

    private bool isValid;
    public bool IsValid
    {
        get { return isValid; }
        private set
        {
            if(isValid != value)
            {
                isValid = value;
                RaisePropertyChanged(nameof(IsValid));
            }
        }
    }

    public ObservableCollection<string> Texts
    {
        get;
    } = new ObservableCollection<string>{ "first", "second", "third" };

    // このCommandをAttachedProperty経由で実行する
    public Command ValidateCommand
    {
        get;
    }

    public Command SaveCommand
    {
        get;
    }

    public ViewModel()
    {
        ValidateCommand = new Command(() => IsValid = !string.IsNullOrEmpty(EntryText));
        
        SaveCommand = new Command(() => {
            Texts.Add(EntryText);
            EntryText = "";
        }, () => IsValid);

        PropertyChanged += (sender, e) => {
            if(e.PropertyName == nameof(IsValid))
            {
                SaveCommand.ChangeCanExecute();
            }
        };
    }

   #region INotifyPropertyChanged implementation
    public event PropertyChangedEventHandler PropertyChanged;

    protected void RaisePropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
   #endregion
}

サンプル用のView

Entryコントロールに先ほど作成したEntryBehavior.TextChangedCommandプロパティを添付、入力内容が変わるごとにバインドされたValidateCommandでテキストが評価されます。そして、結果がOKならばボタンが押せるようになり、NGならボタンは押せなくなる、というサンプルです。

<?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:XFApp8;assembly=XFApp8"
             x:Class="XFApp8.MyPage">
    <ContentPage.BindingContext>
        <local:ViewModel />
    </ContentPage.BindingContext>

    <ContentPage.Content>
        <StackLayout Padding="30">
            <!-- AttachedPropertyを使ってCommandを実行し、バリデーションを行う -->
            <Entry Text="{Binding EntryText, Mode=TwoWay}"
                   local:EntryBehavior.TextChangedCommand="{Binding ValidateCommand}" />
            
            <!-- バリデーション結果によって押せたり、押せなかったり -->
            <Button Command="{Binding SaveCommand}" >
                <Button.Style>
                    <Style TargetType="Button">
                        <Style.Triggers>
                            <Trigger TargetType="Button"
                                     Property="IsEnabled"
                                     Value="true">
                                <Setter Property="Text" Value="保存します" />
                            </Trigger>
                            <Trigger TargetType="Button"
                                     Property="IsEnabled"
                                     Value="false">
                                <Setter Property="Text" Value="何か入力してください" />
                            </Trigger>
                        </Style.Triggers>
                    </Style>
                </Button.Style>
            </Button>
            <ListView ItemsSource="{Binding Texts}">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <TextCell  Text="{Binding }" />
                    </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

AttachedPopertyの部分はこういう書き方もできます。(Interaction.Behaviorsっぽい書き方)

<Entry Text="{Binding EntryText, Mode=TwoWay}" >
    <local:EntryBehavior.TextChangedCommand>
        <Binding Path="ValidateCommand" />
    </local:EntryBehavior.TextChangedCommand>
</Entry>

このサンプルを実行すると...

f:id:ticktack623:20151206161402g:plain

ちゃんと、AttachedPropertyにバインドしたコマンド経由でIsValidプロパティが更新されました。