Xamarin.iOS 中的事件、协议和委托

Xamarin.iOS 使用控件公开大多数用户交互事件。 Xamarin.iOS 应用程序使用这些事件的方式与传统 .NET 应用程序相同。 例如,Xamarin.iOS UIButton 类具有名为 TouchUpInside 的事件,在使用此事件时就如同该类和该事件就在 .NET 应用中一样。

除了采用 .NET 风格外,Xamarin.iOS 还公开了一个可用于更复杂交互和数据绑定的模型。 此方法使用 Apple 所称的委托和协议。 委托在概念上与 C# 中的委托类似,但不定义和调用单个方法,Objective-C 中的委托是符合协议的整个类。 协议类似于 C# 中的接口,只是其方法可以是可选的。 例如,若要使用数据填充 UITableView,需要创建一个委托类,该委托类实现 UITableViewDataSource 协议中定义的方法,UITableView 将调用些方法来填充自身。

本文将介绍所有这些主题,为处理 Xamarin.iOS 中的回调方案打下坚实基础,这些方案包括:

  • 事件 - 将 .NET 事件与 UIKit 控件配合使用。
  • 协议 - 了解协议是什么以及它们的使用方式,并创建一个提供地图注释数据的示例。
  • 委托 – 通过将地图示例扩展为进一步处理包含注释的用户交互,了解 Objective-C 委托,然后了解强委托与弱委托之间的差异以及何时使用这些委托。

为了说明协议和委托,我们将生成一个简单的地图应用程序,该应用程序将注释添加到地图,如下所示:

向地图添加注释的简单地图应用程序的示例添加到地图的注释示例

在生成此应用之前,先来了解 UIKit 下的 .NET 事件。

使用 UIKit 的 .NET 事件

Xamarin.iOS 在 UIKit 控件上公开 .NET 事件。 例如,UIButton 有一个 TouchUpInside 事件,用户可以像在 .NET 中一样处理该事件,如以下代码所示,该代码使用 C# lambda 表达式:

aButton.TouchUpInside += (o,s) => {
    Console.WriteLine("button touched");
};

也可以使用 C# 2.0 样式的匿名方法来实现此目的,如下所示:

aButton.TouchUpInside += delegate {
    Console.WriteLine ("button touched");
};

上述代码通过 UIViewController 的 ViewDidLoad 方法联接。 aButton 变量引用一个按钮,可在 Xcode Interface Builder 中或通过代码添加该按钮。

Xamarin.iOS 还支持目标操作样式,即将代码与通过控件发生的交互关联。

有关 iOS 目标操作模式的更多详细信息,请参阅 Apple 的 iOS 开发人员库中 iOS 核心应用能力的“目标操作”部分。

有关详细信息,请参阅用 Xcode 设计用户界面

事件

如果希望从 UIControl 截获事件,有多个选项可供选择:从使用 C# lambda 和委托函数到使用低级别 Objective-C API 等各种选项。

以下部分显示如何捕获按钮上的 TouchDown 事件,具体取决于所需的控制程度。

C# 样式

使用委托语法:

UIButton button = MakeTheButton ();
button.TouchDown += delegate {
    Console.WriteLine ("Touched");
};

如果使用 lambda:

button.TouchDown += () => {
   Console.WriteLine ("Touched");
};

如果希望多个按钮使用同一处理程序以共享相同代码:

void handler (object sender, EventArgs args)
{
   if (sender == button1)
      Console.WriteLine ("button1");
   else
      Console.WriteLine ("some other button");
}

button1.TouchDown += handler;
button2.TouchDown += handler;

监视多个种类的事件

UIControlEvent 标志的 C# 事件具有对各标志的一对一映射。 如果希望用相同的代码段处理两个或多个事件,可使用 UIControl.AddTarget 方法:

button.AddTarget (handler, UIControlEvent.TouchDown | UIControlEvent.TouchCancel);

使用 lambda 语法:

button.AddTarget ((sender, event)=> Console.WriteLine ("An event happened"), UIControlEvent.TouchDown | UIControlEvent.TouchCancel);

如果需要使用 Objective-C 的低级别功能,例如连接到特定对象实例并调用特定选择器:

[Export ("MySelector")]
void MyObjectiveCHandler ()
{
    Console.WriteLine ("Hello!");
}

// In some other place:

button.AddTarget (this, new Selector ("MySelector"), UIControlEvent.TouchDown);

请注意,如果在继承的基类中实现实例方法,该方法必须是公共方法。

协议

协议是一种提供方法声明列表的 Objective-C 语言功能。 它与 C# 中的接口作用类似,主要区别在于协议可以具有可选方法。 如果采用协议的类不实现可选方法,则不会调用这些方法。 此外,Objective-C 中的单个类可以实现多个协议,就像一个 C# 类可以实现多个接口一样。

Apple 在整个 iOS 中使用协议来定义供类采用的协定,同时将从调用方实现类的操作抽象化,因此与 C# 接口的运行方式一样。 协议既可用于非委托方案(如下一个 MKAnnotation 示例中所示),也可用于委托方案(如本文档后面的“委托”部分中所示)。

使用 Xamarin.ios 的协议

现在来看一个通过 Xamarin.iOS 使用 Objective-C 协议的示例。 在此示例中,我们将使用 MKAnnotation 协议,该协议是 MapKit 框架的一部分。 MKAnnotation 是一种协议,使采用该协议的任何对象能够提供可添加到地图的注释的相关信息。 例如,实现 MKAnnotation 的对象提供注释的位置及其关联的标题。

通过这种方式,MKAnnotation 协议可用于提供注释附带的相关数据。 注释自身的实际视图生成自采用 MKAnnotation 协议的对象中的数据。 例如,用户点击注释时显示的标注文本(如以下屏幕截图所示)来自实现协议的类中的 Title 属性:

用户点击注释时的标注示例文本

如下一部分协议详探所述,Xamarin.iOS 将协议绑定到抽象类。 对于 MKAnnotation 协议,绑定的 C# 类命名为 MKAnnotation 以模拟协议的名称,它是 CocoaTouch 的根基类 NSObject 的子类。 协议要求为坐标实现 getter 和 setter;但标题和副标题是可选的。 因此,在 MKAnnotation 类中,Coordinate 属性是抽象属性,需要进行实现,而 TitleSubtitle 属性标记为虚拟属性,为可选属性,如下所示:

[Register ("MKAnnotation"), Model ]
public abstract class MKAnnotation : NSObject
{
    public abstract CLLocationCoordinate2D Coordinate
    {
        [Export ("coordinate")]
        get;
        [Export ("setCoordinate:")]
        set;
    }

    public virtual string Title
    {
        [Export ("title")]
        get
        {
            throw new ModelNotImplementedException ();
        }
    }

    public virtual string Subtitle
    {
        [Export ("subtitle")]
        get
        {
            throw new ModelNotImplementedException ();
        }
    }
...
}

只要至少实现了 Coordinate 属性,任何类都可以通过从 MKAnnotation 派生来提供注释数据。 例如,下面是一个示例类,该类采用构造函数中的坐标,并返回标题的字符串:

/// <summary>
/// Annotation class that subclasses MKAnnotation abstract class
/// MKAnnotation is bound by Xamarin.iOS to the MKAnnotation protocol
/// </summary>
public class SampleMapAnnotation : MKAnnotation
{
    string title;

    public SampleMapAnnotation (CLLocationCoordinate2D coordinate)
    {
        Coordinate = coordinate;
        title = "Sample";
    }

    public override CLLocationCoordinate2D Coordinate { get; set; }

    public override string Title {
        get {
            return title;
        }
    }
}

通过绑定到的协议,具有子类 MKAnnotation 的任何类都可以提供地图在创建注释视图时要使用的相关数据。 要向地图添加注释,只需调用 MKMapView 实例的 AddAnnotation 方法,如以下代码所示:

//an arbitrary coordinate used for demonstration here
var sampleCoordinate =
    new CLLocationCoordinate2D (42.3467512, -71.0969456); // Boston

//create an annotation and add it to the map
map.AddAnnotation (new SampleMapAnnotation (sampleCoordinate));

此处的映射变量是 MKMapView 的实例,它是表示映射本身的类。 MKMapView 使用派生自 SampleMapAnnotation 实例的 Coordinate 数据在地图上定位注释视图。

MKAnnotation 协议针对实现它的对象提供一组已知功能,在这种情况下,无需使用者(本例中的地图)了解实现详细信息。 这简化了向地图添加各种可能的注释的操作。

协议详探

由于 C# 接口不支持可选方法,因此 Xamarin.iOS 将协议映射到抽象类。 因此,在 Objective-C 中采用协议是通过派生自绑定到协议并实现所需方法的抽象类在 Xamarin.iOS 中实现的。 这些方法将作为类中的抽象方法公开。 协议中的可选方法将绑定到 C# 类的虚拟方法。

例如,下面是在 Xamarin.iOS 中绑定的 UITableViewDataSource 协议的一部分:

public abstract class UITableViewDataSource : NSObject
{
    [Export ("tableView:cellForRowAtIndexPath:")]
    public abstract UITableViewCell GetCell (UITableView tableView, NSIndexPath indexPath);
    [Export ("numberOfSectionsInTableView:")]
    public virtual int NumberOfSections (UITableView tableView){...}
...
}

注意,类是抽象的。 Xamarin.iOS 使类抽象,以支持协议中的可选/必需方法。 但是,与 Objective-C 协议(或 C# 接口)不同,C# 类不支持多个继承。 这会影响使用协议的 C# 代码的设计,通常会导致嵌套类。 有关此问题的详细信息,请参阅本文档后面的“委托”部分。

GetCell(…) 是一个抽象方法,绑定到Objective-C选择器tableView:cellForRowAtIndexPath:,这是 UITableViewDataSource 协议必需的一个方法。 选择器是指代方法名称的 Objective-C 术语。 若要根据需要强制实施该方法,Xamarin.iOS 会将其声明为抽象方法。 另一个方法 NumberOfSections(…) 绑定到 numberOfSectionsInTableview:。 此方法在协议中是可选的,因此 Xamarin.iOS 将其声明为虚拟方法,使其成为用户可选择是否在 C# 中将其替代的可选方法。

Xamarin.iOS 负责处理所有 iOS 绑定。 但是,如果需要手动从 Objective-C 绑定协议,可以通过使用 ExportAttribute 修饰某个类来执行此操作。 这也是 Xamarin.iOS 自身使用的方法。

有关如何在 Xamarin.iOS 中绑定 Objective-C 类型的详细信息,请参阅绑定 Objective-C 类型一文。

但我们还没完成对协议的探索。 它们还在 iOS 中充当 Objective-C 委托的基础,下一部分将以此为主题进行介绍。

代理

iOS 使用 Objective-C 委托来实现委派模式,其中,一个对象会将工作传递到另一个对象。 完成该工作的对象是第一个对象的委托。 对象通过在发生特定事件后向委托发送消息来指示其工作。 像这样在 Objective-C 中发送消息在功能上等效于在 C# 中调用方法。 委托会实现方法以响应这些调用,以便为应用程序提供功能。

通过委托可扩展类的行为,而无需创建子类。 iOS 中的应用程序通常会在一个类在发生重要操作后回调另一个类时使用委托。 例如,当用户点击地图上的注释时,MKMapView 类会回调其委托,使委托类的作者有机会在应用程序中做出响应。 可以通过本文后面部分的“将委托与 Xamarin.iOS 配合使用的示例”了解此类型的委托的用法。

此时,你可能想知道类如何确定要对其委托调用什么方法。 这是另一个要用到协议的地方。 通常,可用于委托的方法来自其采用的协议。

如何将协议与委托一起使用

我们之前了解了如何使用协议来支持向地图添加注释的操作。 协议还用于为类提供一组已知方法,以便类在发生某些事件后调用,例如在用户点击地图上的注释或选择表中的单元格之后。 实现这些方法的类称为调用这些方法的类的委托。

支持委托的类通过公开 Delegate 属性(将向其分配实现委托的类)来实现此操作。 为委托实现的方法取决于该特定委托采用的协议。 对于 UITableView 方法,实现 UITableViewDelegate 协议,对于 UIAccelerometer 方法,则实现 UIAccelerometerDelegate,对于要公开委托的 iOS 中的任何其他类,以此类推。

前面示例中的 MKMapView 类也有一个名为 Delegate 的属性,并会在发生各种事件后调用该属性。 MKMapView 的 Delegate 类型为 MKMapViewDelegate。 稍后将在示例中使用它来响应选择的注释,但现在先来了解强委托和弱委托的区别。

强委托与弱委托

目前为止介绍的委托都是强委托,这意味着它们是强类型。 Xamarin.iOS 绑定附带 iOS 中每个委托协议的强类型类。 但是,iOS 也有弱委托的概念。 iOS 还允许用户自行将协议方法绑定到从 NSObject 派生的任何类中,从而用 ExportAttribute 装饰方法,然后提供适当的选择器,而不是为特定委托将绑定到 Objective-C 协议的类进行子类化。 采用此方法时,请将类的实例分配给 WeakDelegate 属性而不是 Delegate 属性。 弱委托使你可以灵活地将委托类向下放到不同的继承层次结构。 现在来看一个同时使用强委托和弱委托的 Xamarin.iOS 示例。

将委托与 Xamarin.iOS 配合使用的示例

若希望执行代码来响应示例中用户对注释的点击,可以将 MKMapViewDelegate 子类化并将实例分配给 MKMapViewDelegate 属性。 MKMapViewDelegate 协议仅包含可选方法。 因此,所有方法都是绑定到 Xamarin.iOS MKMapViewDelegate 类中此协议的虚拟方法。 当用户选择某个注释时,MKMapView 实例会将 mapView:didSelectAnnotationView: 消息发送到其委托。 要在 Xamarin.iOS 中处理此问题,需要重写 MKMapViewDelegate 子类中的 DidSelectAnnotationView (MKMapView mapView, MKAnnotationView annotationView) 方法,如下所示:

public class SampleMapDelegate : MKMapViewDelegate
{
    public override void DidSelectAnnotationView (
        MKMapView mapView, MKAnnotationView annotationView)
    {
        var sampleAnnotation =
            annotationView.Annotation as SampleMapAnnotation;

        if (sampleAnnotation != null) {

            //demo accessing the coordinate of the selected annotation to
            //zoom in on it
            mapView.Region = MKCoordinateRegion.FromDistance(
                sampleAnnotation.Coordinate, 500, 500);

            //demo accessing the title of the selected annotation
            Console.WriteLine ("{0} was tapped", sampleAnnotation.Title);
        }
    }
}

上面所示的 SampleMapDelegate 类作为包含 MKMapView 实例的控制器中的嵌套类实现。 在 Objective-C 中,控制器通常会直接在类中采用多个协议。 但是,由于协议绑定到 Xamarin.iOS 中的类,因此实现强类型委托的类通常作为嵌套类包含在内。

有了委托类实现,只需将控制器中委托的某实例进行实例化并将其分配给 MKMapViewDelegate 属性即可,如下所示:

public partial class Protocols_Delegates_EventsViewController : UIViewController
{
    SampleMapDelegate _mapDelegate;
    ...
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();

        //set the map's delegate
        _mapDelegate = new SampleMapDelegate ();
        map.Delegate = _mapDelegate;
        ...
    }
    class SampleMapDelegate : MKMapViewDelegate
    {
        ...
    }
}

若要使用弱委托来实现相同的目的,需要在派生自 NSObject 的任何类中自行绑定该方法,并将其分配给 MKMapViewWeakDelegate属性。 由于 UIViewController 类最终派生自 NSObject(与 CocoaTouch 中的每个 Objective-C 类一样),因此只需在控制器中直接实现绑定到 mapView:didSelectAnnotationView: 的方法,并将控制器分配给 MKMapViewWeakDelegate 即可,而无需使用额外的嵌套类。 下面的代码演示了此方法:

public partial class Protocols_Delegates_EventsViewController : UIViewController
{
    ...
    public override void ViewDidLoad ()
    {
        base.ViewDidLoad ();
        //assign the controller directly to the weak delegate
        map.WeakDelegate = this;
    }
    //bind to the Objective-C selector mapView:didSelectAnnotationView:
    [Export("mapView:didSelectAnnotationView:")]
    public void DidSelectAnnotationView (MKMapView mapView,
        MKAnnotationView annotationView)
    {
        ...
    }
}

运行此代码时,应用程序的行为与运行强类型委托版本时的行为完全相同。 此代码的优点是弱委托不需要创建使用强类型委托时创建的额外类。 但这以降低类型安全性为代价。 如果在传递给 ExportAttribute 的选择器中造成了错误,在运行时之前是无法发现该错误的。

事件和委托

委托用于 iOS 中的回调,与 .NET 使用事件类似。 若要使 iOS API 及其使用 Objective-C 委托的方式看起来更像 .NET,Xamarin.iOS 会在 iOS 中许多使用委托的位置公开 .NET 事件。

例如,其中 MKMapViewDelegate 对所选注释作出响应的早期实现也可以通过使用 .NET 事件在 Xamarin.iOS 中实现。 在这种情况下,事件将在 MKMapView 中定义,并称为 DidSelectAnnotationView。 它将具有 MKMapViewAnnotationEventsArgs 类型的 EventArgs 子类。 MKMapViewAnnotationEventsArgsView 属性提供对注释视图的引用,可以从该视图继续执行之前的实现,如下所示:

map.DidSelectAnnotationView += (s,e) => {
    var sampleAnnotation = e.View.Annotation as SampleMapAnnotation;
    if (sampleAnnotation != null) {
        //demo accessing the coordinate of the selected annotation to
        //zoom in on it
        mapView.Region = MKCoordinateRegion.FromDistance (
            sampleAnnotation.Coordinate, 500, 500);

        //demo accessing the title of the selected annotation
        Console.WriteLine ("{0} was tapped", sampleAnnotation.Title);
    }
};

总结

本文介绍了如何在 Xamarin.iOS 中使用事件、协议和委托。 我们了解了 Xamarin.iOS 如何公开控件的普通 .NET 样式事件。 接着了解了 Objective-C 协议,包括其与 C# 接口的区别,以及 Xamarin.iOS 如何使用它们。 最后,我们从 Xamarin.iOS 的角度探究了 Objective-C 委托。 我们了解了 Xamarin.iOS 如何同时支持强类型委托和弱类型委托,以及如何将 .NET 事件绑定到委托方法。