C#
MacOSX
Cocoa
Xamarin

実践! Cocoa Binding

はじめに

Cocoa Bindingとは、MacOS独自の変更通知機構を使ったViewとModel/Controller層の同期機能です。Cocoa Binding使うことにより、多くの「Glue code(グルーコード)」を書くことなく、ModelとViewの値を同期させる方法を提供します。

ただし、バインドする際、プロパティの型は基底にNSObjectを持たなければいけない制約があります。

そんな強力なCocoa Bindingの使用方法について記載します。

※本稿の開発環境は下記の通りです。

項目
macOSX Sierra 10.12.6
Xcode 9.2 (9C40b)
Visual Studio for Mac 7.3.2(build 12)
Xamarin.Mac 4.0.0.214
Development Target 10.12

NSTextFieldとのバインド

ViewController内のプロパティとバインド

ViewControllerにNameプロパティを作成し、これにTextFieldとバインディングしてみます。

まずはMain.StoryBoardをXcodeで開き、ViewにTextField、Push Buttonを追加します。

TextField01.png

TextField02.png

次にボタンを押したときに実行されるメソッドを作成します。Controlキーを押しながら、Push ButtonをViewController.mにドラッグ&ドロップします。Nameに適当な名前をつけ(今回はShowMessageとする)、ConnectボタンをクリックするとActionが作成されます。

TextField03.png

作成後、ViewController.csにNameプロパティ、ボタンを押した際の処理を記載します。

ViewController.cs
public partial class ViewController : NSViewController
{
    public ViewController(IntPtr handle) : base(handle)
    {
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();
    }

    partial void ShowMessage(NSObject sender)
    {
        using(var alert = new NSAlert())
        {
            alert.MessageText = $"{Name}さん";
            alert.InformativeText = $"こんにちは。{Name}さん";
        }
    }

    private NSString name;

    [Outlet]
    public NSString Name
    {
        get => name;
        set
        {
            WillChangeValue(nameof(Name));
            name = value;
            DidChangeValue(nameof(Name));
        }
    }
}

再び、Main.StoryBoardを開き、左上のインスペクタをバインディングインスペクタに切り替えます。Valueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathNameを指定します。

TextField04.png

Xcodeを終了し、アプリケーションを実行します。

TextField01.gif

NSTextFieldのデフォルトでは、TextFieldからフォーカスが外れた時に変更通知が送られるため、TabReturnキーが押されないと同期しません。

文字列が変更されるたびに同期させたい場合はバインディングインスペクタで Continuously Updates Valueにチェックを入れて実行します。

TextField05.png

TextField02.gif

自作クラスとのバインド

次に自作クラスのオブジェクトをバインディングしてみます。Personというクラスを作成し、NSObjectを継承させ、Nameプロパティを追加します。

Person.cs
[Register(nameof(Person))]
public class Person : NSObject
{
    private NSString name;

    [Outlet]
    public NSString Name
    {
        get => name;
        set
        {
            WillChangeValue(nameof(Name));
            name = value;
            DidChangeValue(nameof(Name));
        }
    }
}

ViewContoller内に上記のクラスのプロパティを追加します。

ViewControllr.cs
public partial class ViewController : NSViewController
{
    public ViewController(IntPtr handle) : base(handle)
    {
        Person = new Person();
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();
    }

    partial void ShowMessage(NSObject sender)
    {
        using(var alert = new NSAlert())
        {
            alert.MessageText = $"{Person.Name}さん";
            alert.InformativeText = $"こんにちは。{Person.Name}さん";
            alert.RunSheetModal(View.Window);
        }
    }

    private Person person;

    [Outlet]
    public Person Person
    {
        get => person;
        set
        {
            WillChangeValue(nameof(Person));
            person = value;
            DidChangeValue(nameof(Person));
        }
    }
}

Main.StoryBoardを開き、バインディングインスペクタに切り替えます。Valueを展開し、Model Key PathPerson.Nameを指定します。

TextField06.png

Xcodeを終了し、アプリケーションを実行します。

TextField03.gif

Bottunの制御

次は、TextFieldに入力されていない場合、Buttonを押せないようにします。

PersonクラスにHasNameというプロパティを追加します。

Person.cs
[Register(nameof(Person))]
public class Person : NSObject
{
    private NSString name;

    [Outlet]
    public NSString Name
    {
        get => name;
        set
        {
            WillChangeValue(nameof(Name));
            WillChangeValue(nameof(HasName));
            name = value;
            DidChangeValue(nameof(Name));
            DidChangeValue(nameof(HasName));
        }
    }

    [Outlet]
    public bool HasName => !String.IsNullOrEmpty(Name) && Name.Length != 0;
}

Main.StoryBoardを開き、Viewに配置したButtonを選択しバインディングインスペクタに切り替えます。Availabilityを展開し、Model Key PathPerson.HasNameを指定します。

TextField07.png

Xcodeを終了し、アプリケーションを実行します。

TextField04.gif

NSComboBoxとのバインド

ViewController内のプロパティとバインド

ViewControllerにLanguagesプロパティを作成し、これにComboBoxとバインディングしてみます。

まずはMain.StoryBoardをXcodeで開き、ViewにComboBox、 Push Buttonを追加します。

ComboBox01.png

上記と同様に次にボタンを押したときに実行されるメソッドを作成します。

作成後、ViewController.csにLanguagesプロパティ、選択された値を保持すSelectedLanguage、ボタンを押した際の処理を記載します。

ViewController.cs
public ViewController(IntPtr handle) : base(handle)
{
    Languages = new NSString[]
    {
        (NSString)"Bash",
        (NSString)"C#",
        (NSString)"C++",
        (NSString)"Java",
        (NSString)"JavaScript",
        (NSString)"Swift",
        (NSString)"Objective-C",
        (NSString)"Python"
    };
    selectedLanguage = Languages.FirstOrDefault();
}

partial void ShowMessage(NSObject sender)
{
    using (var alert = new NSAlert())
    {
        alert.MessageText = "あなたの好きな言語は...";
        alert.InformativeText = $"{SelectedLanguage}です。";
        alert.RunSheetModal(View.Window);
    }
}

[Outlet]
public NSString[] Languages { get; }

private NSString selectedLanguage;

[Outlet]
public NSString SelectedLanguage
{
    get => selectedLanguage;
    set
    {
        WillChangeValue(nameof(SelectedLanguage));
        selectedLanguage = value;
        DidChangeValue(nameof(SelectedLanguage));
    }
}

Main.StoryBoardを開き、バインディングインスペクタに切り替え、Content Valueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathLanguagesを指定します。次にValueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathSelectedLanguageを指定します。

ComboBox02.png

ComboBox03.png

Xcodeを終了し、アプリケーションを実行します。

ComboBox01.gif

自作クラスとのバインド

次に自作クラスのオブジェクトをバインディングしてみます。Programmingというクラスを作成しLanguagesSelectedLanguageプロパティを追加します。TextField同様ProgrammingクラスはNSObjectを継承させます。

Programming.cs
public class Programming : NSObject
{
    public Programming()
    {
        Languages = new NSString[]
        {
            (NSString)"Bash",
            (NSString)"C#",
            (NSString)"C++",
            (NSString)"Java",
            (NSString)"JavaScript",
            (NSString)"Swift",
            (NSString)"Objective-C",
            (NSString)"Python"
        };
        selectedLanguage = Languages.FirstOrDefault();
    }

    [Outlet]
    public NSString[] Languages{ get; }

    private NSString selectedLanguage;

    [Outlet]
    public NSString SelectedLanguage
    {
        get => selectedLanguage;
        set
        {
            WillChangeValue(nameof(SelectedLanguage));
            selectedLanguage = value;
            DidChangeValue(nameof(SelectedLanguage));
        }
    }
}

TextField同様、ViewContoller内に上記のクラスのプロパティを追加します。

ViewController.cs
public ViewController(IntPtr handle) : base(handle)
{
    programming = new Programming();
}

partial void ShowMessage(NSObject sender)
{
    using (var alert = new NSAlert())
    {
        alert.MessageText = "あなたの好きな言語は...";
        alert.InformativeText = $"{Programming.SelectedLanguage}です。";
        alert.RunSheetModal(View.Window);
    }
}

private Programming programming;

[Outlet]
public Programming Programming
{
    get => programming;
    set
    {
        WillChangeValue(nameof(Programming));
        programming = value;
        DidChangeValue(nameof(Programming));
    }
}

上記同様、Main.StoryBoardを開き、バインディングインスペクタに切り替え、Content Valueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathProgramming.Languagesを指定します。次にValueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathProgramming.SelectedLanguageを指定します。

Xcodeを終了し、アプリケーションを実行します。

ComboBox03.png

NSPopUpButtonとのバインド

ViewController内のプロパティとバインド

ViewControllerにLanguagesプロパティを作成し、これにPopUpButtonとバインディングしてみます。

まずはMain.StoryBoardをXcodeで開き、ViewにPopUpButton、 Push Buttonを追加します。

PopUpButton01.png

上記と同様に次にボタンを押したときに実行されるメソッドを作成します。

作成後、ViewController.csにToDoプロパティ、選択された値を保持するSelectedToDo、ボタンを押した際の処理を記載します。

ViewContoller
public ViewController(IntPtr handle) : base(handle)
{
    ToDo = new NSString[]
    {
        (NSString)"Xamarin.Android",
        (NSString)"Xamarin.iOS",
        (NSString)"Xamarin.Mac",
        (NSString)"Xamarin.Forms",
        (NSString)"Electron",
    };
    selectedToDo = ToDoList.FirstOrDefault();
}

partial void ShowMessage(NSObject sender)
{
    using (var alert = new NSAlert())
    {
        alert.MessageText = "私が来年やることは...";
        alert.InformativeText = $"{ToDoList.SelectedToDo}です。";
        alert.RunSheetModal(View.Window);
    }
}

[Outlet]
public NSString[] ToDo { get; }

private NSString selectedToDo;

[Outlet]
public NSString SelectedToDo
{
    get => selectedLanguage;
    set
    {
        WillChangeValue(nameof(SelectedToDo));
        selectedToDo = value;
        DidChangeValue(nameof(SelectedToDo));
    }
}

Main.StoryBoardを開き、バインディングインスペクタに切り替え、Content Valueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathToDoを指定します。次にSelected Valueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathSelectedToDoを指定します。

PopUpButton02.png

PopUpButton03.png

Xcodeを終了し、アプリケーションを実行します。

PopUpButton03.gif

自作クラスとのバインド

次に自作クラスのオブジェクトをバインディングしてみます。ComboBoxと同様にPlanというクラスを作成ます。その後、ViewControllerにPlanプロパティを追加します(ソースコードはComboBoxとほぼ同様のため割愛します。)

上記同様、Main.StoryBoardを開き、バインディングインスペクタに切り替え、Content Valueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathPlan.ToDoを指定します。次にSelected Valueを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathPlan.SelectedToDoを指定します。

Xcodeを終了し、アプリケーションを実行します。(実行結果は上記同様のため割愛させていただきます。)

TableViewとのバインド

ViewControllerにPeopleプロパティを作成し、これにTableViewとバインディングしてみます。

まずは、 NSObjectを継承した、PersonというModelを作成します。

Person.cs
[Register(nameof(Person))]
public class Person : NSObject
{
    public Person(NSString name, NSNumber age)
    {
        Name =name;
        Age = age;
    }

    private NSString name;

    [Outlet]
    public NSString Name
    {
        get => name;
        set
        {
            WillChangeValue(nameof(Name));
            name = value;
            DidChangeValue(nameof(Name));
        }
    }

    private NSNumber age;

    [Outlet]
    public NSNumber Age
    {
        get => age;
        set
        {
            WillChangeValue(nameof(Age));
            age = value;
            DidChangeValue(nameof(Age));
        }
    }
}

次に、ViewConrrollerにPersonオブジェクトを保持するリストのプロパティを作成します。こちらとバインドするため、NSObjectを継承した、可変配列であるNSMutableArrayを採用しました。また、PopUpButton、TextFieldとバインドするプロパティ、InputAgeListSelectedAgeも追加します。

ViewController.cs
public partial class ViewController : NSViewController
{
    public ViewController(IntPtr handle) : base(handle)
    {
        people = new NSMutableArray();
        AgeList = Enumerable.Range(0, 100).Select(i => (NSNumber)i).ToArray()
        selectedAge = AgeList.FirstOrDefault();
    }

    private NSMutableArray people;

    [Outlet]
    public NSMutableArray People
    {
        get => people;
        set
        {
            WillChangeValue(nameof(People));
            people = value;
            DidChangeValue(nameof(People));
        }
    }

    private NSString input;

    [Outlet]
    public NSString Input
    {
        get => input;
        set
        {
            WillChangeValue(nameof(Input));
            input = value;
            DidChangeValue(nameof(Input));
        }
    }

    [Outlet]
    public NSNumber[] AgeList { get; }

    private NSNumber selectedAge;

    [Outlet]
    public NSNumber SelectedAge
    {
        get => selectedAge;
        set
        {
            WillChangeValue(nameof(SelectedAge));
            selectedAge = value;
            DidChangeValue(nameof(SelectedAge));
        }
    }
}

次に、Main.StoryBoardをXcodeで開き、ViewにNSTableView、SceneにArrayController、おまけで PopUpButton、 Push Buttonを追加します(今回はTableViewのカラム名をNameAgeとします)。

TableView01.png

TableView02.png

上記同様、ボタンを押したときに実行されるメソッドを作成し(今回はAddPersonとします)、TextFiled、PopUpButtoのバインドを行います。

TableView03.png

次に、ArrayContollerをViewControllerに配置します。ArrayControllerをViewController.hにContorolキーを押しながらドラッグ&ドロップします。Nameに適当な名前をつけ(今回はPersonArrayControllerとします)、ConnectボタンをクリックするとViewControllerに配置されます。

TableView04.png

次に、ArrayControllerを選択した状態で、Attributesの設定項目のObject Controllerを編集します。Class Name をPersonにし、KeysにNameAgeを追加します。

TableView05.png

バインディングインスペクタに切り替え、Controller Contentを展開し、Bind toにチェックを入れます。バインド先をViewControllerに変更し、Model Key PathPeopleを指定します。

TableView06.png

次に、TableViewを選択した状態で、バインディングインスペクタに切り替え、Table Contentを展開し、Bind toにチェックを入れます。バインド先をPerson Array Controllerに変更し、Controller KeyarrangedPbjectsと指定します。

TableView07.png

次にNameとTable View Cellを選択した状態でバインディングインスペクタに切り替え、Valueを展開し、Bind toにチェックを入れます。バインド先をTable Cell Viewにし、Model Key PathでそれぞれarrangedPbjects.Nameと指定します。Ageも同様の作業を行います。

TableView08.png

TableView09.png

最後に、AddPersonの処理を書きます。

ViewController.cs
partial void AddPerson(NSObject sender)
{
    var person = new Person(Input, SelectedAge);
    PersonArrayController.AddObject(person);
}

アプリケーションを実行します。

TableView01.gif

まとめ

強力とまで言われている、データバインディングがCocoaフレームワークに含まれています。MacOS独自の機構ではありますが、これを用いることで、より開発工数を削減できると思われます。Cocoa Bindingを使って素敵なXamarin.Macの開発を行ってみてはいかがでしょうか。