Xamarin.FormsでAttachedProperty
AttachedProperty はMicrosoftのXAMLプラットフォームから移植された概念の一つで、MSDNの日本語版では「添付プロパティ」と訳されています。その性質は「任意のオブジェクトにプロパティを生やすことができる」というものです。
分かりやすい具体例は Grid.Row
や Grid.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>
このサンプルを実行すると...
ちゃんと、AttachedPropertyにバインドしたコマンド経由でIsValidプロパティが更新されました。