Compartir vía


Vistas nativas en XAML

Se puede hacer referencia directamente a las vistas nativas de iOS, Android y Plataforma universal de Windows desde archivos XAML Xamarin.Forms. Las propiedades y los controladores de eventos se pueden establecer en vistas nativas y pueden interactuar con vistas Xamarin.Forms. En este artículo se muestra cómo consumir vistas nativas de archivos XAML Xamarin.Forms.

Para insertar una vista nativa en un archivo XAML Xamarin.Forms:

  1. Agregue una declaración de espacio de nombres xmlns en el archivo XAML para el espacio de nombres que contiene la vista nativa.
  2. Cree una instancia de la vista nativa en el archivo XAML.

Importante

El XAML compilado debe deshabilitarse para las páginas XAML que usen vistas nativas. Esto se puede lograr mediante la decoración de la clase de código subyacente para la página XAML con el atributo [XamlCompilation(XamlCompilationOptions.Skip)]. Para obtener más información acerca de la compilación XAML, consulte Compilación XAML en Xamarin.Forms.

Para hacer referencia a una vista nativa desde un archivo de código subyacente, debe usar un proyecto de recursos compartidos (SAP) y encapsular el código específico de la plataforma con directivas de compilación condicional. Para obtener más información, consulte Referencia a vistas nativas desde el código.

Consumo de vistas nativas

En el ejemplo de código siguiente se muestra cómo consumir vistas nativas para cada plataforma en una Xamarin.FormsContentPage:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
        xmlns:androidWidget="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
        xmlns:androidLocal="clr-namespace:SimpleColorPicker.Droid;assembly=SimpleColorPicker.Droid;targetPlatform=Android"
        xmlns:win="clr-namespace:Windows.UI.Xaml.Controls;assembly=Windows, Version=255.255.255.255,
            Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
        x:Class="NativeViews.NativeViewDemo">
    <StackLayout Margin="20">
        <ios:UILabel Text="Hello World" TextColor="{x:Static ios:UIColor.Red}" View.HorizontalOptions="Start" />
        <androidWidget:TextView Text="Hello World" x:Arguments="{x:Static androidLocal:MainActivity.Instance}" />
        <win:TextBlock Text="Hello World" />
    </StackLayout>
</ContentPage>

Además de especificar el clr-namespace y assembly para un espacio de nombres de vista nativo, también se debe especificar targetPlatform. Debe establecerse en iOS, Android, UWP, Windows (que es equivalente a UWP), macOS, GTK, Tizen o WPF. En tiempo de ejecución, el analizador XAML omitirá los prefijos de espacio de nombres XML que tengan un targetPlatform que no coincida con la plataforma en la que se ejecuta la aplicación.

Cada declaración de espacio de nombres se puede usar para hacer referencia a cualquier clase o estructura del espacio de nombres especificado. Por ejemplo, la declaración de espacio de nombres ios se puede usar para hacer referencia a cualquier clase o estructura del espacio de nombres UIKit de iOS. Las propiedades de la vista nativa se pueden establecer a través de XAML, pero los tipos de propiedad y objeto deben coincidir. Por ejemplo, la propiedad UILabel.TextColor se establece en UIColor.Red mediante la extensión de marcado x:Static y el espacio de nombres ios.

Las propiedades enlazables y las propiedades enlazables adjuntas también se pueden establecer en vistas nativas mediante la sintaxis Class.BindableProperty="value". Cada vista nativa se ajusta en una instancia NativeViewWrapper específica de la plataforma, que se deriva de la clase Xamarin.Forms.View. Establecer una propiedad enlazable o una propiedad enlazable adjunta en una vista nativa transfiere el valor de propiedad al contenedor. Por ejemplo, se puede especificar un diseño horizontal centrado estableciendo View.HorizontalOptions="Center" en la vista nativa.

Nota:

Tenga en cuenta que los estilos no se pueden usar con vistas nativas, ya que los estilos solo pueden dirigirse a propiedades respaldadas por objetos BindableProperty.

Por lo general, los constructores de widgets de Android requieren el objeto Android Context como argumento y esto se puede poner a disposición a través de una propiedad estática en la clase MainActivity. Por lo tanto, al crear un widget de Android en XAML, el objeto Context generalmente debe pasarse al constructor del widget mediante el atributo x:Arguments con una extensión de marcado x:Static. Para obtener más información, consulte Pasar argumentos a vistas nativas.

Nota:

Tenga en cuenta que la nomenclatura de una vista nativa con x:Name no es posible en un proyecto de biblioteca de .NET Standard o en un proyecto de recursos compartidos (SAP). Si lo hace, generará una variable del tipo nativo, lo que provocará un error de compilación. Sin embargo, las vistas nativas se pueden encapsular en instancias ContentView y recuperarse en el archivo de código subyacente, siempre que se use un SAP. Para obtener más información, consulte Referencia a vista nativa desde el código.

Enlaces nativos

El enlace de datos se usa para sincronizar una interfaz de usuario con su origen de datos y simplifica la forma en que una aplicación Xamarin.Forms muestra e interactúa con sus datos. Siempre que el objeto de origen implemente la interfaz INotifyPropertyChanged, los cambios en el objeto de origen se insertan automáticamente en el objeto de destino por medio del marco de enlace, mientras que los cambios en el objeto de destino pueden insertarse de manera opcional en el objeto de origen.

Las propiedades de las vistas nativas también pueden usar el enlace de datos. En el ejemplo de código siguiente se muestra el enlace de datos mediante propiedades de vistas nativas:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
        xmlns:androidWidget="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
        xmlns:androidLocal="clr-namespace:SimpleColorPicker.Droid;assembly=SimpleColorPicker.Droid;targetPlatform=Android"
        xmlns:win="clr-namespace:Windows.UI.Xaml.Controls;assembly=Windows, Version=255.255.255.255,
            Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
        xmlns:local="clr-namespace:NativeSwitch"
        x:Class="NativeSwitch.NativeSwitchPage">
    <StackLayout Margin="20">
        <Label Text="Native Views Demo" FontAttributes="Bold" HorizontalOptions="Center" />
        <Entry Placeholder="This Entry is bound to the native switch" IsEnabled="{Binding IsSwitchOn}" />
        <ios:UISwitch On="{Binding Path=IsSwitchOn, Mode=TwoWay, UpdateSourceEventName=ValueChanged}"
            OnTintColor="{x:Static ios:UIColor.Red}"
            ThumbTintColor="{x:Static ios:UIColor.Blue}" />
        <androidWidget:Switch x:Arguments="{x:Static androidLocal:MainActivity.Instance}"
            Checked="{Binding Path=IsSwitchOn, Mode=TwoWay, UpdateSourceEventName=CheckedChange}"
            Text="Enable Entry?" />
        <win:ToggleSwitch Header="Enable Entry?"
            OffContent="No"
            OnContent="Yes"
            IsOn="{Binding IsSwitchOn, Mode=TwoWay, UpdateSourceEventName=Toggled}" />
    </StackLayout>
</ContentPage>

La página contiene un Entry cuya propiedad IsEnabled se enlaza a la propiedad NativeSwitchPageViewModel.IsSwitchOn. El BindingContext de la página se establece en una nueva instancia de la clase NativeSwitchPageViewModel en el archivo de código subyacente, con la clase ViewModel que implementa la interfaz INotifyPropertyChanged.

La página también contiene un interruptor nativo para cada plataforma. Cada interruptor nativo usa un enlace TwoWay para actualizar el valor de la propiedad NativeSwitchPageViewModel.IsSwitchOn. Por lo tanto, cuando el interruptor está desactivado, Entry está deshabilitado y, cuando el interruptor está activado, Entry está habilitado. En las capturas de pantalla siguientes se muestra esta funcionalidad en cada plataforma:

Conmutador nativo deshabilitadoConmutador nativo habilitado

Los enlaces bidireccionales se admiten automáticamente siempre que la propiedad nativa implemente INotifyPropertyChanged o admita la observación de clave-valor (KVO) en iOS, o que sea DependencyProperty en UWP. Sin embargo, muchas vistas nativas no admiten la notificación de cambio de propiedad. Para estas vistas, puede especificar un valor de propiedad UpdateSourceEventName como parte de la expresión de enlace. Esta propiedad debe establecerse en el nombre de un evento en la vista nativa que indica cuándo ha cambiado la propiedad de destino. A continuación, cuando cambia el valor del interruptor nativo, se notifica a la clase Binding que el usuario ha cambiado el valor del interruptor y se actualiza el valor de la propiedad NativeSwitchPageViewModel.IsSwitchOn.

Pasar argumentos a vistas nativas

Los argumentos del constructor se pueden pasar a vistas nativas mediante el atributo x:Arguments con una extensión de marcado x:Static. Además, se pueden llamar a los Factory Method de vistas nativas (métodos public static que devuelven objetos o valores del mismo tipo que la clase o estructura que define los métodos) especificando el nombre del método mediante el atributo x:FactoryMethod y sus argumentos mediante el atributo x:Arguments.

En el siguiente ejemplo de código se muestra ambas técnicas:

<ContentPage ...
        xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
        xmlns:androidWidget="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
        xmlns:androidGraphics="clr-namespace:Android.Graphics;assembly=Mono.Android;targetPlatform=Android"
        xmlns:androidLocal="clr-namespace:SimpleColorPicker.Droid;assembly=SimpleColorPicker.Droid;targetPlatform=Android"
        xmlns:winControls="clr-namespace:Windows.UI.Xaml.Controls;assembly=Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
        xmlns:winMedia="clr-namespace:Windows.UI.Xaml.Media;assembly=Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
        xmlns:winText="clr-namespace:Windows.UI.Text;assembly=Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
        xmlns:winui="clr-namespace:Windows.UI;assembly=Windows, Version=255.255.255.255, Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows">
        ...
        <ios:UILabel Text="Simple Native Color Picker" View.HorizontalOptions="Center">
            <ios:UILabel.Font>
                <ios:UIFont x:FactoryMethod="FromName">
                    <x:Arguments>
                        <x:String>Papyrus</x:String>
                        <x:Single>24</x:Single>
                    </x:Arguments>
                </ios:UIFont>
            </ios:UILabel.Font>
        </ios:UILabel>
        <androidWidget:TextView x:Arguments="{x:Static androidLocal:MainActivity.Instance}"
                    Text="Simple Native Color Picker"
                    TextSize="24"
                    View.HorizontalOptions="Center">
            <androidWidget:TextView.Typeface>
                <androidGraphics:Typeface x:FactoryMethod="Create">
                    <x:Arguments>
                        <x:String>cursive</x:String>
                        <androidGraphics:TypefaceStyle>Normal</androidGraphics:TypefaceStyle>
                    </x:Arguments>
                </androidGraphics:Typeface>
            </androidWidget:TextView.Typeface>
        </androidWidget:TextView>
        <winControls:TextBlock Text="Simple Native Color Picker"
                    FontSize="20"
                    FontStyle="{x:Static winText:FontStyle.Italic}"
                    View.HorizontalOptions="Center">
            <winControls:TextBlock.FontFamily>
                <winMedia:FontFamily>
                    <x:Arguments>
                        <x:String>Georgia</x:String>
                    </x:Arguments>
                </winMedia:FontFamily>
            </winControls:TextBlock.FontFamily>
        </winControls:TextBlock>
        ...
</ContentPage>

El Factory Method UIFont.FromName se usa para establecer la propiedad UILabel.Font en un nuevo UIFont en iOS. Los argumentos de método especifican el nombre y el tamaño de UIFont que son elementos secundarios del atributo x:Arguments.

El Factory Method Typeface.Create se usa para establecer la propiedad TextView.Typeface en un nuevo Typeface en Android. Los argumentos de método especifican el nombre de la familia y el estilo de Typeface que son elementos secundarios del atributo x:Arguments.

El constructor FontFamily se usa para establecer la propiedad TextBlock.FontFamily en un nuevo FontFamily en la Plataforma universal de Windows (UWP). El nombre FontFamily se especifica mediante el argumento de método que es un elemento secundario del atributo x:Arguments.

Nota:

Los argumentos deben coincidir con los tipos requeridos por el constructor o el Factory Method.

Las capturas de pantalla siguientes muestran el resultado de especificar argumentos de constructor y Factory Method para establecer la fuente en diferentes vistas nativas:

Establecer fuentes en vistas nativas

Para obtener más información sobre cómo pasar argumentos en XAML, consulta Paso de argumentos en XAML.

Consulte las vistas nativas desde el código

Aunque no es posible asignar un nombre a una vista nativa con el atributo x:Name, es posible recuperar una instancia de vista nativa declarada en un archivo XAML de su archivo de código subyacente en un proyecto de acceso compartido, siempre que la vista nativa sea un elemento secundario de un ContentView que especifique un valor de atributo x:Name. A continuación, dentro de las directivas de compilación condicional en el archivo de código subyacente debe:

  1. Recuperar el valor de la propiedad ContentView.Content y convertirlo en un tipo NativeViewWrapper específico de la plataforma.
  2. Recupere la propiedad NativeViewWrapper.NativeElement y convertirla en el tipo de vista nativo.

A continuación, se puede invocar la API nativa en la vista nativa para realizar las operaciones deseadas. Este enfoque también ofrece la ventaja de que varias vistas nativas XAML para distintas plataformas pueden ser elementos secundarios del mismo ContentView. En el siguiente código de ejemplo se muestra esta técnica:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
        xmlns:androidWidget="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
        xmlns:androidLocal="clr-namespace:SimpleColorPicker.Droid;assembly=SimpleColorPicker.Droid;targetPlatform=Android"
        xmlns:winControls="clr-namespace:Windows.UI.Xaml.Controls;assembly=Windows, Version=255.255.255.255,
            Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
        xmlns:local="clr-namespace:NativeViewInsideContentView"
        x:Class="NativeViewInsideContentView.NativeViewInsideContentViewPage">
    <StackLayout Margin="20">
        <ContentView x:Name="contentViewTextParent" HorizontalOptions="Center" VerticalOptions="CenterAndExpand">
            <ios:UILabel Text="Text in a UILabel" TextColor="{x:Static ios:UIColor.Red}" />
            <androidWidget:TextView x:Arguments="{x:Static androidLocal:MainActivity.Instance}"
                Text="Text in a TextView" />
              <winControls:TextBlock Text="Text in a TextBlock" />
        </ContentView>
        <ContentView x:Name="contentViewButtonParent" HorizontalOptions="Center" VerticalOptions="EndAndExpand">
            <ios:UIButton TouchUpInside="OnButtonTap" View.HorizontalOptions="Center" View.VerticalOptions="Center" />
            <androidWidget:Button x:Arguments="{x:Static androidLocal:MainActivity.Instance}"
                Text="Scale and Rotate Text"
                Click="OnButtonTap" />
            <winControls:Button Content="Scale and Rotate Text" />
        </ContentView>
    </StackLayout>
</ContentPage>

En el ejemplo anterior, las vistas nativas de cada plataforma son elementos secundarios de controles ContentView, con el valor de atributo x:Name que se usa para recuperar el ContentView en el código subyacente:

public partial class NativeViewInsideContentViewPage : ContentPage
{
    public NativeViewInsideContentViewPage()
    {
        InitializeComponent();

#if __IOS__
        var wrapper = (Xamarin.Forms.Platform.iOS.NativeViewWrapper)contentViewButtonParent.Content;
        var button = (UIKit.UIButton)wrapper.NativeView;
        button.SetTitle("Scale and Rotate Text", UIKit.UIControlState.Normal);
        button.SetTitleColor(UIKit.UIColor.Black, UIKit.UIControlState.Normal);
#endif
#if __ANDROID__
        var wrapper = (Xamarin.Forms.Platform.Android.NativeViewWrapper)contentViewTextParent.Content;
        var textView = (Android.Widget.TextView)wrapper.NativeView;
        textView.SetTextColor(Android.Graphics.Color.Red);
#endif
#if WINDOWS_UWP
        var textWrapper = (Xamarin.Forms.Platform.UWP.NativeViewWrapper)contentViewTextParent.Content;
        var textBlock = (Windows.UI.Xaml.Controls.TextBlock)textWrapper.NativeElement;
        textBlock.Foreground = new Windows.UI.Xaml.Media.SolidColorBrush(Windows.UI.Colors.Red);
        var buttonWrapper = (Xamarin.Forms.Platform.UWP.NativeViewWrapper)contentViewButtonParent.Content;
        var button = (Windows.UI.Xaml.Controls.Button)buttonWrapper.NativeElement;
        button.Click += (sender, args) => OnButtonTap(sender, EventArgs.Empty);
#endif
    }

    async void OnButtonTap(object sender, EventArgs e)
    {
        contentViewButtonParent.Content.IsEnabled = false;
        contentViewTextParent.Content.ScaleTo(2, 2000);
        await contentViewTextParent.Content.RotateTo(360, 2000);
        contentViewTextParent.Content.ScaleTo(1, 2000);
        await contentViewTextParent.Content.RelRotateTo(360, 2000);
        contentViewButtonParent.Content.IsEnabled = true;
    }
}

Se tiene acceso a la propiedad ContentView.Content para recuperar la vista nativa ajustada como una instancia NativeViewWrapper específica de la plataforma. A continuación, se obtiene acceso a la propiedad NativeViewWrapper.NativeElement para recuperar la vista nativa como su tipo nativo. A continuación, se invoca la API de la vista nativa para realizar las operaciones deseadas.

Los botones nativos de iOS y Android comparten el mismo controlador de eventos OnButtonTap, ya que cada botón nativo consume un delegado EventHandler en respuesta a un evento táctil. Sin embargo, la Plataforma universal de Windows (UWP) usa un elemento independiente RoutedEventHandler, que a su vez consume el controlador de eventos OnButtonTap en este ejemplo. Por lo tanto, cuando se hace clic en un botón nativo, se ejecuta el controlador de eventos OnButtonTap, que escala y gira el control nativo contenido en el ContentView objeto denominado contentViewTextParent. Las capturas de pantalla siguientes muestran que esto se produce en cada plataforma:

ContentView que contiene un control nativo

Vistas nativas de subclase

Muchas vistas nativas de iOS y Android no son adecuadas para crear instancias en XAML porque usan métodos, en lugar de propiedades, para configurar el control. La solución a este problema consiste en subclases de vistas nativas en contenedores que definen una API más fácil de XAML que usa propiedades para configurar el control y que usa eventos independientes de la plataforma. A continuación, las vistas nativas ajustadas se pueden colocar en un proyecto de recursos compartidos (SAP) y estar rodeadas de directivas de compilación condicionales, o colocarse en proyectos específicos de la plataforma y hacer referencia a ellos desde XAML en un proyecto de biblioteca de .NET Standard.

En el ejemplo de código siguiente se muestra una página Xamarin.Forms que consume vistas nativas con subclases:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:ios="clr-namespace:UIKit;assembly=Xamarin.iOS;targetPlatform=iOS"
        xmlns:iosLocal="clr-namespace:SubclassedNativeControls.iOS;assembly=SubclassedNativeControls.iOS;targetPlatform=iOS"
        xmlns:android="clr-namespace:Android.Widget;assembly=Mono.Android;targetPlatform=Android"
        xmlns:androidLocal="clr-namespace:SimpleColorPicker.Droid;assembly=SimpleColorPicker.Droid;targetPlatform=Android"
        xmlns:androidLocal="clr-namespace:SubclassedNativeControls.Droid;assembly=SubclassedNativeControls.Droid;targetPlatform=Android"
        xmlns:winControls="clr-namespace:Windows.UI.Xaml.Controls;assembly=Windows, Version=255.255.255.255,
            Culture=neutral, PublicKeyToken=null, ContentType=WindowsRuntime;targetPlatform=Windows"
        xmlns:local="clr-namespace:SubclassedNativeControls"
        x:Class="SubclassedNativeControls.SubclassedNativeControlsPage">
    <StackLayout Margin="20">
        <Label Text="Subclassed Native Views Demo" FontAttributes="Bold" HorizontalOptions="Center" />
        <StackLayout Orientation="Horizontal">
          <Label Text="You have chosen:" />
          <Label Text="{Binding SelectedFruit}" />      
        </StackLayout>
        <iosLocal:MyUIPickerView ItemsSource="{Binding Fruits}"
            SelectedItem="{Binding SelectedFruit, Mode=TwoWay, UpdateSourceEventName=SelectedItemChanged}" />
        <androidLocal:MySpinner x:Arguments="{x:Static androidLocal:MainActivity.Instance}"
            ItemsSource="{Binding Fruits}"
            SelectedObject="{Binding SelectedFruit, Mode=TwoWay, UpdateSourceEventName=ItemSelected}" />
        <winControls:ComboBox ItemsSource="{Binding Fruits}"
            SelectedItem="{Binding SelectedFruit, Mode=TwoWay, UpdateSourceEventName=SelectionChanged}" />
    </StackLayout>
</ContentPage>

La página contiene un Label que muestra la fruta elegida por el usuario desde un control nativo. El Label enlaza a la propiedad SubclassedNativeControlsPageViewModel.SelectedFruit. El BindingContext de la página se establece en una nueva instancia de la clase SubclassedNativeControlsPageViewModel en el archivo de código subyacente, con la clase ViewModel que implementa la interfaz INotifyPropertyChanged.

La página también contiene una vista de selector nativa para cada plataforma. Cada vista nativa muestra la colección de frutas enlazando su propiedad ItemSource a la colección SubclassedNativeControlsPageViewModel.Fruits. Esto permite al usuario elegir una fruta, como se muestra en las capturas de pantalla siguientes:

Vistas nativas de subclase

En iOS y Android, los selectores nativos usan métodos para configurar los controles. Por lo tanto, estos selectores deben tener subclases para exponer propiedades para que sean compatibles con XAML. En la Plataforma universal de Windows (UWP), el ComboBox ya es compatible con XAML, por lo que no requiere subclases.

iOS

La implementación de iOS pone las subclases en la vista UIPickerView y expone las propiedades y un evento que se puede consumir fácilmente desde XAML:

public class MyUIPickerView : UIPickerView
{
    public event EventHandler<EventArgs> SelectedItemChanged;

    public MyUIPickerView()
    {
        var model = new PickerModel();
        model.ItemChanged += (sender, e) =>
        {
            if (SelectedItemChanged != null)
            {
                SelectedItemChanged.Invoke(this, e);
            }
        };
        Model = model;
    }

    public IList<string> ItemsSource
    {
        get
        {
            var pickerModel = Model as PickerModel;
            return (pickerModel != null) ? pickerModel.Items : null;
        }
        set
        {
            var model = Model as PickerModel;
            if (model != null)
            {
                model.Items = value;
            }
        }
    }

    public string SelectedItem
    {
        get { return (Model as PickerModel).SelectedItem; }
        set { }
    }
}

La clase MyUIPickerView expone las propiedades ItemsSource y SelectedItem, y un evento SelectedItemChanged. Un UIPickerView requiere un modelo de datos UIPickerViewModel subyacente, al que acceden las propiedades y el evento MyUIPickerView. La clase PickerModel proporciona el modelo de datos UIPickerViewModel:

class PickerModel : UIPickerViewModel
{
    int selectedIndex = 0;
    public event EventHandler<EventArgs> ItemChanged;
    public IList<string> Items { get; set; }

    public string SelectedItem
    {
        get
        {
            return Items != null && selectedIndex >= 0 && selectedIndex < Items.Count ? Items[selectedIndex] : null;
        }
    }

    public override nint GetRowsInComponent(UIPickerView pickerView, nint component)
    {
        return Items != null ? Items.Count : 0;
    }

    public override string GetTitle(UIPickerView pickerView, nint row, nint component)
    {
        return Items != null && Items.Count > row ? Items[(int)row] : null;
    }

    public override nint GetComponentCount(UIPickerView pickerView)
    {
        return 1;
    }

    public override void Selected(UIPickerView pickerView, nint row, nint component)
    {
        selectedIndex = (int)row;
        if (ItemChanged != null)
        {
            ItemChanged.Invoke(this, new EventArgs());
        }
    }
}

La clase PickerModel proporciona el almacenamiento subyacente para la clase MyUIPickerView a través de la propiedad Items. Cada vez que cambia el elemento seleccionado en el MyUIPickerView, se ejecuta el método Selected, que actualiza el índice seleccionado y desencadena el evento ItemChanged. Esto garantiza que la propiedad SelectedItem siempre devolverá el último elemento seleccionado por el usuario. Además, la clase PickerModel invalida los métodos que se usan para configurar la instancia MyUIPickerView.

Android

La implementación de Android pone las subclases en la vista Spinner y expone las propiedades y un evento que se puede consumir fácilmente desde XAML:

class MySpinner : Spinner
{
    ArrayAdapter adapter;
    IList<string> items;

    public IList<string> ItemsSource
    {
        get { return items; }
        set
        {
            if (items != value)
            {
                items = value;
                adapter.Clear();

                foreach (string str in items)
                {
                    adapter.Add(str);
                }
            }
        }
    }

    public string SelectedObject
    {
        get { return (string)GetItemAtPosition(SelectedItemPosition); }
        set
        {
            if (items != null)
            {
                int index = items.IndexOf(value);
                if (index != -1)
                {
                    SetSelection(index);
                }
            }
        }
    }

    public MySpinner(Context context) : base(context)
    {
        ItemSelected += OnBindableSpinnerItemSelected;

        adapter = new ArrayAdapter(context, Android.Resource.Layout.SimpleSpinnerItem);
        adapter.SetDropDownViewResource(Android.Resource.Layout.SimpleSpinnerDropDownItem);
        Adapter = adapter;
    }

    void OnBindableSpinnerItemSelected(object sender, ItemSelectedEventArgs args)
    {
        SelectedObject = (string)GetItemAtPosition(args.Position);
    }
}

La clase MySpinner expone las propiedades ItemsSource y SelectedObject y un evento ItemSelected. Los elementos mostrados por la clase MySpinner se proporcionan mediante el Adapter asociado a la vista y los elementos se rellenan en el Adapter cuando se establece la propiedad ItemsSource por primera vez. Siempre que el elemento seleccionado de la clase MySpinner cambie, el controlador de eventos OnBindableSpinnerItemSelected actualiza la propiedad SelectedObject.