Xamarin.Mac の Table View

この記事では、Xamarin.Mac アプリケーションでのテーブル ビューの使用について説明しています。 Xcode と Interface Builder でテーブル ビューを作成し、コードを使用して操作する方法について説明します。

Xamarin.Mac アプリケーションで C# と .NET を使用している場合、Objective-C や Xcode で開発者が使用しているのと同じ Table View にアクセスできます。 Xamarin.Mac は直接 Xcode と統合できるため、Xcode の Interface Builder を使用して、Table View を作成および管理できます (または、必要に応じて、C# コードで直接作成することもできます)。

Table View では、複数の行に 1 列以上の情報が含まれている表形式でデータが表示されます。 作成中の Table View の種類に基づいて、ユーザーは、列の並べ替え、列の再構成、列の追加、列の削除、またはテーブルに含まれているデータの編集を行うことができます。

テーブルの例

この記事では、Xamarin.Mac アプリケーションでの Table View の使用の基本について説明します。 まずは、Hello Mac に関する記事を参照することを強くお勧めします。特に、この記事で使用する主要な概念と手法について説明している「Xcode と Interface Builder の概要」セクションと「Outlet と Action」セクションを参照してください。

Xamarin.Mac Internals に関するドキュメントの「Objective-C への C# クラスとメソッドの公開」セクションの参照もお勧めします。そこでは、C# クラスを Objective-C オブジェクトと UI 要素に接続するために使用する Register コマンドと Export コマンドについて説明しています。

Table View の概要

Table View では、複数の行に 1 列以上の情報が含まれている表形式でデータが表示されます。 Table View は、Scroll View (NSScrollView) 内に表示されます。また、macOS 10.7 以降では、ユーザーは、Cell (NSCell) ではなく、任意の NSView を使用して、行と列の両方を表示できます。 ただし、NSCell は引き続き使用できますが、通常は、NSTableCellView をサブクラス化して、カスタムの行と列を作成します。

Table View は、独自のデータを格納するのではなく、必要に応じて必要な行と列の両方を提供する Data Source (NSTableViewDataSource) に依存しています。

Table View の動作をカスタマイズするには、テーブル列の管理、機能選択の種類、行の選択と編集、カスタム追跡、個々の列と行のカスタム ビューをサポートする Table View Delegate (NSTableViewDelegate) のサブクラスを指定します。

Table View を作成する場合、Apple は次を提案しています。

  • ユーザーが Column Header をクリックしてテーブルを並べ替えられるようにする。
  • その列に表示されるデータを記述する名詞または短い名詞句である Column Header を作成する。

詳細については、Apple の OS X ヒューマン インターフェイス ガイドラインのコンテンツ ビューに関するセクションを参照してください。

Xcode での Table View の作成と管理

新しい Xamarin.Mac Cocoa アプリケーションを作成すると、既定で標準の空白のウィンドウが表示されます。 このウィンドウは、プロジェクトに自動的に含まれる .storyboard ファイルで定義されます。 Windows デザインを編集するには、ソリューション エクスプローラーで、Main.storyboard ファイルをダブルクリックします:

メイン ストーリーボードの選択

これにより、Xcode の Interface Builder でウィンドウ デザインが開きます:

Xcode での UI の編集

Library Inspector の検索ボックスに「table」と入力すると、Table View コントロールを見つけやすくなります。

ライブラリからの Table View の選択

Table View を Interface Editor の View Controller にドラッグし、View Controller のコンテンツ領域を塗りつぶし、それを Constraint Editor のウィンドウで縮小拡大する場所に設定します。

制約の編集

Interface Hierarchy で Table View を選択すると、Attribute Inspector で次のプロパティを使用できます。

使用できるプロパティが Attribute Inspector に表示されているスクリーンショット。

  • Content Mode - ビュー (NSView) またはセル (NSCell) を使用して、行や列にデータを表示できます。 macOS 10.7 以降では、ビューを使用する必要があります。
  • Floats Group Rows - true の場合、テーブル ビューでは、グループ化されたセルが浮かんでいるように描画されます。
  • Columns - 表示する列の数を定義します。
  • Headers - true の場合、列にはヘッダーが含まれます。
  • Reordering - true の場合、ユーザーは、テーブル内の列をドラッグして並べ替えることができます。
  • Resizing - true の場合、ユーザーは、列のヘッダーをドラッグして列のサイズを変更できます。
  • Column Sizing - テーブルで列のサイズを自動的に設定する方法を制御します。
  • Highlight - セルを選択したときにテーブルで使用する強調表示の種類を制御します。
  • Alternate Rows - true の場合、他の行は別の背景色になります。
  • Horizontal Grid - セルの間で横に引く罫線の種類を選択します。
  • Vertical Grid - セルの間で縦に引く罫線の種類を選択します。
  • Grid Color - セルの罫線の色を設定します。
  • Background - セルの背景色を設定します。
  • Selection - ユーザーによるテーブル内のセルの選択方法を制御できます。
    • Multiple - true の場合、ユーザーは複数の行と列を選択できます。
    • Column - true の場合、ユーザーは列を選択できます。
    • Type Select - true の場合、ユーザーは文字を入力して行を選択できます。
    • Empty - true の場合、ユーザーは、行や列を選択する必要はなく、テーブルでは選択を行うことができません。
  • Autosave - 自動保存されるときのテーブル形式の名前。
  • Column Information - true の場合、列の順序と幅が自動保存されます。
  • Line Breaks - セルでの改行の処理方法を選択します。
  • Truncates Last Visible Line - true の場合、セルの境界内に収まらないデータは切り取られます。

重要

従来の Xamarin.Mac アプリケーションを管理している場合を除き、NSCell ベースの Table View ではなく、NSView ベースの Table View を使用する必要があります。 NSCell はレガシと見なされており、今後サポートされなくなる可能性があります。

Interface Hierarchy で Table Column を選択すると、Attribute Inspector で次のプロパティを使用できます。

Table Column で使用できるプロパティが Attribute Inspector に表示されているスクリーンショット。

  • Title - 列のタイトルを設定します。
  • Alignment - セル内のテキストの配置を設定します。
  • Title Font - セルのヘッダー テキストのフォントを選択します。
  • Sort Key - 列内のデータの並べ替えに使用するキーです。 ユーザーがこの列を並べ替えることができない場合は、空白のままにします。
  • Selector - 並べ替えの実行に使用する Action です。 ユーザーがこの列を並べ替えることができない場合は、空白のままにします。
  • Order - 列のデータの並べ替え順序です。
  • Resizing - 列のサイズ変更の種類を選択します。
  • Editable - true の場合、ユーザーはセル ベース テーブル内のセルを編集できます。
  • Hidden - true の場合、列は非表示になります。

また、ハンドルを左または右 (列の右側の垂直方向の中心) にドラッグすると列のサイズを変更できます。

テーブル ビューで各列を選択して、最初の列のタイトルとして Product を指定し、2 番目の列に Details を指定します。

Interface Hierarchy で Table Cell View (NSTableViewCell) を選択すると、Attribute Inspector で次のプロパティを使用できます。

Table Cell View で使用できるプロパティが Attribute Inspector に表示されているスクリーンショット。

これらはすべて、標準ビューのプロパティです。 この列の行のサイズを変更することもできます。

Interface Hierarchy で Table Cell View (これは既定で NSTextField です) を選択すると、Attribute Inspector で次のプロパティを使用できます。

Table View Cell で使用できるプロパティが Attribute Inspector に表示されているスクリーンショット。

ここで設定する標準テキスト フィールドのプロパティはすべて用意されています。 既定では、標準のテキスト フィールドを使用して、列内のセルのデータが表示されます。

Interface Hierarchy で Table Cell View (NSTableFieldCell) を選択すると、Attribute Inspector で次のプロパティを使用できます。

別の Table View Cell で使用できるプロパティが Attribute Inspector に表示されているスクリーンショット。

ここで最も重要な設定は、次のとおりです。

  • Layout - この列のセルのレイアウト方法を選択します。
  • Uses Single Line Mode - true の場合、セルは 1 行に制限されます。
  • First Runtime Layout Width - true の場合、アプリケーションの初回実行時に設定 (手動または自動的に) されているセルの幅を使用します。
  • Action - セルに対して Edit Action を送信するタイミングを制御します。
  • Behavior - セルが選択可能または編集可能かどうかを定義します。
  • Rich Text - true の場合、セルには書式設定およびスタイル設定されたテキストが表示されます。
  • Undo - true の場合、セルは元に戻す動作を担います。

Interface Hierarchy の Table Column の下部にある Table Cell View (NSTableFieldCell) を選択します。

Table Cell View の選択

この操作を行うと、特定の列に対して作成されたすべてのセルの基本 Pattern として使用されている Table Cell View を編集できます。

Action と Outlet の追加

他の Cocoa UI コントロールと同様に、Action と Outlet を使用して (必要な機能に基づく)、Table View とその列とセルを C# コードに公開する必要があります。

このプロセスは、公開する Table View 要素でも同じです。

  1. Assistant Editor に切り替えて、ViewController.h ファイルが選択されていることを確認します。

    Assistant Editor

  2. Interface Hierarchy から Table View を選択し、Ctrl キーを押しながらクリックして ViewController.h ファイルにドラッグします。

  3. ProductTable という名前の Table View の Outlet を作成します。

    ProductTable という名前の Table View 用に作成された Outlet 接続を示すスクリーンショット。

  4. ProductColumn と DetailsColumn という名前のテーブル列の Outlet を作成します。

    他の Table Views 用に作成された Outlet 接続を示すスクリーンショット。

  5. 変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

次に、アプリケーションの実行時にテーブルのデータの一部を表示するコードを記述します。

Table View の設定

次に、Interface Builder で設計されており、Outlet 経由で公開されている Table View で、それを設定するための C# コードを作成する必要があります。

まずは、個々の行の情報を保持するために、新しい Product クラスを作成しましょう。 ソリューション エクスプローラーでプロジェクトを右クリックし、[Add]>[New File...] を選択します[General]>[Empty Class] を選択し、[Name] で「Product」と入力して、[New] ボタンをクリックします。

空のクラスの作成

Product.cs ファイルを次のようにします。

using System;

namespace MacTables
{
  public class Product
  {
    #region Computed Properties
    public string Title { get; set;} = "";
    public string Description { get; set;} = "";
    #endregion

    #region Constructors
    public Product ()
    {
    }

    public Product (string title, string description)
    {
      this.Title = title;
      this.Description = description;
    }
    #endregion
  }
}

次に、NSTableDataSource のサブクラスを作成して、テーブルのデータを要求どおりに指定する必要があります。 ソリューション エクスプローラーでプロジェクトを右クリックし、[Add]>[New File...] を選択します[General]>[Empty Class] を選択し、[Name] に「ProductTableDataSource」と入力して、[New] ボタンをクリックします。

ProductTableDataSource.cs ファイルを編集して、次のようにします。

using System;
using AppKit;
using CoreGraphics;
using Foundation;
using System.Collections;
using System.Collections.Generic;

namespace MacTables
{
  public class ProductTableDataSource : NSTableViewDataSource
  {
    #region Public Variables
    public List<Product> Products = new List<Product>();
    #endregion

    #region Constructors
    public ProductTableDataSource ()
    {
    }
    #endregion

    #region Override Methods
    public override nint GetRowCount (NSTableView tableView)
    {
      return Products.Count;
    }
    #endregion
  }
}

このクラスでは、Table View の項目用のストレージがあり、テーブル内の行数を返すように GetRowCount がオーバーライドされます。

最後に、テーブルの動作を指定する NSTableDelegate のサブクラスを作成する必要があります。 ソリューション エクスプローラーでプロジェクトを右クリックし、[Add]>[New File...] を選択します[General]>[Empty Class] を選択し、[Name] に「ProductTableDelegate」と入力して、[New] ボタンをクリックします。

ProductTableDelegate.cs ファイルを編集して、次のようにします。

using System;
using AppKit;
using CoreGraphics;
using Foundation;
using System.Collections;
using System.Collections.Generic;

namespace MacTables
{
  public class ProductTableDelegate: NSTableViewDelegate
  {
    #region Constants
    private const string CellIdentifier = "ProdCell";
    #endregion

    #region Private Variables
    private ProductTableDataSource DataSource;
    #endregion

    #region Constructors
    public ProductTableDelegate (ProductTableDataSource datasource)
    {
      this.DataSource = datasource;
    }
    #endregion

    #region Override Methods
    public override NSView GetViewForItem (NSTableView tableView, NSTableColumn tableColumn, nint row)
    {
      // This pattern allows you reuse existing views when they are no-longer in use.
      // If the returned view is null, you instance up a new view
      // If a non-null view is returned, you modify it enough to reflect the new data
      NSTextField view = (NSTextField)tableView.MakeView (CellIdentifier, this);
      if (view == null) {
        view = new NSTextField ();
        view.Identifier = CellIdentifier;
        view.BackgroundColor = NSColor.Clear;
        view.Bordered = false;
        view.Selectable = false;
        view.Editable = false;
      }

      // Setup view based on the column selected
      switch (tableColumn.Title) {
      case "Product":
        view.StringValue = DataSource.Products [(int)row].Title;
        break;
      case "Details":
        view.StringValue = DataSource.Products [(int)row].Description;
        break;
      }

      return view;
    }
    #endregion
  }
}

ProductTableDelegate のインスタンスを作成するときに、テーブルにデータを提供する ProductTableDataSource のインスタンスも渡します。 GetViewForItem メソッドでは、ビュー (データ) を返して、give 列と行のセルを表示します。 可能であれば、新しいビューを作成する必要がない場合は、既存のビューを再利用してセルを表示します。

テーブルを設定するには、ViewController.cs ファイルを編集して、AwakeFromNib メソッドを次のようにします。

public override void AwakeFromNib ()
{
  base.AwakeFromNib ();

  // Create the Product Table Data Source and populate it
  var DataSource = new ProductTableDataSource ();
  DataSource.Products.Add (new Product ("Xamarin.iOS", "Allows you to develop native iOS Applications in C#"));
  DataSource.Products.Add (new Product ("Xamarin.Android", "Allows you to develop native Android Applications in C#"));
  DataSource.Products.Add (new Product ("Xamarin.Mac", "Allows you to develop Mac native Applications in C#"));

  // Populate the Product Table
  ProductTable.DataSource = DataSource;
  ProductTable.Delegate = new ProductTableDelegate (DataSource);
}

アプリケーションを実行すると、次が表示されます。

3 つのエントリが表示された [プロダクト テーブル] というウィンドウを示すスクリーンショット。

Column ごとの並べ替え

ユーザーが Column Header をクリックしてテーブルを並べ替えられるようにしましょう。 まず、Main.storyboard ファイルをダブルクリックして、Interface Builder で編集用に開きます。 Product 列を選択し、[Sort Key] に「Title」、[Selector] に「compare:」と入力して、[Order] で Ascending を選択します。

Product 列の並べ替えキーを設定できる Interface Builder を示すスクリーンショット。

Details 列を選択し、[Sort Key] に「Description」、[Selector] に「compare:」と入力して、[Order] で Ascending を選択します。

Details 列の並べ替えキーを設定できる Interface Builder を示すスクリーンショット。

変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

次に、ProductTableDataSource.cs ファイルを編集して、次のメソッドを追加します。

public void Sort(string key, bool ascending) {

  // Take action based on key
  switch (key) {
  case "Title":
    if (ascending) {
      Products.Sort ((x, y) => x.Title.CompareTo (y.Title));
    } else {
      Products.Sort ((x, y) => -1 * x.Title.CompareTo (y.Title));
    }
    break;
  case "Description":
    if (ascending) {
      Products.Sort ((x, y) => x.Description.CompareTo (y.Description));
    } else {
      Products.Sort ((x, y) => -1 * x.Description.CompareTo (y.Description));
    }
    break;
  }

}

public override void SortDescriptorsChanged (NSTableView tableView, NSSortDescriptor[] oldDescriptors)
{
  // Sort the data
  if (oldDescriptors.Length > 0) {
    // Update sort
    Sort (oldDescriptors [0].Key, oldDescriptors [0].Ascending);
  } else {
    // Grab current descriptors and update sort
    NSSortDescriptor[] tbSort = tableView.SortDescriptors;
    Sort (tbSort[0].Key, tbSort[0].Ascending);
  }

  // Refresh table
  tableView.ReloadData ();
}

Sort メソッドを使用すると、特定の Product クラス フィールドに基づいて、データ ソース内のデータを昇順または降順で並べ替えることができます。 オーバーライドされた SortDescriptorsChanged メソッドが、列見出しをクリックするたびに呼び出されます。 Interface Builder で設定したキー値と、その列の並べ替え順序が渡されます。

アプリケーションを実行して列ヘッダーをクリックすると、行がその列ごとに並べ替えられます。

アプリの実行例

行の選択

ユーザーが 1 つの行を選択できるようにする場合は、Main.storyboard ファイルをダブルクリックして、Interface Builder で編集用に開きます。 Interface Hierarchy で Table View を選択し、Attribute Inspector の [Multiple] チェックボックスのチェックを外します。

Attribute Inspector で [Multiple] を選択できる Interface Builder を示すスクリーンショット。

変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

次に、ProductTableDelegate.cs ファイルを編集し、次のメソッドを追加します。

public override bool ShouldSelectRow (NSTableView tableView, nint row)
{
  return true;
}

これにより、ユーザーは Table view のあらゆる行を選択できるようになります。 ユーザーが行を選択できないようにする場合は、ユーザーが選択または false に設定できないようにしたい行で、ShouldSelectRow に対して false を返します。

Table View (NSTableView) には、行の選択を操作するための次のメソッドが含まれています。

  • DeselectRow(nint) - テーブル内の特定の行の選択を解除します。
  • SelectRow(nint,bool) - 特定の行を選択します。 2 番目の false パラメータを渡して、一度に 1 行のみ選択します。
  • SelectedRow - テーブル内で選択している現在の表を返します。
  • IsRowSelected(nint) - 特定の行が選択されている場合は true を返します。

複数行の選択

ユーザーが複数の行を選択できるようにする場合は、Main.storyboard ファイルをダブルクリックして、Interface Builder で編集用に開きます。 Interface Hierarchy で Table View を選択し、Attribute Inspector の [Multiple] チェックボックスにチェックを入れます。

[Multiple] を選択して複数の行を選択できる Interface Builder を示すスクリーンショット。

変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

次に、ProductTableDelegate.cs ファイルを編集し、次のメソッドを追加します。

public override bool ShouldSelectRow (NSTableView tableView, nint row)
{
  return true;
}

これにより、ユーザーは Table view のあらゆる行を選択できるようになります。 ユーザーが行を選択できないようにする場合は、ユーザーが選択または false に設定できないようにしたい行で、ShouldSelectRow に対して false を返します。

Table View (NSTableView) には、行の選択を操作するための次のメソッドが含まれています。

  • DeselectAll(NSObject) - テーブル内のすべての行の選択を解除します。 選択を行うオブジェクト内で送信する最初のパラメータに this を使用します。
  • DeselectRow(nint) - テーブル内の特定の行の選択を解除します。
  • SelectAll(NSobject) - テーブル内のすべての行を選択します。 選択を行うオブジェクト内で送信する最初のパラメータに this を使用します。
  • SelectRow(nint,bool) - 特定の行を選択します。 2 番目のパラメータの false を渡し、選択範囲を解除して 1 行のみを選択し、true を渡して選択範囲を広げ、この行を含めます。
  • SelectRows(NSIndexSet,bool) - 特定の行のセットを選択します。 2 番目のパラメータの false を渡し、選択範囲を解除してこれらの行のみを選択し、true を渡して選択範囲を広げ、これらの行を含めます。
  • SelectedRow - テーブル内で選択している現在の表を返します。
  • SelectedRows - 選択した行のインデックスを含む NSIndexSet を返します。
  • SelectedRowCount - 選択した行の数を返します。
  • IsRowSelected(nint) - 特定の行が選択されている場合は true を返します。

入力による行の選択

選択した Table View でユーザーが文字を入力して、その文字が含まれている最初の行を選択できるようにする場合は、Main.storyboard ファイルをダブルクリックして、Interface Builder で編集用に開きます。 Interface Hierarchy で Table View を選択し、Attribute Inspector の [Type Select] チェックボックスにチェックを入れます。

選択の種類の設定

変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

ここで、ProductTableDelegate.cs ファイルを編集し、次のメソッドを追加しましょう。

public override nint GetNextTypeSelectMatch (NSTableView tableView, nint startRow, nint endRow, string searchString)
{
  nint row = 0;
  foreach(Product product in DataSource.Products) {
    if (product.Title.Contains(searchString)) return row;

    // Increment row counter
    ++row;
  }

  // If not found select the first row
  return 0;
}

GetNextTypeSelectMatch メソッドでは、特定の searchString を受け取り、その Title にその文字列が含まれている最初の Product の行を返します。

アプリケーションを実行して文字を入力すると、行が選択されます。

アプリケーションを実行した結果を示すスクリーンショット。

列の並べ替え

ユーザーが Table View で並べ替える列をドラッグできるようにする場合は、Main.storyboard ファイルをダブルクリックして、Interface Builder で編集用に開きます。 Interface Hierarchy で Table View を選択し、Attribute Inspector の [Reordering] チェックボックスにチェックを入れます。

Attribute Inspector で [Reordering] を選択できる Interface Builder を示すスクリーンショット。

[Autosave] プロパティに値を指定して Column Information フィールドにチェックを入れている場合、テーブルのレイアウトに加えた変更は自動保存され、次回アプリケーションが実行されたときに復元されます。

変更内容を保存し、Visual Studio for Mac に戻って Xcode と同期します。

ここで、ProductTableDelegate.cs ファイルを編集し、次のメソッドを追加しましょう。

public override bool ShouldReorder (NSTableView tableView, nint columnIndex, nint newColumnIndex)
{
  return true;
}

ShouldReorder メソッドでは、newColumnIndex にドラッグして並べ替える必要がある列の true を返す必要があります。それ以外の場合は、false を返します。

アプリケーションを実行する場合は、列ヘッダーをドラッグして列を並べ替えることができます。

並べ替えた列の例

セルの編集

ユーザーが特定のセルの値を編集できるようにする場合は、ProductTableDelegate.cs ファイルを編集し、GetViewForItem メソッドを次のように変更します。

public override NSView GetViewForItem (NSTableView tableView, NSTableColumn tableColumn, nint row)
{
  // This pattern allows you reuse existing views when they are no-longer in use.
  // If the returned view is null, you instance up a new view
  // If a non-null view is returned, you modify it enough to reflect the new data
  NSTextField view = (NSTextField)tableView.MakeView (tableColumn.Title, this);
  if (view == null) {
    view = new NSTextField ();
    view.Identifier = tableColumn.Title;
    view.BackgroundColor = NSColor.Clear;
    view.Bordered = false;
    view.Selectable = false;
    view.Editable = true;

    view.EditingEnded += (sender, e) => {

      // Take action based on type
      switch(view.Identifier) {
      case "Product":
        DataSource.Products [(int)view.Tag].Title = view.StringValue;
        break;
      case "Details":
        DataSource.Products [(int)view.Tag].Description = view.StringValue;
        break;
      }
    };
  }

  // Tag view
  view.Tag = row;

  // Setup view based on the column selected
  switch (tableColumn.Title) {
  case "Product":
    view.StringValue = DataSource.Products [(int)row].Title;
    break;
  case "Details":
    view.StringValue = DataSource.Products [(int)row].Description;
    break;
  }

  return view;
}

アプリケーションを実行するとユーザーはテーブル ビューのセルを編集できます。

セルの編集の例

Table View での画像の使用

NSTableViewのセルの一部として画像を含めるには、テーブル ビューの NSTableViewDelegate's GetViewForItem メソッドによってデータが返される方法を変更して、一般的なNSTextFieldではなくNSTableCellViewを使用する必要があります。 次に例を示します。

public override NSView GetViewForItem (NSTableView tableView, NSTableColumn tableColumn, nint row)
{

  // This pattern allows you reuse existing views when they are no-longer in use.
  // If the returned view is null, you instance up a new view
  // If a non-null view is returned, you modify it enough to reflect the new data
  NSTableCellView view = (NSTableCellView)tableView.MakeView (tableColumn.Title, this);
  if (view == null) {
    view = new NSTableCellView ();
    if (tableColumn.Title == "Product") {
      view.ImageView = new NSImageView (new CGRect (0, 0, 16, 16));
      view.AddSubview (view.ImageView);
      view.TextField = new NSTextField (new CGRect (20, 0, 400, 16));
    } else {
      view.TextField = new NSTextField (new CGRect (0, 0, 400, 16));
    }
    view.TextField.AutoresizingMask = NSViewResizingMask.WidthSizable;
    view.AddSubview (view.TextField);
    view.Identifier = tableColumn.Title;
    view.TextField.BackgroundColor = NSColor.Clear;
    view.TextField.Bordered = false;
    view.TextField.Selectable = false;
    view.TextField.Editable = true;

    view.TextField.EditingEnded += (sender, e) => {

      // Take action based on type
      switch(view.Identifier) {
      case "Product":
        DataSource.Products [(int)view.TextField.Tag].Title = view.TextField.StringValue;
        break;
      case "Details":
        DataSource.Products [(int)view.TextField.Tag].Description = view.TextField.StringValue;
        break;
      }
    };
  }

  // Tag view
  view.TextField.Tag = row;

  // Setup view based on the column selected
  switch (tableColumn.Title) {
  case "Product":
    view.ImageView.Image = NSImage.ImageNamed ("tags.png");
    view.TextField.StringValue = DataSource.Products [(int)row].Title;
    break;
  case "Details":
    view.TextField.StringValue = DataSource.Products [(int)row].Description;
    break;
  }

  return view;
}

詳細については、画像の使用に関するドキュメントの Table View での画像の使用に関するセクションを参照してください。

Row への [Delete] ボタンの追加

アプリの要件に基づいて、テーブルの各行にアクション ボタンの設定が必要となる場合があります。 この例として、上で作成した Table View の例を展開して、各行に [Delete] ボタンを含めましょう。

まず、Xcode の Interface Builder で、Main.storyboard を編集し、Table View を選択して、列の数を 3 つに増やします。 次に、新しい列の Title を Action に変更します。

列名の編集

Storyboard への変更を保存し、Visual Studio for Mac に戻って変更を同期します。

次に、ViewController.cs ファイルを編集し、次のパブリック メソッドを追加します。

public void ReloadTable ()
{
  ProductTable.ReloadData ();
}

同じファイルで、次のように、ViewDidLoad メソッド内で新しい Table View Delegate の作成を変更します。

// Populate the Product Table
ProductTable.DataSource = DataSource;
ProductTable.Delegate = new ProductTableDelegate (this, DataSource);

次に、ProductTableDelegate.cs ファイルを編集し、View Controller へのプライベート接続を含めて、デリゲートの新しいインスタンスを作成するときにコントローラーをパラメータとして受け取ります。

#region Private Variables
private ProductTableDataSource DataSource;
private ViewController Controller;
#endregion

#region Constructors
public ProductTableDelegate (ViewController controller, ProductTableDataSource datasource)
{
  this.Controller = controller;
  this.DataSource = datasource;
}
#endregion

ここで、次の新しいプライベート メソッドをクラスに追加します。

private void ConfigureTextField (NSTableCellView view, nint row)
{
  // Add to view
  view.TextField.AutoresizingMask = NSViewResizingMask.WidthSizable;
  view.AddSubview (view.TextField);

  // Configure
  view.TextField.BackgroundColor = NSColor.Clear;
  view.TextField.Bordered = false;
  view.TextField.Selectable = false;
  view.TextField.Editable = true;

  // Wireup events
  view.TextField.EditingEnded += (sender, e) => {

    // Take action based on type
    switch (view.Identifier) {
    case "Product":
      DataSource.Products [(int)view.TextField.Tag].Title = view.TextField.StringValue;
      break;
    case "Details":
      DataSource.Products [(int)view.TextField.Tag].Description = view.TextField.StringValue;
      break;
    }
  };

  // Tag view
  view.TextField.Tag = row;
}

この操作を行うと、GetViewForItem メソッドで以前に実行されていたすべての Text View 構成が取得されて、呼び出し可能な 1 か所に配置されます (テーブルの最後の列には Text View ではなく Button が含まれているため)。

最後に、GetViewForItem メソッドを編集し、次のようにします。

public override NSView GetViewForItem (NSTableView tableView, NSTableColumn tableColumn, nint row)
{

  // This pattern allows you reuse existing views when they are no-longer in use.
  // If the returned view is null, you instance up a new view
  // If a non-null view is returned, you modify it enough to reflect the new data
  NSTableCellView view = (NSTableCellView)tableView.MakeView (tableColumn.Title, this);
  if (view == null) {
    view = new NSTableCellView ();

    // Configure the view
    view.Identifier = tableColumn.Title;

    // Take action based on title
    switch (tableColumn.Title) {
    case "Product":
      view.ImageView = new NSImageView (new CGRect (0, 0, 16, 16));
      view.AddSubview (view.ImageView);
      view.TextField = new NSTextField (new CGRect (20, 0, 400, 16));
      ConfigureTextField (view, row);
      break;
    case "Details":
      view.TextField = new NSTextField (new CGRect (0, 0, 400, 16));
      ConfigureTextField (view, row);
      break;
    case "Action":
      // Create new button
      var button = new NSButton (new CGRect (0, 0, 81, 16));
      button.SetButtonType (NSButtonType.MomentaryPushIn);
      button.Title = "Delete";
      button.Tag = row;

      // Wireup events
      button.Activated += (sender, e) => {
        // Get button and product
        var btn = sender as NSButton;
        var product = DataSource.Products [(int)btn.Tag];

        // Configure alert
        var alert = new NSAlert () {
          AlertStyle = NSAlertStyle.Informational,
          InformativeText = $"Are you sure you want to delete {product.Title}? This operation cannot be undone.",
          MessageText = $"Delete {product.Title}?",
        };
        alert.AddButton ("Cancel");
        alert.AddButton ("Delete");
        alert.BeginSheetForResponse (Controller.View.Window, (result) => {
          // Should we delete the requested row?
          if (result == 1001) {
            // Remove the given row from the dataset
            DataSource.Products.RemoveAt((int)btn.Tag);
            Controller.ReloadTable ();
          }
        });
      };

      // Add to view
      view.AddSubview (button);
      break;
    }

  }

  // Setup view based on the column selected
  switch (tableColumn.Title) {
  case "Product":
    view.ImageView.Image = NSImage.ImageNamed ("tag.png");
    view.TextField.StringValue = DataSource.Products [(int)row].Title;
    view.TextField.Tag = row;
    break;
  case "Details":
    view.TextField.StringValue = DataSource.Products [(int)row].Description;
    view.TextField.Tag = row;
    break;
  case "Action":
    foreach (NSView subview in view.Subviews) {
      var btn = subview as NSButton;
      if (btn != null) {
        btn.Tag = row;
      }
    }
    break;
  }

  return view;
}

このコードのいくつかのセクションを詳しく見てみましょう。 まず、新しい NSTableViewCell が作成中の場合は、Column の名前に基づいてアクションが実行されます。 最初の 2 つの列 (Product と Details) に対して、新しい ConfigureTextField メソッドが呼び出されます。

Action 列では、新しい NSButton が作成されて、Sub View として Cell に追加されます。

// Create new button
var button = new NSButton (new CGRect (0, 0, 81, 16));
button.SetButtonType (NSButtonType.MomentaryPushIn);
button.Title = "Delete";
button.Tag = row;
...

// Add to view
view.AddSubview (button);

Button の Tag プロパティは、現在処理中の行の数を保持するために使用されます。 この数は、後で、ユーザーが Button の Activated イベントで削除する行を要求したときに使用します。

// Wireup events
button.Activated += (sender, e) => {
  // Get button and product
  var btn = sender as NSButton;
  var product = DataSource.Products [(int)btn.Tag];

  // Configure alert
  var alert = new NSAlert () {
    AlertStyle = NSAlertStyle.Informational,
    InformativeText = $"Are you sure you want to delete {product.Title}? This operation cannot be undone.",
    MessageText = $"Delete {product.Title}?",
  };
  alert.AddButton ("Cancel");
  alert.AddButton ("Delete");
  alert.BeginSheetForResponse (Controller.View.Window, (result) => {
    // Should we delete the requested row?
    if (result == 1001) {
      // Remove the given row from the dataset
      DataSource.Products.RemoveAt((int)btn.Tag);
      Controller.ReloadTable ();
    }
  });
};

イベント ハンドラーの開始時に、特定のテーブル行にあるボタンとプロダクトを取得します。 その後、行の削除を確認するアラートがユーザーに表示されます。 ユーザーが行の削除を選択すると、特定の行が Data Source から削除され、テーブルの再読み込みが行われます。

// Remove the given row from the dataset
DataSource.Products.RemoveAt((int)btn.Tag);
Controller.ReloadTable ();

最後に、Table View Cell を新規作成するのではなく再利用する場合は、処理中の Column に基づいて、次のコードによって構成されます。

// Setup view based on the column selected
switch (tableColumn.Title) {
case "Product":
  view.ImageView.Image = NSImage.ImageNamed ("tag.png");
  view.TextField.StringValue = DataSource.Products [(int)row].Title;
  view.TextField.Tag = row;
  break;
case "Details":
  view.TextField.StringValue = DataSource.Products [(int)row].Description;
  view.TextField.Tag = row;
  break;
case "Action":
  foreach (NSView subview in view.Subviews) {
    var btn = subview as NSButton;
    if (btn != null) {
      btn.Tag = row;
    }
  }
  break;
}

Action 列では、NSButton が見つかるまで、すべての Sub View がスキャンされます。その後、その Tag プロパティは、現在の Row を指すように更新されます。

これらの変更を行うと、アプリを実行したときに、各行に [Delete] ボタンが表示されます。

削除ボタンが表示された Table View

ユーザーが [Delete] ボタンをクリックすると、特定の Row を削除するように求めるアラートが表示されます。

行の削除アラート

ユーザーが削除を選択すると、行が削除され、テーブルが再作成されます。

行が削除された後のテーブル

Data Binding Table View

Xamarin.Mac アプリケーションで Key-Value Coding と Data Binding の手法を使用すると、UI 要素を設定して操作するために記述および管理が必要なコードの量を大幅に減らすことができます。 また、バッキング データ (データ モデル) をフロントエンド ユーザー インターフェイス (Model-View-Controller) からさらに切り離すことで、アプリケーション デザインを管理しやすくなるという利点もあります。

キーと値のコーディング (KVC) は、インスタンス変数やアクセサー メソッド (get/set) を介してアクセスするのではなく、キー (特別にフォーマットされた文字列) を使用してプロパティを識別し、オブジェクトのプロパティに間接的にアクセスするためのメカニズムです。 Xamarin.Mac アプリケーションで Key-Value Coding に準拠したアクセサーを実装すると、Key-Value Observing (KVO)、Data Binding、Core Data、Cocoa バインディング、スクリプト機能など、他の macOS 機能にアクセスできます。

詳細については、Data Binding と Key-Value Coding に関するドキュメントの「テーブル ビューのデータ バインディング」セクションを参照してください。

まとめ

この記事では、Xamarin.Mac アプリケーションでの Table View の使用について詳しく説明しました。 Table View のさまざまな種類と使用方法、Xcode の Interface Builder で Table View を作成および管理する方法、C# コードで Table View を操作する方法について確認しました。