Parte 5. De enlaces de datos a MVVM

Download SampleDescargar el ejemplo

El patrón de arquitectura Modelo-Vista-Modelo de vista (MVVM) se inventó teniendo en cuenta XAML. El patrón aplica una separación entre tres capas de software: la interfaz de usuario de XAML, denominada Vista, los datos subyacentes, denominados Modelo, y un intermediario entre Vista y Modelo, denominado Modelo de vista. La Vista y el Modelo de vista a menudo se conectan a través de enlaces de datos definidos en el archivo XAML. El contexto de enlace (BindingContext) para la Vista suele ser una instancia de Modelo de vista.

Un Modelo de vista simple

Como introducción a los Modelos de vista, echemos un vistazo primero a un programa sin uno. Anteriormente vimos cómo definir una nueva declaración de espacio de nombres XML para permitir que un archivo XAML haga referencia a clases en otros ensamblados. Este es un programa que define una declaración de espacio de nombres XML para el espacio de nombres System:

xmlns:sys="clr-namespace:System;assembly=netstandard"

El programa puede usar x:Static para obtener la fecha y hora actuales de la propiedad estática DateTime.Now y establecer ese valor DateTime a BindingContext en StackLayout:

<StackLayout BindingContext="{x:Static sys:DateTime.Now}" …>

BindingContext es una propiedad especial: cuando se establece BindingContext en un elemento, todos los elementos secundarios de ese elemento lo heredan. Esto significa que todos los elementos secundarios de StackLayout tienen este mismo BindingContexty pueden contener enlaces simples a propiedades de ese objeto.

En el programa One-Shot DateTime, dos de los elementos secundarios contienen enlaces a propiedades de ese valor de DateTime, pero otros dos elementos secundarios contienen enlaces que parecen faltar en una ruta de enlace. Esto significa que el propio valor DateTime se usa para StringFormat:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:sys="clr-namespace:System;assembly=netstandard"
             x:Class="XamlSamples.OneShotDateTimePage"
             Title="One-Shot DateTime Page">

    <StackLayout BindingContext="{x:Static sys:DateTime.Now}"
                 HorizontalOptions="Center"
                 VerticalOptions="Center">

        <Label Text="{Binding Year, StringFormat='The year is {0}'}" />
        <Label Text="{Binding StringFormat='The month is {0:MMMM}'}" />
        <Label Text="{Binding Day, StringFormat='The day is {0}'}" />
        <Label Text="{Binding StringFormat='The time is {0:T}'}" />

    </StackLayout>
</ContentPage>

El problema es que la fecha y la hora se establecen una vez cuando se compila por primera vez la página y nunca cambian:

View Displaying Date and Time

Un archivo XAML puede mostrar un reloj que siempre muestra la hora actual, pero necesita código complementario. Al pensar en términos de MVVM, las clases Modelo y Modelo de vista se escriben completamente en el código. La Vista suele ser un archivo XAML que hace referencia a las propiedades definidas en Modelo de vista a través de enlaces de datos.

Un Modelo adecuado es independiente del Modelo de vista, y un Modelo de vista adecuado es independiente de la Vista. Sin embargo, a menudo un programador adapta los tipos de datos expuestos por el Modelo de vista a los tipos de datos asociados a interfaces de usuario concretas. Por ejemplo, si un Modelo tiene acceso a una base de datos que contiene cadenas ASCII de 8 bits de caracteres, el Modelo de vista tendrá que convertir entre esas cadenas a cadenas Unicode para dar cabida al uso exclusivo de Unicode en la interfaz de usuario.

En ejemplos sencillos de MVVM (como los que se muestran aquí), a menudo no hay ningún Modelo y el patrón implica solo una Vista y un Modelo de vista vinculados con enlaces de datos.

Este es un Modelo de vista para un reloj con una sola propiedad denominada DateTime, que actualiza la propiedad DateTime cada segundo:

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    class ClockViewModel : INotifyPropertyChanged
    {
        DateTime dateTime;

        public event PropertyChangedEventHandler PropertyChanged;

        public ClockViewModel()
        {
            this.DateTime = DateTime.Now;

            Device.StartTimer(TimeSpan.FromSeconds(1), () =>
                {
                    this.DateTime = DateTime.Now;
                    return true;
                });
        }

        public DateTime DateTime
        {
            set
            {
                if (dateTime != value)
                {
                    dateTime = value;

                    if (PropertyChanged != null)
                    {
                        PropertyChanged(this, new PropertyChangedEventArgs("DateTime"));
                    }
                }
            }
            get
            {
                return dateTime;
            }
        }
    }
}

Los Modelos de vista generalmente implementan la interfaz INotifyPropertyChanged, lo cual significa que la clase activa un evento PropertyChanged cada vez que cambia una de sus propiedades. El mecanismo de enlace de datos de Xamarin.Forms asocia un controlador a este evento PropertyChanged para que se pueda notificar cuando cambia una propiedad y mantener el destino actualizado con el nuevo valor.

Un reloj basado en este Modelo de vista puede ser tan sencillo como lo siguiente:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.ClockPage"
             Title="Clock Page">

    <Label Text="{Binding DateTime, StringFormat='{0:T}'}"
           FontSize="Large"
           HorizontalOptions="Center"
           VerticalOptions="Center">
        <Label.BindingContext>
            <local:ClockViewModel />
        </Label.BindingContext>
    </Label>
</ContentPage>

Observe cómo se establece ClockViewModel en BindingContext deLabel mediante etiquetas de elemento de propiedad. Como alternativa, puede crear instancias de ClockViewModel en una colección Resources y establecerla en BindingContext a través de una extensión de marcado StaticResource. O bien, el archivo de código subyacente puede crear una instancia de Modelo de vista.

La extensión de marcado Binding en la propiedad Text de los formatos Label a la propiedad de DateTime. Esta es la pantalla:

View Displaying Date and Time via ViewModel

También es posible acceder a las propiedades individuales de la propiedad DateTime del Modelo de vista separando las propiedades con puntos:

<Label Text="{Binding DateTime.Second, StringFormat='{0}'}" … >

MVVM interactivo

MVVM se suele usar con enlaces de datos bidireccionales para una vista interactiva basada en un modelo de datos subyacente.

Esta es una clase denominada HslViewModel que convierte un valor Color en los valores Hue, Saturation y Luminosity y viceversa:

using System;
using System.ComponentModel;
using Xamarin.Forms;

namespace XamlSamples
{
    public class HslViewModel : INotifyPropertyChanged
    {
        double hue, saturation, luminosity;
        Color color;

        public event PropertyChangedEventHandler PropertyChanged;

        public double Hue
        {
            set
            {
                if (hue != value)
                {
                    hue = value;
                    OnPropertyChanged("Hue");
                    SetNewColor();
                }
            }
            get
            {
                return hue;
            }
        }

        public double Saturation
        {
            set
            {
                if (saturation != value)
                {
                    saturation = value;
                    OnPropertyChanged("Saturation");
                    SetNewColor();
                }
            }
            get
            {
                return saturation;
            }
        }

        public double Luminosity
        {
            set
            {
                if (luminosity != value)
                {
                    luminosity = value;
                    OnPropertyChanged("Luminosity");
                    SetNewColor();
                }
            }
            get
            {
                return luminosity;
            }
        }

        public Color Color
        {
            set
            {
                if (color != value)
                {
                    color = value;
                    OnPropertyChanged("Color");

                    Hue = value.Hue;
                    Saturation = value.Saturation;
                    Luminosity = value.Luminosity;
                }
            }
            get
            {
                return color;
            }
        }

        void SetNewColor()
        {
            Color = Color.FromHsla(Hue, Saturation, Luminosity);
        }

        protected virtual void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Los cambios realizados en las propiedades Hue, Saturation y Luminosity hacen que la propiedad Color cambie y los cambios en Color hacen que cambien las otras tres propiedades. Esto puede parecer un bucle infinito, salvo que la clase no invoca el evento PropertyChanged a menos que la propiedad haya cambiado. Esto pone punto final a lo que de otro modo sería un bucle de retroalimentación incontrolable.

El siguiente archivo XAML contiene un BoxView cuya propiedad Color está enlazada a la propiedad Color del Modelo de vista, y tres Slider y tres vistas Label enlazadas a las propiedades Hue, Saturation y Luminosity:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.HslColorScrollPage"
             Title="HSL Color Scroll Page">
    <ContentPage.BindingContext>
        <local:HslViewModel Color="Aqua" />
    </ContentPage.BindingContext>

    <StackLayout Padding="10, 0">
        <BoxView Color="{Binding Color}"
                 VerticalOptions="FillAndExpand" />

        <Label Text="{Binding Hue, StringFormat='Hue = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Hue, Mode=TwoWay}" />

        <Label Text="{Binding Saturation, StringFormat='Saturation = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Saturation, Mode=TwoWay}" />

        <Label Text="{Binding Luminosity, StringFormat='Luminosity = {0:F2}'}"
               HorizontalOptions="Center" />

        <Slider Value="{Binding Luminosity, Mode=TwoWay}" />
    </StackLayout>
</ContentPage>

El enlace en cada Label es el valor predeterminado OneWay. Solo necesita mostrar el valor. Pero el enlace en cada Slider es TwoWay. Esto permite que Slider se inicialice a partir del Modelo de vista. Observe que la propiedad Color está establecida en Aqua cuando se crea una instancia de Modelo de vista. Pero un cambio en Slider también debe establecer un nuevo valor para la propiedad en el Modelo de vista, que a continuación calcula un nuevo color.

MVVM using Two-Way Data Bindings

Comandos con Modelos de vista

En muchos casos, el patrón MVVM está restringido a la manipulación de elementos de datos: objetos de interfaz de usuario en los objetos de datos paralelos de Vista en el Modelo de vista.

Sin embargo, a veces la Vista debe contener botones que desencadenen varias acciones en el Modelo de vista. Pero el Modelo de vista no debe contener controladores Clicked para los botones porque eso vincularía el Modelo de vista a un paradigma de interfaz de usuario determinado.

Para permitir que los Modelos de vista sean más independientes respecto a determinados objetos de interfaz de usuario, pero seguir permitiendo llamar a métodos dentro del Modelo de vista, existe una interfaz de comandos. Esta interfaz de comandos es compatible con los siguientes elementos de Xamarin.Forms:

  • Button
  • MenuItem
  • ToolbarItem
  • SearchBar
  • TextCell (y por lo tanto también ImageCell)
  • ListView
  • TapGestureRecognizer

Con la excepción del elemento SearchBar y ListView, estos elementos definen dos propiedades:

  • Command de tipo System.Windows.Input.ICommand
  • CommandParameter de tipo Object

SearchBar define las propiedades SearchCommand y SearchCommandParameter, mientras que ListView define una propiedad RefreshCommand de tipo ICommand.

La interfaz de ICommand declara dos métodos y un evento:

  • void Execute(object arg)
  • bool CanExecute(object arg)
  • event EventHandler CanExecuteChanged

El Modelo de vista puede definir propiedades de tipo ICommand. Después puedes enlazar estas propiedades a la propiedad Command de cada Button u otro elemento, o quizás una vista personalizada que implemente esta interfaz. Opcionalmente, puede establecer la propiedad CommandParameter para identificar objetos Button individuales (u otros elementos) enlazados a esta propiedad de Modelo de vista. Internamente, Button llama al método Execute cada vez que el usuario pulsa en Button, pasando al método Execute su CommandParameter.

El método CanExecute y el evento CanExecuteChanged se usan para los casos en los que una pulsación Button podría ser actualmente no válida, en cuyo caso Button debe deshabilitarse. Button llama a CanExecute cuando la propiedad Command se establece por primera vez y cada vez que se desencadena el evento CanExecuteChanged. Si CanExecute devuelve false, Button se deshabilita y no genera llamadas Execute.

Para obtener ayuda para agregar comandos a los Modelos de vista, Xamarin.Forms define dos clases que implementan ICommand: Command y Command<T>, donde T es el tipo de los argumentos en Execute y CanExecute. Estas dos clases definen varios constructores más un método ChangeCanExecute al que el Modelo de vista puede llamar para forzar el objeto Command para desencadenar el evento CanExecuteChanged.

Este es un Modelo de vista para un teclado simple destinado a escribir números de teléfono. Observe que el método Execute y CanExecute se definen como funciones lambda directamente en el constructor:

using System;
using System.ComponentModel;
using System.Windows.Input;
using Xamarin.Forms;

namespace XamlSamples
{
    class KeypadViewModel : INotifyPropertyChanged
    {
        string inputString = "";
        string displayText = "";
        char[] specialChars = { '*', '#' };

        public event PropertyChangedEventHandler PropertyChanged;

        // Constructor
        public KeypadViewModel()
        {
            AddCharCommand = new Command<string>((key) =>
                {
                    // Add the key to the input string.
                    InputString += key;
                });

            DeleteCharCommand = new Command(() =>
                {
                    // Strip a character from the input string.
                    InputString = InputString.Substring(0, InputString.Length - 1);
                },
                () =>
                {
                    // Return true if there's something to delete.
                    return InputString.Length > 0;
                });
        }

        // Public properties
        public string InputString
        {
            protected set
            {
                if (inputString != value)
                {
                    inputString = value;
                    OnPropertyChanged("InputString");
                    DisplayText = FormatText(inputString);

                    // Perhaps the delete button must be enabled/disabled.
                    ((Command)DeleteCharCommand).ChangeCanExecute();
                }
            }

            get { return inputString; }
        }

        public string DisplayText
        {
            protected set
            {
                if (displayText != value)
                {
                    displayText = value;
                    OnPropertyChanged("DisplayText");
                }
            }
            get { return displayText; }
        }

        // ICommand implementations
        public ICommand AddCharCommand { protected set; get; }

        public ICommand DeleteCharCommand { protected set; get; }

        string FormatText(string str)
        {
            bool hasNonNumbers = str.IndexOfAny(specialChars) != -1;
            string formatted = str;

            if (hasNonNumbers || str.Length < 4 || str.Length > 10)
            {
            }
            else if (str.Length < 8)
            {
                formatted = String.Format("{0}-{1}",
                                          str.Substring(0, 3),
                                          str.Substring(3));
            }
            else
            {
                formatted = String.Format("({0}) {1}-{2}",
                                          str.Substring(0, 3),
                                          str.Substring(3, 3),
                                          str.Substring(6));
            }
            return formatted;
        }

        protected void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Este Modelo de vista supone que la propiedad AddCharCommand está enlazada a la propiedad Command de varios botones (o cualquier otra cosa que tenga una interfaz de comando), cada una de las cuales se identifica mediante CommandParameter. Estos botones agregan caracteres a una propiedad InputString, a la que a luego se da formato como un número de teléfono para la propiedad DisplayText.

También hay una segunda propiedad de tipo ICommand denominada DeleteCharCommand. Esto está enlazado a un botón de retroceso, pero el botón debe deshabilitarse si no hay caracteres que eliminar.

El teclado siguiente podría ser más sofisticado visualmente. Alternativamente, el marcado se ha reducido a un mínimo para demostrar con más claridad el uso de la interfaz de comandos:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples;assembly=XamlSamples"
             x:Class="XamlSamples.KeypadPage"
             Title="Keypad Page">

    <Grid HorizontalOptions="Center"
          VerticalOptions="Center">
        <Grid.BindingContext>
            <local:KeypadViewModel />
        </Grid.BindingContext>

        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
            <ColumnDefinition Width="80" />
        </Grid.ColumnDefinitions>

        <!-- Internal Grid for top row of items -->
        <Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>

            <Frame Grid.Column="0"
                   OutlineColor="Accent">
                <Label Text="{Binding DisplayText}" />
            </Frame>

            <Button Text="&#x21E6;"
                    Command="{Binding DeleteCharCommand}"
                    Grid.Column="1"
                    BorderWidth="0" />
        </Grid>

        <Button Text="1"
                Command="{Binding AddCharCommand}"
                CommandParameter="1"
                Grid.Row="1" Grid.Column="0" />

        <Button Text="2"
                Command="{Binding AddCharCommand}"
                CommandParameter="2"
                Grid.Row="1" Grid.Column="1" />

        <Button Text="3"
                Command="{Binding AddCharCommand}"
                CommandParameter="3"
                Grid.Row="1" Grid.Column="2" />

        <Button Text="4"
                Command="{Binding AddCharCommand}"
                CommandParameter="4"
                Grid.Row="2" Grid.Column="0" />

        <Button Text="5"
                Command="{Binding AddCharCommand}"
                CommandParameter="5"
                Grid.Row="2" Grid.Column="1" />

        <Button Text="6"
                Command="{Binding AddCharCommand}"
                CommandParameter="6"
                Grid.Row="2" Grid.Column="2" />

        <Button Text="7"
                Command="{Binding AddCharCommand}"
                CommandParameter="7"
                Grid.Row="3" Grid.Column="0" />

        <Button Text="8"
                Command="{Binding AddCharCommand}"
                CommandParameter="8"
                Grid.Row="3" Grid.Column="1" />

        <Button Text="9"
                Command="{Binding AddCharCommand}"
                CommandParameter="9"
                Grid.Row="3" Grid.Column="2" />

        <Button Text="*"
                Command="{Binding AddCharCommand}"
                CommandParameter="*"
                Grid.Row="4" Grid.Column="0" />

        <Button Text="0"
                Command="{Binding AddCharCommand}"
                CommandParameter="0"
                Grid.Row="4" Grid.Column="1" />

        <Button Text="#"
                Command="{Binding AddCharCommand}"
                CommandParameter="#"
                Grid.Row="4" Grid.Column="2" />
    </Grid>
</ContentPage>

La propiedad Command del primer Button que aparece en este marcado está enlazada a DeleteCharCommand; el resto se enlaza a AddCharCommand con un CommandParameter que es el mismo que el carácter que aparece en la cara del Button. Este es el programa en acción:

Calculator using MVVM and Commands

Invocar métodos asincrónicos

Los comandos también pueden invocar métodos asincrónicos. Esto se logra mediante el uso de las palabras clave async y await al especificar el método Execute:

DownloadCommand = new Command (async () => await DownloadAsync ());

Esto indica que el método DownloadAsync es Task y se debe esperar:

async Task DownloadAsync ()
{
    await Task.Run (() => Download ());
}

void Download ()
{
    ...
}

Implementar un menú de navegación

El programa XamlSamples que contiene todo el código fuente de esta serie de artículos usa un Modelo de vista para su página principal. El Modelo de vista es una definición de una clase corta con tres propiedades denominadas Type, Title y Description que contienen el tipo de cada una de las páginas de ejemplo, un título y una descripción breve. Además, el Modelo de vista define una propiedad estática denominada All que es una colección de todas las páginas del programa:

public class PageDataViewModel
{
    public PageDataViewModel(Type type, string title, string description)
    {
        Type = type;
        Title = title;
        Description = description;
    }

    public Type Type { private set; get; }

    public string Title { private set; get; }

    public string Description { private set; get; }

    static PageDataViewModel()
    {
        All = new List<PageDataViewModel>
        {
            // Part 1. Getting Started with XAML
            new PageDataViewModel(typeof(HelloXamlPage), "Hello, XAML",
                                  "Display a Label with many properties set"),

            new PageDataViewModel(typeof(XamlPlusCodePage), "XAML + Code",
                                  "Interact with a Slider and Button"),

            // Part 2. Essential XAML Syntax
            new PageDataViewModel(typeof(GridDemoPage), "Grid Demo",
                                  "Explore XAML syntax with the Grid"),

            new PageDataViewModel(typeof(AbsoluteDemoPage), "Absolute Demo",
                                  "Explore XAML syntax with AbsoluteLayout"),

            // Part 3. XAML Markup Extensions
            new PageDataViewModel(typeof(SharedResourcesPage), "Shared Resources",
                                  "Using resource dictionaries to share resources"),

            new PageDataViewModel(typeof(StaticConstantsPage), "Static Constants",
                                  "Using the x:Static markup extensions"),

            new PageDataViewModel(typeof(RelativeLayoutPage), "Relative Layout",
                                  "Explore XAML markup extensions"),

            // Part 4. Data Binding Basics
            new PageDataViewModel(typeof(SliderBindingsPage), "Slider Bindings",
                                  "Bind properties of two views on the page"),

            new PageDataViewModel(typeof(SliderTransformsPage), "Slider Transforms",
                                  "Use Sliders with reverse bindings"),

            new PageDataViewModel(typeof(ListViewDemoPage), "ListView Demo",
                                  "Use a ListView with data bindings"),

            // Part 5. From Data Bindings to MVVM
            new PageDataViewModel(typeof(OneShotDateTimePage), "One-Shot DateTime",
                                  "Obtain the current DateTime and display it"),

            new PageDataViewModel(typeof(ClockPage), "Clock",
                                  "Dynamically display the current time"),

            new PageDataViewModel(typeof(HslColorScrollPage), "HSL Color Scroll",
                                  "Use a view model to select HSL colors"),

            new PageDataViewModel(typeof(KeypadPage), "Keypad",
                                  "Use a view model for numeric keypad logic")
        };
    }

    public static IList<PageDataViewModel> All { private set; get; }
}

El archivo XAML para MainPage define un ListBox cuya propiedad ItemsSource se establece en la propiedad All y que contiene TextCell para mostrar las propiedades Title y Description de cada página:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:XamlSamples"
             x:Class="XamlSamples.MainPage"
             Padding="5, 0"
             Title="XAML Samples">

    <ListView ItemsSource="{x:Static local:PageDataViewModel.All}"
              ItemSelected="OnListViewItemSelected">
        <ListView.ItemTemplate>
            <DataTemplate>
                <TextCell Text="{Binding Title}"
                          Detail="{Binding Description}" />
            </DataTemplate>
        </ListView.ItemTemplate>
    </ListView>
</ContentPage>

Las páginas se muestran en una lista desplazable:

Scrollable list of pages

El controlador del archivo de código subyacente se desencadena cuando el usuario selecciona un elemento. El controlador establece la propiedad SelectedItem de ListBox de nuevo en null y, a continuación, crea una instancia de la página seleccionada y accede a ella:

private async void OnListViewItemSelected(object sender, SelectedItemChangedEventArgs args)
{
    (sender as ListView).SelectedItem = null;

    if (args.SelectedItem != null)
    {
        PageDataViewModel pageData = args.SelectedItem as PageDataViewModel;
        Page page = (Page)Activator.CreateInstance(pageData.Type);
        await Navigation.PushAsync(page);
    }
}

Vídeo

Xamarin Evolve 2016: MVVM simplificado con Xamarin.Forms y Prism

Resumen

XAML es una herramienta eficaz para definir interfaces de usuario en aplicaciones de Xamarin.Forms, especialmente cuando se usan el enlace de datos y MVVM. El resultado es una representación limpia, elegante y potencialmente útil de una interfaz de usuario con toda la compatibilidad en segundo plano en el código.

Encuentre más vídeos de Xamarin en Channel 9 y YouTube.