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&amp;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();
        }
    }
}

ビルドと実行

実行します。

f:id:mokake:20170106230551p:plain

「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は隔靴掻痒な部分があるので、ほどほどの妥協が必要です。