Eventi, protocolli e delegati in Xamarin.iOS

Xamarin.iOS usa i controlli per esporre gli eventi per la maggior parte delle interazioni utente. Le applicazioni Xamarin.iOS usano questi eventi nello stesso modo delle applicazioni .NET tradizionali. Ad esempio, la classe UIButton di Xamarin.iOS ha un evento denominato TouchUpInside e usa questo evento come se questa classe e l'evento fossero in un'app .NET.

Oltre a questo approccio .NET, Xamarin.iOS espone un altro modello che può essere usato per interazioni e data binding più complessi. Questa metodologia usa ciò che Apple chiama delegati e protocolli. I delegati sono simili nel concetto ai delegati in C#, ma invece di definire e chiamare un singolo metodo, un delegato in Objective-C è un'intera classe conforme a un protocollo. Un protocollo è simile a un'interfaccia in C#, ad eccezione del fatto che i relativi metodi possono essere facoltativi. Ad esempio, per popolare un oggetto UITableView con i dati, è necessario creare una classe delegato che implementi i metodi definiti nel protocollo UITableViewDataSource che UITableView chiamerebbe per popolare se stesso.

Questo articolo illustra tutti questi argomenti, offrendo una solida base per la gestione degli scenari di callback in Xamarin.iOS, tra cui:

  • Eventi : uso di eventi .NET con i controlli UIKit.
  • Protocolli: apprendimento dei protocolli e del modo in cui vengono usati e creazione di un esempio che fornisce dati per un'annotazione della mappa.
  • Delegati: informazioni sui Objective-C delegati estendendo l'esempio di mappa per gestire l'interazione dell'utente che include un'annotazione, quindi imparando la differenza tra delegati forti e deboli e quando usarli.

Per illustrare protocolli e delegati, verrà creata una semplice applicazione mappa che aggiunge un'annotazione a una mappa, come illustrato di seguito:

An example of a simple map application that adds an annotation to a mapAn example annotation added to a map

Prima di affrontare questa app, è possibile iniziare esaminando gli eventi .NET in UIKit.

Eventi .NET con UIKit

Xamarin.iOS espone gli eventi .NET nei controlli UIKit. Ad esempio, UIButton ha un evento TouchUpInside, gestito normalmente in .NET, come illustrato nel codice seguente che usa un'espressione lambda C#:

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

È anche possibile implementare questa operazione con un metodo anonimo in stile C# 2.0 simile al seguente:

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

Il codice precedente viene cablato nel ViewDidLoad metodo di UIViewController. La aButton variabile fa riferimento a un pulsante, che è possibile aggiungere in Xcode Interface Builder o con codice.

Xamarin.iOS supporta anche lo stile di azione di destinazione per connettere il codice a un'interazione che si verifica con un controllo .

Per altre informazioni sul modello di azione di destinazione iOS, vedere la sezione Target-Action (Azione di destinazione) di Core Application Competencies for iOS (Competenze dell'applicazione di base per iOS) nella libreria per sviluppatori iOS di Apple.

Per altre informazioni, vedere Progettazione di interfacce utente con Xcode.

evento

Per intercettare gli eventi da UIControl, sono disponibili diverse opzioni: dall'uso delle espressioni lambda C# e delle funzioni delegate all'uso delle API di basso livello Objective-C .

La sezione seguente illustra come acquisire l'evento TouchDown su un pulsante, a seconda della quantità di controllo necessaria.

Stile C#

Uso della sintassi del delegato:

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

Se invece si vogliono espressioni lambda:

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

Se si desidera che più pulsanti usino lo stesso gestore per condividere lo stesso codice:

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

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

Monitoraggio di più tipi di evento

Gli eventi C# per i flag UIControlEvent hanno un mapping uno-a-uno ai singoli flag. Quando si vuole che la stessa parte di codice gestisca due o più eventi, usare il UIControl.AddTarget metodo :

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

Uso della sintassi lambda:

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

Se è necessario usare le funzionalità di basso livello di , ad esempio l'associazione a una particolare istanza di Objective-Coggetto e la chiamata di un selettore specifico:

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

// In some other place:

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

Si noti che, se si implementa il metodo di istanza in una classe di base ereditata, deve essere un metodo pubblico.

Protocolli

Un protocollo è una Objective-C funzionalità del linguaggio che fornisce un elenco di dichiarazioni di metodo. Serve uno scopo simile a un'interfaccia in C#, la differenza principale è che un protocollo può avere metodi facoltativi. I metodi facoltativi non vengono chiamati se la classe che adotta un protocollo non li implementa. Inoltre, una singola classe in Objective-C può implementare più protocolli, proprio come una classe C# può implementare più interfacce.

Apple usa protocolli in tutti iOS per definire i contratti per le classi da adottare, astraendo al tempo stesso la classe di implementazione dal chiamante, in modo che funzioni esattamente come un'interfaccia C#. I protocolli vengono usati sia in scenari non delegati (ad esempio con l'esempio MKAnnotation illustrato di seguito) che con delegati (come illustrato più avanti in questo documento, nella sezione Delegati).

Protocolli con Xamarin.ios

Di seguito viene illustrato un esempio relativo all'uso di un Objective-C protocollo di Xamarin.iOS. Per questo esempio si userà il MKAnnotation protocollo , che fa parte del MapKit framework. MKAnnotation è un protocollo che consente a qualsiasi oggetto che lo adotta di fornire informazioni su un'annotazione che può essere aggiunta a una mappa. Ad esempio, un oggetto che implementa MKAnnotation fornisce la posizione dell'annotazione e il titolo associato.

In questo modo, il MKAnnotation protocollo viene usato per fornire dati pertinenti che accompagnano un'annotazione. La visualizzazione effettiva per l'annotazione stessa viene compilata dai dati nell'oggetto che adotta il MKAnnotation protocollo. Ad esempio, il testo per il callout visualizzato quando l'utente tocca l'annotazione (come illustrato nello screenshot seguente) deriva dalla Title proprietà nella classe che implementa il protocollo:

Example text for the callout when the user taps on the annotation

Come descritto nella sezione successiva, Protocols Deep Dive, Xamarin.iOS associa i protocolli alle classi astratte. Per il MKAnnotation protocollo, la classe C# associata viene denominata MKAnnotation per simulare il nome del protocollo ed è una sottoclasse di NSObject, la classe di base radice per CocoaTouch. Il protocollo richiede l'implementazione di un getter e un setter per la coordinata; Tuttavia, un titolo e un sottotitolo sono facoltativi. Pertanto, nella classe la Coordinate proprietà è astratta, che richiede l'implementazione e le Title proprietà e Subtitle sono contrassegnate come virtuali, rendendole facoltative, come illustrato di MKAnnotation seguito:

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

Qualsiasi classe può fornire dati di annotazione semplicemente derivando da MKAnnotation, purché venga implementata almeno la Coordinate proprietà . Ad esempio, ecco una classe di esempio che accetta la coordinata nel costruttore e restituisce una stringa per il titolo:

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

Tramite il protocollo a cui è associato, qualsiasi classe a cui le sottoclassi MKAnnotation possono fornire dati rilevanti che verranno usati dalla mappa quando crea la visualizzazione dell'annotazione. Per aggiungere un'annotazione a una mappa, è sufficiente chiamare il AddAnnotation metodo di un'istanza MKMapView , come illustrato nel codice seguente:

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

La variabile map qui è un'istanza di , MKMapViewovvero la classe che rappresenta la mappa stessa. MKMapView Userà i Coordinate dati derivati dall'istanza SampleMapAnnotation di per posizionare la visualizzazione dell'annotazione sulla mappa.

Il MKAnnotation protocollo fornisce un set noto di funzionalità in tutti gli oggetti che lo implementano, senza che il consumer (in questo caso la mappa) debba conoscere i dettagli di implementazione. Ciò semplifica l'aggiunta di un'ampia gamma di possibili annotazioni a una mappa.

Approfondimento dei protocolli

Poiché le interfacce C# non supportano metodi facoltativi, Xamarin.iOS esegue il mapping dei protocolli alle classi astratte. Pertanto, l'adozione di un protocollo in Objective-C viene eseguita in Xamarin.iOS derivando dalla classe astratta associata al protocollo e implementando i metodi richiesti. Questi metodi verranno esposti come metodi astratti nella classe . I metodi facoltativi del protocollo verranno associati ai metodi virtuali della classe C#.

Ad esempio, ecco una parte del UITableViewDataSource protocollo come associato in Xamarin.iOS:

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

Si noti che la classe è astratta. Xamarin.iOS rende la classe astratta per supportare i metodi facoltativi/obbligatori nei protocolli. Tuttavia, a differenza Objective-C dei protocolli o delle interfacce C#, le classi C# non supportano più ereditarietà. Ciò influisce sulla progettazione del codice C# che usa i protocolli e in genere porta a classi annidate. Altre informazioni su questo problema sono illustrate più avanti in questo documento, nella sezione Delegati.

GetCell(…) è un metodo astratto, associato al Objective-Cselettore, tableView:cellForRowAtIndexPath:, che è un metodo obbligatorio del UITableViewDataSource protocollo. Il selettore è il termine per il nome del Objective-C metodo. Per applicare il metodo come richiesto, Xamarin.iOS lo dichiara come astratto. L'altro metodo, NumberOfSections(…), è associato a numberOfSectionsInTableview:. Questo metodo è facoltativo nel protocollo, quindi Xamarin.iOS lo dichiara come virtuale, rendendolo facoltativo per eseguire l'override in C#.

Xamarin.iOS si occupa di tutte le associazioni iOS. Tuttavia, se è necessario associare manualmente un protocolloObjective-C, è possibile farlo decorando una classe con .ExportAttribute Si tratta dello stesso metodo usato da Xamarin.iOS stesso.

Per altre informazioni su come associare Objective-C tipi in Xamarin.iOS, vedere l'articolo Tipi di bindingObjective-C.

Non stiamo ancora usando protocolli. Vengono usati anche in iOS come base per Objective-C i delegati, che è l'argomento della sezione successiva.

Delegati

iOS usa Objective-C delegati per implementare il modello di delega, in cui un oggetto passa il lavoro a un altro. L'oggetto che esegue il lavoro è il delegato del primo oggetto. Un oggetto indica al delegato di eseguire operazioni inviando messaggi dopo determinati eventi. L'invio di un messaggio simile a questo in Objective-C equivale a livello funzionale alla chiamata di un metodo in C#. Un delegato implementa metodi in risposta a queste chiamate e quindi fornisce funzionalità all'applicazione.

I delegati consentono di estendere il comportamento delle classi senza dover creare sottoclassi. Le applicazioni in iOS spesso usano delegati quando una classe richiama a un'altra dopo un'azione importante. Ad esempio, la MKMapView classe richiama il delegato quando l'utente tocca un'annotazione su una mappa, dando all'autore della classe delegato la possibilità di rispondere all'interno dell'applicazione. È possibile usare un esempio di questo tipo di utilizzo delegato più avanti in questo articolo, in Esempio uso di un delegato con Xamarin.iOS.

A questo punto, ci si potrebbe chiedere come una classe determina quali metodi chiamare sul relativo delegato. Si tratta di un'altra posizione in cui si usano i protocolli. In genere, i metodi disponibili per un delegato provengono dai protocolli che adottano.

Modalità di utilizzo dei protocolli con i delegati

Si è visto in precedenza come vengono usati i protocolli per supportare l'aggiunta di annotazioni a una mappa. I protocolli vengono usati anche per fornire un set noto di metodi per le classi da chiamare dopo che si verificano determinati eventi, ad esempio dopo che l'utente tocca un'annotazione su una mappa o seleziona una cella in una tabella. Le classi che implementano questi metodi sono note come delegati delle classi che le chiamano.

Le classi che supportano la delega espongono una proprietà Delegate, a cui viene assegnata una classe che implementa il delegato. I metodi implementati per il delegato dipendono dal protocollo adottato dal delegato specifico. Per il UITableView metodo si implementa il UITableViewDelegate protocollo, per il UIAccelerometer metodo , si implementa UIAccelerometerDelegatee così via per qualsiasi altra classe in iOS per cui si vuole esporre un delegato.

La MKMapView classe illustrata nell'esempio precedente ha anche una proprietà denominata Delegate, che chiamerà dopo diversi eventi. Il delegato per MKMapView è di tipo MKMapViewDelegate. Questa operazione verrà usata a breve in un esempio per rispondere all'annotazione dopo che è stata selezionata, ma prima di tutto esaminiamo la differenza tra delegati sicuri e deboli.

Delegati sicuri e delegati deboli

I delegati esaminati finora sono delegati sicuri, vale a dire che sono fortemente tipizzato. Le associazioni Xamarin.iOS vengono fornite con una classe fortemente tipizzata per ogni protocollo delegato in iOS. Tuttavia, iOS ha anche il concetto di delegato debole. Invece di sottoclassare una classe associata al Objective-C protocollo per un delegato specifico, iOS consente anche di scegliere di associare manualmente i metodi di protocollo in qualsiasi classe che si vuole derivare da NSObject, decorare i metodi con ExportAttribute e quindi fornire i selettori appropriati. Quando si usa questo approccio, si assegna un'istanza della classe alla proprietà WeakDelegate anziché alla proprietà Delegate. Un delegato debole offre la flessibilità necessaria per ridurre la classe del delegato in una gerarchia di ereditarietà diversa. Si esaminerà ora un esempio di Xamarin.iOS che usa delegati sicuri e deboli.

Esempio di uso di un delegato con Xamarin.iOS

Per eseguire il codice in risposta all'utente toccando l'annotazione nell'esempio, è possibile sottoclassare MKMapViewDelegate e assegnare un'istanza alla MKMapViewproprietà di .Delegate Il MKMapViewDelegate protocollo contiene solo metodi facoltativi. Di conseguenza, tutti i metodi sono virtuali associati a questo protocollo nella classe Xamarin.iOS MKMapViewDelegate . Quando l'utente seleziona un'annotazione, l'istanza MKMapView invierà il mapView:didSelectAnnotationView: messaggio al relativo delegato. Per gestirlo in Xamarin.iOS, è necessario eseguire l'override del DidSelectAnnotationView (MKMapView mapView, MKAnnotationView annotationView) metodo nella sottoclasse MKMapViewDelegate come segue:

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

La classe SampleMapDelegate illustrata in precedenza viene implementata come classe nidificata nel controller che contiene l'istanza MKMapView . In Objective-Csi noterà spesso che il controller adotta più protocolli direttamente all'interno della classe . Tuttavia, poiché i protocolli sono associati alle classi in Xamarin.iOS, le classi che implementano delegati fortemente tipizzato vengono in genere incluse come classi annidate.

Con l'implementazione della classe delegato sul posto, è sufficiente creare un'istanza del delegato nel controller e assegnarla alla MKMapViewproprietà della Delegate classe come illustrato di seguito:

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
    {
        ...
    }
}

Per usare un delegato debole per eseguire la stessa operazione, è necessario associare il metodo manualmente in qualsiasi classe che deriva da NSObject e assegnarlo alla WeakDelegate proprietà di MKMapView. Poiché la UIViewController classe deriva in definitiva da NSObject (come ogni Objective-C classe in CocoaTouch), è sufficiente implementare un metodo associato a mapView:didSelectAnnotationView: direttamente nel controller e assegnare il controller a MKMapView, WeakDelegateevitando la necessità della classe annidata aggiuntiva. Il codice seguente illustra questo approccio:

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)
    {
        ...
    }
}

Quando si esegue questo codice, l'applicazione si comporta esattamente come quando si esegue la versione delegata fortemente tipizzata. Il vantaggio di questo codice è che il delegato debole non richiede la creazione della classe aggiuntiva creata quando è stato usato il delegato fortemente tipizzato. Tuttavia, ciò è a scapito della sicurezza dei tipi. Se si dovesse commettere un errore nel selettore passato a , non si scoprirà fino al ExportAttributeruntime.

Eventi e delegati

I delegati vengono usati per i callback in iOS in modo analogo al modo in cui .NET usa gli eventi. Per rendere le API iOS e il modo in cui usano Objective-C delegati sembrano più simili a .NET, Xamarin.iOS espone gli eventi .NET in molte posizioni in cui i delegati vengono usati in iOS.

Ad esempio, l'implementazione precedente in cui ha MKMapViewDelegate risposto a un'annotazione selezionata può essere implementata anche in Xamarin.iOS usando un evento .NET. In tal caso, l'evento verrebbe definito in MKMapView e chiamato DidSelectAnnotationView. Avrebbe una sottoclasse EventArgs di tipo MKMapViewAnnotationEventsArgs. La View proprietà di MKMapViewAnnotationEventsArgs darebbe un riferimento alla visualizzazione annotazione, da cui è possibile procedere con la stessa implementazione precedente, come illustrato di seguito:

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

Riepilogo

Questo articolo ha illustrato come usare eventi, protocolli e delegati in Xamarin.iOS. È stato illustrato come Xamarin.iOS esponga gli eventi di stile .NET normali per i controlli. Successivamente sono stati illustrati Objective-C i protocolli, tra cui il modo in cui sono diverse dalle interfacce C# e il modo in cui Xamarin.iOS li usa. Infine, sono stati esaminati Objective-C i delegati dal punto di vista di Xamarin.iOS. È stato illustrato come Xamarin.iOS supporta delegati fortemente e con tipizzata e come associare eventi .NET ai metodi delegati.