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 和接口生成器简介 以及 输出口和操作 部分,因为它涵盖了我们将在本文中使用的关键概念和技术。

你可能还想要查看 Xamarin.Mac Internals 文档的向 Objective-CC# 类/方法公开部分,其中介绍了Register用于将 C# 类Objective-C连接到对象和 UI 元素的 和 Export 命令。

表视图简介

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

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

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

创建表视图时,Apple 会提出以下建议:

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

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

在 Xcode 中创建和维护表视图

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

选择main情节提要

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

在 Xcode 中编辑 UI

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

从库中选择表视图

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

编辑约束

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

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

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

重要

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

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

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

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

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

让我们在表视图中选择每个列,并为第一列指定 标题Product 为第二列 Details指定标题。

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

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

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

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

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

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

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

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

此处最重要的设置包括:

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

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

选择表单元格视图

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

添加操作和出口

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

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

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

    助理编辑器

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

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

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

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

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

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

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

填充表视图

由于表视图在接口生成器中设计并通过 输出口公开,接下来需要创建 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顺序”:

屏幕截图显示了接口生成器,你可以在其中设置 Product 列的排序键。

选择列Details,输入 Description (对于“排序键”)和“选择器”,compare:然后选择Ascending顺序”:

屏幕截图显示了接口生成器,你可以在其中设置“详细信息”列的排序键。

保存更改并返回到 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 中设置的 Key 值以及该列的排序顺序。

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

示例应用运行

行选择

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

显示接口生成器的屏幕截图,可在其中选择属性检查器中的“多个”。

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

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

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

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

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

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

多行选择

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

屏幕截图显示了接口生成器,你可以在其中选择“多个”以允许多行选择。

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

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

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

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

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

  • DeselectAll(NSObject) - 取消选择表中的所有行。 使用 this 在对象中发送执行选择的第一个参数。
  • DeselectRow(nint) - 取消选择表中的给定行。
  • SelectAll(NSobject) - 选择表中的所有行。 使用 this 在对象中发送执行选择的第一个参数。
  • SelectRow(nint,bool) - 选择给定行。 第二个参数的 Pass false 清除所选内容并仅选择一行,传递 true 以扩展所选内容并包含此行。
  • SelectRows(NSIndexSet,bool) - 选择给定的行集。 第二个参数的 Pass 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 的 ,并返回中具有该字符串的第一个 ProductTitle行。

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

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

对列重新排序

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

屏幕截图显示了接口生成器,可在其中选择属性检查器中的“重新序列化”。

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

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

现在,让我们编辑 ProductTableDelegate.cs 文件并添加以下方法:

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

方法 ShouldReorder 应返回 true 它希望允许拖动到 中重新排序 newColumnIndex的任何列,否则返回 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,需要更改表视图GetViewForItemNSTableViewDelegate's的 方法返回数据的方式,以使用 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 的接口生成器中编辑 Main.storyboard ,选择表视图并将列数增加到 3 (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 完成的所有文本视图配置,并将其置于单个可调用位置 (,因为表的最后一列不包含文本视图,而是包含 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 正在创建新,则根据列的名称执行操作。 对于 product 和Details) (的前两列,将调用新 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);

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

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

// 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 应用程序中使用Key-Value编码和数据绑定技术,可以大大减少填充和使用 UI 元素而必须编写和维护的代码量。 还可以进一步将后备数据 (数据模型) 与前端用户界面 (模型-视图-控制器) 分离,从而简化维护、更灵活的应用程序设计。

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

有关详细信息,请参阅数据绑定和Key-Value编码文档的表视图数据绑定部分。

总结

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