Xamarin.Mac 中的表视图

本文介绍如何在 Xamarin.Mac 应用程序中使用表视图。 它介绍如何在 Xcode 和 Interface Builder 中创建表视图,并在代码中与其交互。

在 Xamarin.Mac 应用程序中使用 C# 和 .NET 时,你可以访问的表视图与使用 Objective-CXcode 的开发人员访问的菜单相同。 由于 Xamarin.Mac 与 Xcode 直接集成,你可以使用 Xcode 的 Interface Builder 来创建和维护表视图(或选择直接使用 C# 代码创建)

表视图以表格格式显示数据,其中包含多行中一列或多列信息。 根据要创建的表视图的类型,用户可以按列排序、重新组织列、添加列、删除列或编辑表中包含的数据。

示例表

本文将介绍在 Xamarin.Mac 应用程序中使用表视图的基础知识。 强烈建议先阅读 Hello, Mac 一文,特别是 Xcode 和 Interface Builder 简介输出口和操作部分,因为其中介绍了我们将在本文中使用的关键概念和技术。

你可能还需要查看 Xamarin.Mac 内部机制文档的向 Objective-C 公开 C# 类/方法部分,因为其中介绍了用于将 C# 类连接到 Objective-C 对象和 UI 元素的 RegisterExport 命令。

表视图简介

表视图以表格格式显示数据,其中包含多行中一列或多列信息。 表视图显示在滚动视图 (NSScrollView) 内,从 macOS 10.7 开始,可以使用任何 NSView 而不是单元格 (NSCell) 来显示行和列。 也就是说,你仍然可以使用 NSCell,但通常会子类化 NSTableCellView 并创建自定义行和列。

表视图不存储它自己的数据,而是依赖于数据源 (NSTableViewDataSource) 来根据需要提供所需的行和列。

可以通过提供表视图委托 (NSTableViewDelegate) 的子类来支持表列管理、键入以选择功能、行选择和编辑、自定义跟踪以及单个列和行的自定义视图来自定义表视图的行为。

创建表视图时,Apple 建议以下各项:

  • 允许用户通过单击列标题对表进行排序。
  • 创建作为名词或短名词短语的列标题,用于描述该列中显示的数据。

有关详细信息,请参阅 Apple OS X 人机界面指南内容视图部分。

在 Xcode 中创建和维护表视图

创建新的 Xamarin.Mac Cocoa 应用程序时,默认情况下会获得标准空白窗口。 此窗口在项目中自动包含的 .storyboard 文件中定义。 若要编辑窗口设计,请在“解决方案资源管理器”中双击 Main.storyboard 文件:

选择主情节提要

这将在 Xcode 的 Interface Builder 中打开窗口设计:

在 Xcode 中编辑 UI

在“库检查器”的搜索框中键入 table,以便更轻松地查找表视图控件:

从库中选择表视图

将表视图拖到“界面编辑器”的视图控制器上,使其填充视图控制器的内容区域,并将其设置为在“约束编辑器”中的窗口收缩和增长的位置:

编辑约束

选择“接口层次结构”中的表视图,“属性检查器”中提供以下属性:

屏幕截图显示属性检查器中可用的属性。

  • 内容模式 - 允许使用视图 (NSView) 或单元格 (NSCell) 在行和列中显示数据。 从 macOS 10.7 开始,应使用视图。
  • 浮点数组行 - 如果 true,表视图将绘制分组单元格,就像它们浮动一样。
  • - 定义显示的列数。
  • 标头 - 如果 true,列将具有标头。
  • 重新排序 - 如果 true,用户将能够拖动对表中的列进行重新排序。
  • 调整大小 - 如果 true,用户将能够拖动列标题以调整列的大小。
  • 列大小调整 - 控制表自动调整列大小的方式。
  • 突出显示 - 控制选定单元格时表格使用的突出显示类型。
  • 交替行 - 如果 true,相邻的行将具有不同的背景色。
  • 水平网格 - 选择水平单元格之间绘制的边框类型。
  • 垂直网格 - 选择垂直单元格之间绘制的边框类型。
  • 网格颜色 - 设置单元格边框颜色。
  • 背景 - 设置单元格背景色。
  • 选择 - 允许你控制用户可以如何选择表中的单元格,如下所示:
    • 多个 - 如果 true,用户可以选择多个行和列。
    • - 如果 true,用户可以选择列。
    • 类型选择 - 如果 true,用户可以键入字符以选择行。
    • - 如果 true,用户不需要选择行或列,表格允许完全不选择。
  • 自动保存 - 表格式的名称会自动保存。
  • 列信息 - 如果 true,将自动保存列的顺序和宽度。
  • 换行符 - 选择单元格如何处理换行符。
  • 截断最后一个可见行 - 如果 true,则单元格将被截断,因为数据无法在其范围内存储。

重要

除非要维护旧版 Xamarin.Mac 应用程序,否则应使用基于 NSView 的表视图,而不是基于 NSCell 的表视图。 NSCell 被视为旧版,今后可能不受支持。

在“接口层次结构”中选择一个表列,“属性检查器”中提供了以下属性:

屏幕截图显示属性检查器中表列可用的属性。

  • 标题 - 设置列的标题。
  • 对齐 - 设置单元格中文本的对齐方式。
  • 标题字体 - 选择单元格标题文本的字体。
  • 排序键 - 用于对列中的数据进行排序的键。 如果用户无法对此列进行排序,请留空。
  • 选择器 - 用于执行排序的操作。 如果用户无法对此列进行排序,请留空。
  • 顺序 - 列数据的排序顺序。
  • 调整大小 - 选择列的大小调整类型。
  • 可编辑 - 如果 true,用户可以编辑基于单元格的表中的单元格。
  • 隐藏 - 如果 true,列处于隐藏状态。

还可以通过向左或向右拖动列的手柄(垂直居中)调整列的大小。

让我们选择表视图中的每一列,并为第一列提供 Product标题 ,第二列则为 Details

在“接口层次结构”中选择表单元格视图 (NSTableViewCell) ,“属性检查器”中提供以下属性:

屏幕截图显示属性检查器中表单元格视图可用的属性。

这些是标准视图的所有属性。 还可以在此处选择调整此列的行大小。

在“接口层次结构”中选择表视图单元(默认情况下,这是 NSTextField),“属性检查器”中提供以下属性:

屏幕截图显示属性检查器中表视图单元格可用的属性。

你将拥有要在此处设置的标准文本字段的所有属性。 默认情况下,标准文本字段用于显示列中单元格的数据。

在“接口层次结构”中选择表单元格视图 (NSTableFieldCell) ,“属性检查器”中提供以下属性:

屏幕截图显示属性检查器中可用于其他表视图单元格的属性。

此处最重要的设置包括:

  • 布局 - 选择此列中的单元格布局方式。
  • 使用单行模式 - 如果 true,单元格限制为单行。
  • 第一个运行时布局宽度 - 如果 true,单元格在首次运行应用程序时将首选为其设置的宽度(手动或自动)。
  • 操作 - 控制何时为单元格发送编辑操作
  • 行为 - 定义单元格是否可选择或可编辑。
  • 格式文本 - 如果 true,单元格可以显示带格式的文本和样式文本。
  • 撤消 - 如果 true,单元格承担撤消行为的责任。

选择“接口层次结构”中表列底部的表单元格视图 (NSTableFieldCell):

选择表单元格视图

这样,您可以编辑用作为给定列创建的所有单元格的基模式的表格单元格视图。

添加操作和出口

与任何其他 Cocoa UI 控件一样,我们需要使用操作出口(基于所需的功能)向 C# 代码公开表视图及其列和单元格。

对于要公开的任何表视图元素,此过程是相同的:

  1. 切换到“助理编辑器”并确保已选择 ViewController.h 文件:

    助手编辑器

  2. 从“接口层次结构”中选择表视图,按住 Control 键单击并拖动到 ViewController.h 文件。

  3. 为名为 ProductTable 的表视图创建出口

    屏幕截图显示为名为 ProductTable 的表视图创建的输出口连接。

  4. 为表列创建出口,也称为 ProductColumnDetailsColumn

    屏幕截图显示为其他表视图创建的输出口连接。

  5. 保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。

接下来,我们将编写代码,以便在运行应用程序时显示表的一些数据。

填充表视图

在 Interface Builder 设计的表视图并通过出口公开后,接下来需要创建 C# 代码来填充它。

首先,让我们创建一个新的 Product 类来保存各个行的信息。 在“解决方案资源管理器”中,右键单击项目并选择“添加”>“新建文件...”。选择“常规>“空类”,输入 Product 作为“名称”,然后单击“新建”按钮:

创建空类

使 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 子类,以便在请求表时为表提供数据。 在“解决方案资源管理器”中,右键单击项目并选择“添加”>“新建文件...”。选择“常规”>“空类”,输入 ProductTableDataSource 作为“名称”,然后单击“新建”按钮。

编辑 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
  }
}

此类包含表视图项的存储,并重写 GetRowCount 以返回表中的行数。

最后,我们需要创建一个 NSTableDelegate 的子类来提供表的行为。 在“解决方案资源管理器”中,右键单击项目并选择“添加”>“新建文件...”。选择“常规>“空类”,输入 ProductTableDelegate 作为“名称”,然后单击“新建”按钮。

编辑 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 方法负责返回视图(数据)以显示给定列和行的单元格。 如果可能,将重复使用现有视图来显示单元格(如果不是新视图)。

若要填充表,让我们编辑 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);
}

如果运行应用程序,将显示以下内容:

屏幕截图显示一个名为“产品表”的窗口,其中包含三个条目。

按列排序

让我们允许用户通过单击列标题对表中的数据进行排序。 首先,双击 Main.storyboard 该文件将其打开,以便在 Interface Builder 中编辑。 选择 Product 列,为“排序键”输入 Title,为“选择器”compare:输入 ,并为“顺序”选择 Ascending

屏幕截图显示 Interface Builder,你可以在其中设置“产品”列的排序键。

选择 Details 列,为“排序键”输入 Description,为“选择器”compare:输入 ,并为“顺序”选择 Ascending

屏幕截图显示 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 中设置的值以及该列的排序顺序。

如果我们运行应用程序并单击列标题,则行将按该列进行排序:

示例应用运行

行选择

如果要允许用户选择单行,请双击 Main.storyboard 文件以在 Interface Builder 中编辑。 在“接口层次结构”中选择表视图,然后取消选中“属性检查器”中的“多个”复选框:

屏幕截图显示 Interface Builder,你可以在其中的属性检查器中选择“多个”。

保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。

接下来,编辑 ProductTableDelegate.cs 文件并添加以下方法:

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

这将允许用户选择表视图中的任何单行。 如果不希望用户能够选择任何行,则返回 false,表示不希望用户能够选择或 false 每一行的 ShouldSelectRow

表视图 (NSTableView) 包含以下用于处理行选择的方法:

  • DeselectRow(nint) - 取消选择表中的给定行。
  • SelectRow(nint,bool) - 选择给定行。 传递第二个参数的 false,以便一次仅选择一行。
  • SelectedRow - 返回表中所选的当前行。
  • IsRowSelected(nint) - 如果选择给定行,则返回 true

多行选择

如果希望允许用户选择多行,请双击 Main.storyboard 文件将其打开,以便在 Interface Builder 中编辑。 选择“接口层次结构”中的表视图,并在“属性检查器”中选中“多个”复选框:

屏幕截图显示 Interface Builder,你可在其中选择“多个”以允许多行选择。

保存更改并返回到 Visual Studio for Mac 以与 Xcode 同步。

接下来,编辑 ProductTableDelegate.cs 文件并添加以下方法:

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

这将允许用户选择表视图中的任何单行。 如果不希望用户能够选择任何行,则返回 false,表示不希望用户能够选择或 false 每一行的 ShouldSelectRow

表视图 (NSTableView) 包含以下用于处理行选择的方法:

  • DeselectAll(NSObject) - 取消选择表中的所有行。 对执行选择的对象中发送的第一个参数使用 this
  • DeselectRow(nint) - 取消选择表中的给定行。
  • SelectAll(NSobject) - 选择表中的所有行。 对执行选择的对象中发送的第一个参数使用 this
  • SelectRow(nint,bool) - 选择给定行。 传递第二个参数的 false 清除选定内容,只选择一行,传递 true 以扩展所选内容并包含此行。
  • SelectRows(NSIndexSet,bool) - 选择给定的行集。 传递第二个参数的 false 清除选定内容并仅选择这些行,传递 true 以扩展所选内容并包括这些行。
  • SelectedRow - 返回表中所选的当前行。
  • SelectedRows - 返回包含所选行索引的 NSIndexSet
  • SelectedRowCount - 返回所选行数。
  • IsRowSelected(nint) - 如果选择给定行,则返回 true

键入以选择行

如果希望允许用户键入选中“表视图”的字符,并选择具有该字符的第一行,请双击 Main.storyboard 文件将其打开,以便在 Interface Builder 中编辑。 选择“接口层次结构”中的表视图,并在“属性检查器”中选中“类型选择”复选框:

设置选择类型

保存更改并返回到 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

如果运行应用程序并键入字符,则选择一行:

屏幕截图显示运行应用程序的结果。

重新排序列

如果要允许用户在表视图中拖动重新排序列,请双击 Main.storyboard 文件将其打开,以便在 Interface Builder 中编辑。 在“接口层次结构”中选择表视图,并在“属性检查器”中选中“重新排序”复选框:

屏幕截图显示 Interface Builder,你可在其中的属性检查器中选择“重新排序”。

如果我们为“自动保存”属性提供值并检查“列信息”字段,则对表布局所做的任何更改将自动保存,并在下次运行应用程序时还原。

保存更改并返回到 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;
}

现在,如果运行应用程序,用户可以编辑表视图中的单元格:

编辑单元格的示例

在表视图中使用图像

若要在 NSTableView 中包含图像作为单元格的一部分,需要更改表视图的 NSTableViewDelegate'sGetViewForItem 方法返回数据的方式,以使用 NSTableCellView 而不是典型的 NSTextField。 例如:

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;
}

有关详细信息,请参阅“使用图像”文档的“使用表视图”部分。

向行添加“删除”按钮

根据应用的要求,有时可能需要为表中的每一行提供一个操作按钮。 在此示例中,让我们展开上面创建的表视图示例,在每一行中包含“删除”按钮。

首先,在 Xcode 的 Interface Builder 中编辑 Main.storyboard ,选择表视图并将列数增加到三 (3)。 接下来,将新列的“标题”更改为 Action

编辑列名

保存对情节提要所做的更改,并返回到 Visual Studio for Mac 以同步更改。

接下来,编辑 ViewController.cs 文件并添加以下公共方法:

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

在同一文件中,修改在 ViewDidLoad 方法内创建新的表视图委托,如下所示:

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

现在,编辑 ProductTableDelegate.cs 文件以包含与视图控制器的专用连接,并在创建新委托实例时将控制器作为参数:

#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 方法中完成的所有文本视图配置,并将其置于单个可调用的位置(因为表的最后一列不包含文本视图,而是按钮)。

最后,编辑 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 操作,则根据列的名称执行。 对于前两列(ProductDetails),将调用新的 ConfigureTextField 方法。

对于“操作”列,将创建一个新的 NSButton,并将其作为子视图添加到单元格中:

// 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);

按钮的 Tag 属性用于保存当前正在处理的行数。 当用户请求在按钮的 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 ();
    }
  });
};

在事件处理程序的开头,我们获取位于给定表行上的按钮和产品。 然后向用户显示一个警报,确认删除行。 如果用户选择删除该行,则会从数据源中删除给定行,并重新加载表:

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

最后,如果要重复使用表视图单元格而不是创建新单元格,则以下代码将基于正在处理的列对其进行配置:

// 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;
}

对于“操作”列,将扫描所有子视图,直到找到 NSButton,然后将它的 Tag 属性更新为指向当前行。

在这些更改到位后,当应用运行时,每一行都会有一个“删除”按钮:

带有删除按钮的表视图

当用户单击“删除”按钮时,将显示一个警报,要求他们删除给定的行:

删除行警报

如果用户选择删除,将删除该行,并将重绘表:

删除行后的表

数据绑定表视图

通过在 Xamarin.Mac 应用程序中使用键值编码和数据绑定技术,可以大大减少必须编写和维护的代码量,以填充和使用 UI 元素。 还可以从前端用户界面 (Model-View-Controller)进一步分离支持数据(数据模型),从而更轻松地维护、更灵活的应用程序设计。

键值编码 (KVC) 是间接访问对象属性的机制,使用键(特殊格式的字符串)来标识属性,而不是通过实例变量或访问器方法 (get/set) 访问它们。 通过在 Xamarin.Mac 应用程序中实现符合键值编码的访问器,可以访问其他 macOS 功能,例如键值观察 (KVO)、数据绑定、核心数据、Cocoa 绑定和可脚本性。

有关详细信息,请参阅“数据绑定和键值编码”文档的“表视图数据绑定”部分。

总结

本文详细介绍了如何使用 Xamarin.Mac 应用程序中的表视图。 我们了解了表视图的不同类型和用法,如何在 Xcode 的 Interface Builder 中创建和维护表视图,以及如何在 C# 代码中使用表视图。