実践!Cocoa Binding

# 実践! Cocoa Binding

## はじめに

Cocoa Bindingとは、MacOS独自の変更通知機構を使ったViewとModel/Controller層の同期機能です。Cocoa Binding使うことにより、多くの[「Glue code(グルーコード)」](https://en.wikipedia.org/wiki/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とのバインド

### ViewController内のプロパティとバインド

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

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

![TextField01.png](https://qiita-image-store.s3.amazonaws.com/0/153907/13c7379e-f1ec-fdf9-157c-cb6c32c48765.png)

![TextField02.png](https://qiita-image-store.s3.amazonaws.com/0/153907/45843b28-a56c-1f1b-1746-ba2dcb1e8f3c.png)

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

![TextField03.png](https://qiita-image-store.s3.amazonaws.com/0/153907/479461d6-8a88-1c5c-12d1-5c2ae0969d2d.png)

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

```csharp: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 Path`で`Name`を指定します。

![TextField04.png](https://qiita-image-store.s3.amazonaws.com/0/153907/4a1f5845-3d1a-c30b-ba81-63b2a0e00c55.png)

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

![TextField01.gif](https://qiita-image-store.s3.amazonaws.com/0/153907/c86ccac2-5589-019a-bd72-21340a1b8650.gif)

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

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

![TextField05.png](https://qiita-image-store.s3.amazonaws.com/0/153907/8e2f9c3c-d17b-d6ac-36bf-45c7fc3c16f2.png)

![TextField02.gif](https://qiita-image-store.s3.amazonaws.com/0/153907/1d9c97c6-76e6-aaab-e659-e55eaf6be2b6.gif)

### 自作クラスとのバインド

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

```csharp: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内に上記のクラスのプロパティを追加します。

```csharp: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 Path`を`Person.Name`を指定します。

![TextField06.png](https://qiita-image-store.s3.amazonaws.com/0/153907/e6ac118f-2a0c-4f76-d568-807a5951ad8a.png)

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

![TextField03.gif](https://qiita-image-store.s3.amazonaws.com/0/153907/b692b7eb-89fe-351a-302f-8c362067aa5c.gif)

### Bottunの制御

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

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

```csharp: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 Path`で`Person.HasName`を指定します。

![TextField07.png](https://qiita-image-store.s3.amazonaws.com/0/153907/185b87e3-acf8-8f63-a080-c034d7a75537.png)

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

![TextField04.gif](https://qiita-image-store.s3.amazonaws.com/0/153907/c2b434f1-2da8-25cc-f062-28ef3e766067.gif)

## NSComboBoxとのバインド

### ViewController内のプロパティとバインド

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

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

![ComboBox01.png](https://qiita-image-store.s3.amazonaws.com/0/153907/bd0e544e-b5dd-ac8b-d09f-b2366bac97c0.png)

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

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

```csharp: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 Path`で`Languages`を指定します。次に`Value`を展開し、`Bind to`にチェックを入れます。バインド先をViewControllerに変更し、`Model Key Path`で`SelectedLanguage`を指定します。

![ComboBox02.png](https://qiita-image-store.s3.amazonaws.com/0/153907/111c51eb-65c8-f003-5741-80ebfccb764c.png)

![ComboBox03.png](https://qiita-image-store.s3.amazonaws.com/0/153907/30d9ff0c-7eb6-ed58-790c-9f48f52606af.png)

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

![ComboBox01.gif](https://qiita-image-store.s3.amazonaws.com/0/153907/2313c915-3b97-3cd3-2372-49a87c2ec93f.gif)

### 自作クラスとのバインド

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

```csharp: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内に上記のクラスのプロパティを追加します。

```csharp: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 Path`で`Programming.Languages`を指定します。次に`Value`を展開し、`Bind to`にチェックを入れます。バインド先をViewControllerに変更し、`Model Key Path`で`Programming.SelectedLanguage`を指定します。

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

![ComboBox03.png](https://qiita-image-store.s3.amazonaws.com/0/153907/71d1325a-8897-62ac-340b-c651b4a18132.png)

## NSPopUpButtonとのバインド

### ViewController内のプロパティとバインド

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

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

![PopUpButton01.png](https://qiita-image-store.s3.amazonaws.com/0/153907/139cc2fa-2e0b-0df0-06dc-1a316bb74260.png)

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

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

```csharp: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 Path`を`ToDo`を指定します。次に`Selected Value`を展開し、`Bind to`にチェックを入れます。バインド先をViewControllerに変更し、`Model Key Path`で`SelectedToDo`を指定します。

![PopUpButton02.png](https://qiita-image-store.s3.amazonaws.com/0/153907/67c7886c-f6ec-6fa5-42ef-78cd9a2a7d26.png)

![PopUpButton03.png](https://qiita-image-store.s3.amazonaws.com/0/153907/8e6efcc5-4651-07a6-86e6-2e4f700377b8.png)

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

![PopUpButton03.gif](https://qiita-image-store.s3.amazonaws.com/0/153907/64c3d9f0-b3bc-cf8c-1cc2-1f0bb1458ee8.gif)


### 自作クラスとのバインド

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

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

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

## TableViewとのバインド

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

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

```csharp: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とバインドするプロパティ、`Input`、`AgeList`、`SelectedAge`も追加します。

```csharp: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のカラム名を`Name`、`Age`とします)。

![TableView01.png](https://qiita-image-store.s3.amazonaws.com/0/153907/4663b9ae-efd3-7511-f807-4df30a8df74b.png)

![TableView02.png](https://qiita-image-store.s3.amazonaws.com/0/153907/f44ad4cc-623d-6e1a-bcf7-20ace500b6aa.png)


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

![TableView03.png](https://qiita-image-store.s3.amazonaws.com/0/153907/ffcd0138-299e-f810-a8aa-340fd136869f.png)

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

![TableView04.png](https://qiita-image-store.s3.amazonaws.com/0/153907/7967eb34-5eb6-b6aa-44f5-b521196f6ca1.png)

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

![TableView05.png](https://qiita-image-store.s3.amazonaws.com/0/153907/8ca2b3d9-1ea6-c198-687b-7cd6537bcc13.png)

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

![TableView06.png](https://qiita-image-store.s3.amazonaws.com/0/153907/2392ceb4-93f1-4605-23f7-dad1741a57b6.png)

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

![TableView07.png](https://qiita-image-store.s3.amazonaws.com/0/153907/867b0779-87ff-0811-2c55-1a9e5bd55efb.png)

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

![TableView08.png](https://qiita-image-store.s3.amazonaws.com/0/153907/070981e3-c948-215d-d1d0-17fc3db4fff8.png)

![TableView09.png](https://qiita-image-store.s3.amazonaws.com/0/153907/4d31fdd7-968f-7df1-26d8-301ae4dea913.png)


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

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

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

![TableView01.gif](https://qiita-image-store.s3.amazonaws.com/0/153907/70801743-c832-0433-b196-d32ce5a35cc8.gif)

## まとめ

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