Eto.FormsのXAMLでCommandをバインド
前回はXAMLで値をバインドしましたが、Vを起点に処理するためには、コマンドが不可欠です。現在のEto.Formsでも、ある程度のコマンドを扱うことができます。
サンプルプロジェクトを作成してみます。元ネタはXamarinのコマンドの説明です。
「イベントをコマンドで簡単に」
Simplifying Events with Commanding | Xamarin Blog
では、VMから作っていきます。
VMを作る
今回も、サンプルですから、VMにモデルデータを含めておきます。
using System; using System.ComponentModel; // INotifyPropertyChanged using System.Windows.Input; // ICommand using System.Runtime.CompilerServices; // CallerMemberName using Eto.Forms; // Command namespace EtoXamlSample2 { public class SampleViewModel : INotifyPropertyChanged { public int Number { get; set; } private double _result = 0.0; public double Result { get { return _result; } private set { TrySetProperty(ref _result, value); } } public ICommand SquareRootCommand { get; private set; } public SampleViewModel() { Number = 81; SquareRootCommand = new Command(Calculate); } void Calculate(object sender, EventArgs e) { Result = Math.Sqrt((double)Number); } #region VMの基礎 public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged( [CallerMemberName] string propertyName = null) { if (PropertyChanged == null) return; PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } protected bool TrySetProperty<T>( ref T storage, T value, [CallerMemberName] string propertyName = null) { if (object.Equals(storage, value)) return false; storage = value; RaisePropertyChanged(propertyName); return true; } #endregion } }
最初のusing宣言では、テンプレートに追加をしています。ICommandがSystem.Windows.Inputなのに対して、その実装クラスCommandはEto.Forms名前空間にある点は注意が必要です。
コンストラクタで、コマンドを定義しています。Eto.FormsのCommandは、コンストラクタでは実行デリゲートのみ設定できます。 CanExecuteは(ここでは書いていませんが)構築後に設定する形になります。
VMの基礎は、前回同様いただきものです。
XAMLを作る
テンプレートから改変したのはStackLayoutの中身だけです。 メニューの方が長いのは無視しましょう。
<?xml version="1.0" encoding="UTF-8"?> <Form xmlns="http://schema.picoe.ca/eto.forms" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="My Eto Form" ClientSize="400, 350" > <StackLayout> <Label>Target</Label> <TextBox Text="{Binding Number}" /> <Label>Answer</Label> <TextBox Text="{Binding Result}" /> <Button x:Name="Sqrt" Text="SQRT" Command="{Binding SquareRootCommand}" /> </StackLayout> <Form.Menu> <MenuBar> <ButtonMenuItem Text="F&ile"> <Command x:Name="clickCommand" MenuText="Click Me!" ToolBarText="Click Me!" Executed="HandleClickMe" /> </ButtonMenuItem> <MenuBar.ApplicationItems> <ButtonMenuItem Text="Preferences.." Shortcut="{On Control+O, Mac=Application+Comma}" /> </MenuBar.ApplicationItems> <MenuBar.QuitItem> <ButtonMenuItem Text="Quit!" Shortcut="CommonModifier+Q" Click="HandleQuit" /> </MenuBar.QuitItem> </MenuBar> </Form.Menu> <Form.ToolBar> <ToolBar> <x:Reference Name="clickCommand"/> </ToolBar> </Form.ToolBar> </Form>
見ての通り、Buttonにコマンドをバインドしています。
コードビハインド
VMとVを接続します。 テンプレートからの変更は、myModelフィールドの追加とコンストラクタ内の処理だけです。
using System; using System.Collections.Generic; using Eto.Forms; using Eto.Drawing; using Eto.Serialization.Xaml; namespace EtoXamlSample2 { public class MainForm : Form { SampleViewModel myModel; public MainForm() { XamlReader.Load(this); myModel = new SampleViewModel(); DataContext = myModel; } protected void HandleClickMe(object sender, EventArgs e) { MessageBox.Show("I was clicked!"); } protected void HandleQuit(object sender, EventArgs e) { Application.Instance.Quit(); } } }
ビルドと実行
実行します。
「SQRT」ボタンを押すと、Targetの値の平方根がAnswerに入ります。
負の値を設定すると?
VMの計算式は、単純にMath.Sqrtを呼び出しているだけなので、例外になるはずです。
では、Targetの中身を全部選択してマイナス記号を入力します。
すると、例外が出ます。ただし値の変換での例外です。つまり、テキストが「-」になったため、intに変換できなくなったわけです。
バリデーションの仕組みは、残念ながら無いようなので、TextBoxのTextChangingイベントでチェックしてみます。
XAMLのTargetを入れる部分を、次のように修正します。チェックに使えるTextChangingプロパティはCommandプロパティではないので、コードビハインドに書くこととしています。
<TextBox Text="{Binding Number}" TextChanging="CheckNumber" />
VM側に判定の中身を入れておきます。
public bool IsCalculable(string text) { int i = 0; if (int.TryParse(text, out i) == false) return false; return i >= 0; }
コードビハインドで両者を接続します。
protected void CheckNumber(object sender, TextChangingEventArgs e) { if (myModel.IsCalculable(e.Text) == false) { e.Cancel = true; } }
これをビルド、実行すると、0以上(非負)の整数にならない値は入力ができなくなります。
CanExecuteを使う
今回のように「平方根をとる」なら、このアプローチで十分でしょう。
しかし、もしこれが「2乗する」など、負の値でも許容される場合は、使い勝手の観点からは「マイナス記号だけが入った状態」も途中なら認めたいところです。
ここでは、(平方根の計算、という部分はそのままで)Target
欄を自由にしつつ、CanExecute
でボタンのクリック可否だけ調整してみることにします。
まずは、先ほどの追加修正のうち、XAMLとコードビハインドのものを元に戻します。
次に、VMを色々と変更します。ただし、Eto.FormsのCommand(Eto.Forms.Command)はICommandが使えますが、「Enabled
プロパティを返す」だけの実装なので、こちらを操作します。Enabledへの設定で、CanExecuteChanged
は自動的に発火します。
(※)APIドキュメントでのCommandクラスの定義にはICommandやIBindableがありませんが、ソースにはあります。
- 変換失敗による例外を避けるため、
Number
プロパティをint
からstring
に変更 Number
プロパティのset
で、値が非負の整数に変換できるかに基づきSquareRootCommand.Enabled
プロパティを設定Enabled
を使うために、SquareRootCommand
の型をICommand
からCommand
に変更
Calculate
メソッドでのNumber
の扱いを(型の変更にあわせて)変更
これらを全て行うと、次のようなコードになります。
using System; using System.ComponentModel; // INotifyPropertyChanged //using System.Windows.Input; // ICommandがなくなった using System.Runtime.CompilerServices; // CallerMemberName using Eto.Forms; // Command namespace EtoXamlSample2 { public class SampleViewModel : INotifyPropertyChanged { private string _number = "0"; public string Number { get { return _number; } set { TrySetProperty(ref _number, value); SquareRootCommand.Enabled = IsCalculable(value); } } private double _result = 0.0; public double Result { get { return _result; } private set { TrySetProperty(ref _result, value); } } public Command SquareRootCommand { get; private set; } public SampleViewModel() { SquareRootCommand = new Command(Calculate); } public bool IsCalculable(string text) { int i = 0; if (int.TryParse(text, out i) == false) return false; return i >= 0; } void Calculate(object sender, EventArgs e) { Result = Math.Sqrt(double.Parse(Number)); } #region VMの基礎(割愛) } }
これをビルド、実行すると、Targetの中身が非負の整数である場合だけ、「SQRT」ボタンが有効になるのが分かります。
ちなみにint.TryParseによるチェックなので、Targetの中身が「+12」のようにプラス記号で始まるのも有効です。
他にも「ラムダ式で書いてもっと簡潔に」などあるのですが、基本的な考え方は以上です。
付記
Targetの中身がマイナス符号だけになった瞬間に例外が出るのは、前回も述べましたが、バインドのタイミングが変化の瞬間だからです。
その辺も含めて、Eto.Formsは隔靴掻痒な部分があるので、ほどほどの妥協が必要です。