Interfaz de comandos de Xamarin.Forms
En la arquitectura Model-View-ViewModel (MVVM), los enlaces de datos se definen entre las propiedades de ViewModel, que suele ser una clase que se deriva de INotifyPropertyChanged
, y las propiedades en la vista, que suele ser el archivo XAML. A veces, una aplicación tiene necesidades que van más allá de estos enlaces de propiedad y solicita al usuario que inicie comandos que influyen en el modelo de vista. Por lo general, estos comandos se señalizan mediante clics de botón o pulsaciones con el dedo, y tradicionalmente se procesan en el archivo de código subyacente en un controlador para el evento Clicked
del elemento Button
o el evento Tapped
de un elemento TapGestureRecognizer
.
La interfaz de comandos proporciona un enfoque alternativo para implementar comandos que se adapta mucho mejor a la arquitectura MVVM. El propio modelo de vista puede contener comandos, que son métodos que se ejecutan en respuesta a una actividad específica en la vista como un clic de Button
. Los enlaces de datos se definen entre estos comandos y el objeto Button
.
Para permitir un enlace de datos entre un objeto Button
y un ViewModel, el objeto Button
define dos propiedades:
Command
de tipoSystem.Windows.Input.ICommand
CommandParameter
de tipoObject
Para usar la interfaz de comandos, defina un enlace de datos que tenga como destino la propiedad Command
del objeto Button
donde el origen sea una propiedad en el modelo de vista de tipo ICommand
. El modelo de vista contiene código asociado con la propiedad ICommand
que se ejecuta cuando se hace clic en el botón. Puede establecer CommandParameter
en datos arbitrarios para distinguir entre varios botones si todos se enlazan a la misma propiedad ICommand
en el modelo de vista.
Las propiedades Command
y CommandParameter
también se definen mediante las clases siguientes:
MenuItem
y, por tanto,ToolbarItem
, que se deriva deMenuItem
TextCell
y, por tanto,ImageCell
, que se deriva deTextCell
TapGestureRecognizer
SearchBar
define una propiedad SearchCommand
de tipo ICommand
y una propiedad SearchCommandParameter
. La propiedad RefreshCommand
de ListView
también es de tipo ICommand
.
Todos estos comandos se pueden controlar en un modelo de vista de forma que no dependa del objeto de interfaz de usuario concreto de la vista.
La interfaz ICommand
La interfaz System.Windows.Input.ICommand
no forma parte de Xamarin.Forms. En su lugar, se define en el espacio de nombres System.Windows.Input y consta de dos métodos y un evento:
public interface ICommand
{
public void Execute (Object parameter);
public bool CanExecute (Object parameter);
public event EventHandler CanExecuteChanged;
}
Para usar la interfaz de comandos, el modelo de vista contiene propiedades de tipo ICommand
:
public ICommand MyCommand { private set; get; }
El modelo de vista también debe hacer referencia a una clase que implemente la interfaz ICommand
. Esta clase se describirá en breve. En la vista, la propiedad Command
de un objeto Button
está enlazada a esa propiedad:
<Button Text="Execute command"
Command="{Binding MyCommand}" />
Cuando el usuario presiona el elemento Button
, Button
llama al método Execute
del objeto ICommand
enlazado a su propiedad Command
. Es la parte más sencilla de la interfaz de comandos.
El método CanExecute
es más complejo. Cuando se define por primera vez el enlace en la propiedad Command
del objeto Button
, y cuando cambia de algún modo el enlace de datos, el objeto Button
llama al método CanExecute
del objeto ICommand
. Si CanExecute
devuelve false
, el objeto Button
se deshabilita a sí mismo. Esto indica que el comando concreto no está disponible actualmente o no es válido.
El objeto Button
también adjunta un controlador al evento CanExecuteChanged
de ICommand
. El evento se desencadena desde dentro del modelo de vista. Cuando se desencadena ese evento, el objeto Button
vuelve a llamar a CanExecute
. El objeto Button
se habilita a sí mismo si CanExecute
devuelve true
y se deshabilita si CanExecute
devuelve false
.
Importante
No use la propiedad IsEnabled
de Button
si usa la interfaz de comandos.
La clase Command
Cuando en el modelo de vista se define una propiedad de tipo ICommand
, el modelo de vista también debe contener o hacer referencia a una clase que implemente la interfaz ICommand
. Esta clase debe contener o hacer referencia a los métodos Execute
y CanExecute
, y desencadenar el evento CanExecuteChanged
cada vez que el método CanExecute
pueda devolver otro valor.
Puede escribir este tipo de clase, o bien puede usar una escrita por otra persona. Como ICommand
forma parte de Microsoft Windows, se ha usado durante años con las aplicaciones MVVM de Windows. El uso de una clase de Windows que implementa ICommand
permite compartir los modelos de vista entre las aplicaciones de Windows y las de Xamarin.Forms.
Si el uso compartido de modelos de vista entre Windows y Xamarin.Forms no constituye un problema, puede usar la clase Command
o Command<T>
incluida en Xamarin.Forms para implementar la interfaz ICommand
. Estas clases permiten especificar los cuerpos de los métodos Execute
y CanExecute
en los constructores de clase. Use Command<T>
cuando use la propiedad CommandParameter
para distinguir entre varias vistas enlazadas a la misma propiedad ICommand
, y la clase Command
más sencilla cuando no sea un requisito.
Comandos básicos
En la página Entrada de personas del programa de ejemplo se muestran algunos comandos sencillos implementados en un modelo de vista.
PersonViewModel
define tres propiedades denominadas Name
, Age
y Skills
que definen una persona. Esta clase no contiene ninguna propiedad ICommand
:
public class PersonViewModel : INotifyPropertyChanged
{
string name;
double age;
string skills;
public event PropertyChangedEventHandler PropertyChanged;
public string Name
{
set { SetProperty(ref name, value); }
get { return name; }
}
public double Age
{
set { SetProperty(ref age, value); }
get { return age; }
}
public string Skills
{
set { SetProperty(ref skills, value); }
get { return skills; }
}
public override string ToString()
{
return Name + ", age " + Age;
}
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
El elemento PersonCollectionViewModel
que se muestra a continuación crea objetos de tipo PersonViewModel
y permite al usuario rellenar los datos. Para ello, la clase define las propiedades IsEditing
de tipo bool
y PersonEdit
de tipo PersonViewModel
. Además, la clase define tres propiedades de tipo ICommand
y una propiedad denominada Persons
de tipo IList<PersonViewModel>
:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
PersonViewModel personEdit;
bool isEditing;
public event PropertyChangedEventHandler PropertyChanged;
···
public bool IsEditing
{
private set { SetProperty(ref isEditing, value); }
get { return isEditing; }
}
public PersonViewModel PersonEdit
{
set { SetProperty(ref personEdit, value); }
get { return personEdit; }
}
public ICommand NewCommand { private set; get; }
public ICommand SubmitCommand { private set; get; }
public ICommand CancelCommand { private set; get; }
public IList<PersonViewModel> Persons { get; } = new ObservableCollection<PersonViewModel>();
bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
if (Object.Equals(storage, value))
return false;
storage = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Esta lista abreviada no incluye el constructor de la clase, que es donde se definen las tres propiedades de tipo ICommand
, que se mostrará en breve. Tenga en cuenta que los cambios en las tres propiedades de tipo ICommand
y la propiedad Persons
no hacen que se desencadenen eventos PropertyChanged
. Estas propiedades se establecen al crear la clase por primera vez y no cambian a partir de entonces.
Antes de examinar el constructor de la clase PersonCollectionViewModel
, veremos los archivos XAML para el programa Entrada de personas. Este archivo contiene un elemento Grid
con su propiedad BindingContext
establecida en el elemento PersonCollectionViewModel
. El objeto Grid
contiene un objeto Button
con el texto New con su propiedad Command
enlazada a la propiedad NewCommand
del modelo de vista, un formulario de entrada con propiedades enlazadas a la propiedad IsEditing
, así como las propiedades de PersonViewModel
, y dos botones más enlazados a las propiedades SubmitCommand
y CancelCommand
de la clase ViewModel. El objeto ListView
final muestra la colección de las personas que ya se han introducido:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.PersonEntryPage"
Title="Person Entry">
<Grid Margin="10">
<Grid.BindingContext>
<local:PersonCollectionViewModel />
</Grid.BindingContext>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- New Button -->
<Button Text="New"
Grid.Row="0"
Command="{Binding NewCommand}"
HorizontalOptions="Start" />
<!-- Entry Form -->
<Grid Grid.Row="1"
IsEnabled="{Binding IsEditing}">
<Grid BindingContext="{Binding PersonEdit}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Label Text="Name: " Grid.Row="0" Grid.Column="0" />
<Entry Text="{Binding Name}"
Grid.Row="0" Grid.Column="1" />
<Label Text="Age: " Grid.Row="1" Grid.Column="0" />
<StackLayout Orientation="Horizontal"
Grid.Row="1" Grid.Column="1">
<Stepper Value="{Binding Age}"
Maximum="100" />
<Label Text="{Binding Age, StringFormat='{0} years old'}"
VerticalOptions="Center" />
</StackLayout>
<Label Text="Skills: " Grid.Row="2" Grid.Column="0" />
<Entry Text="{Binding Skills}"
Grid.Row="2" Grid.Column="1" />
</Grid>
</Grid>
<!-- Submit and Cancel Buttons -->
<Grid Grid.Row="2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Text="Submit"
Grid.Column="0"
Command="{Binding SubmitCommand}"
VerticalOptions="CenterAndExpand" />
<Button Text="Cancel"
Grid.Column="1"
Command="{Binding CancelCommand}"
VerticalOptions="CenterAndExpand" />
</Grid>
<!-- List of Persons -->
<ListView Grid.Row="3"
ItemsSource="{Binding Persons}" />
</Grid>
</ContentPage>
Este es el funcionamiento: el usuario presiona primero el botón New (Nuevo). Esto habilita el formulario de entrada, pero deshabilita el botón New. Después, el usuario escribe un nombre, la edad y las habilidades. En cualquier momento durante la edición, el usuario puede presionar el botón Cancel (Cancelar) para volver a empezar. El botón Submit (Enviar) solo se habilita cuando se ha escrito un nombre y una edad válida. Al presionar este botón Submit, se transfiere a la persona a la colección mostrada por ListView
. Después de presionar el botón Cancel o Submit, se borra el formulario de entrada y se vuelve a habilitar el botón New.
En la pantalla de iOS de la izquierda se muestra el diseño antes de escribir una vigencia válida. En la pantallas de Android se muestra el botón Enviar habilitado después de haber establecido una edad:
El programa no tiene ninguna función para editar las entradas existentes y no guarda las entradas al salir de la página.
Toda la lógica para los botones New, Submit y Cancel se controla en PersonCollectionViewModel
a través de definiciones de las propiedades NewCommand
, SubmitCommand
y CancelCommand
. El constructor de PersonCollectionViewModel
establece estas tres propiedades en objetos de tipo Command
.
Un constructor de la clase Command
permite pasar argumentos de tipo Action
y Func<bool>
correspondientes a los métodos Execute
y CanExecute
. Es más fácil definir estas acciones y funciones como funciones lambda directamente en el constructor de Command
. Esta es la definición del objeto Command
para la propiedad NewCommand
:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
NewCommand = new Command(
execute: () =>
{
PersonEdit = new PersonViewModel();
PersonEdit.PropertyChanged += OnPersonEditPropertyChanged;
IsEditing = true;
RefreshCanExecutes();
},
canExecute: () =>
{
return !IsEditing;
});
···
}
void OnPersonEditPropertyChanged(object sender, PropertyChangedEventArgs args)
{
(SubmitCommand as Command).ChangeCanExecute();
}
void RefreshCanExecutes()
{
(NewCommand as Command).ChangeCanExecute();
(SubmitCommand as Command).ChangeCanExecute();
(CancelCommand as Command).ChangeCanExecute();
}
···
}
Cuando el usuario hace clic en el botón New, se ejecuta la función execute
pasada al constructor de Command
. Esto crea un objeto PersonViewModel
, establece un controlador en el evento PropertyChanged
de ese objeto, establece IsEditing
en true
, y llama al método RefreshCanExecutes
definido después del constructor.
Además de implementar la interfaz ICommand
, la clase Command
también define un método denominado ChangeCanExecute
. El modelo de vista debe llamar a ChangeCanExecute
para una propiedad ICommand
cada vez que suceda algo que pueda cambiar el valor devuelto del método CanExecute
. Una llamada a ChangeCanExecute
hace que la clase Command
desencadene el método CanExecuteChanged
. El objeto Button
ha adjuntado un controlador para ese evento y responde mediante una nueva llamada a CanExecute
y, después, se habilita a sí mismo en función del valor devuelto de ese método.
Cuando el método execute
de NewCommand
llama a RefreshCanExecutes
, la propiedad NewCommand
recibe una llamada a ChangeCanExecute
, y Button
llama al método canExecute
, que ahora devuelve false
porque la propiedad IsEditing
es true
.
El controlador PropertyChanged
para el nuevo objeto PersonViewModel
llama al método ChangeCanExecute
de SubmitCommand
. Aquí se muestra la implementación de esa propiedad de comando:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
SubmitCommand = new Command(
execute: () =>
{
Persons.Add(PersonEdit);
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return PersonEdit != null &&
PersonEdit.Name != null &&
PersonEdit.Name.Length > 1 &&
PersonEdit.Age > 0;
});
···
}
···
}
La función canExecute
para SubmitCommand
se llama cada vez que cambia una propiedad en el objeto PersonViewModel
que se está editando. Solo devuelve true
cuando la propiedad Name
tiene al menos un carácter de longitud, y Age
es mayor que 0. En ese momento, se habilita el botón Submit.
La función execute
para Submit quita el controlador de cambio de propiedad de PersonViewModel
, agrega el objeto a la colección Persons
y devuelve todo a las condiciones iniciales.
La función execute
para el botón Cancel hace lo mismo que el botón Submit excepto agregar el objeto a la colección:
public class PersonCollectionViewModel : INotifyPropertyChanged
{
···
public PersonCollectionViewModel()
{
···
CancelCommand = new Command(
execute: () =>
{
PersonEdit.PropertyChanged -= OnPersonEditPropertyChanged;
PersonEdit = null;
IsEditing = false;
RefreshCanExecutes();
},
canExecute: () =>
{
return IsEditing;
});
}
···
}
El método canExecute
devuelve true
en cualquier momento que se modifique un elemento PersonViewModel
.
Estas técnicas se podrían adaptar para escenarios más complejos: una propiedad de PersonCollectionViewModel
se podría enlazar a la propiedad SelectedItem
del objeto ListView
para editar los elementos existentes, y se podría agregar un botón Delete para eliminar esos elementos.
No es necesario definir los métodos execute
y canExecute
como funciones lambda. Puede escribirlos como métodos privados estándar en el modelo de vista y hacer referencia a ellos en constructores de Command
. Pero este enfoque tiende a crear una gran cantidad de métodos a los que solo se hace referencia una vez en el modelo de vista.
Uso de parámetros de comando
A veces es conveniente que uno o varios botones (u otros objetos de interfaz de usuario) compartan la misma propiedad ICommand
en el modelo de vista. En este caso, se usa la propiedad CommandParameter
para distinguir los botones.
Se puede seguir usando la clase Command
para estas propiedades ICommand
compartidas. La clase define un constructor alternativo que acepta métodos execute
y canExecute
con parámetros de tipo Object
. Esta es la forma de pasar CommandParameter
a estos métodos.
Pero cuando se usa CommandParameter
, resulta más fácil utilizar la clase Command<T>
genérica para especificar el tipo del objeto establecido en CommandParameter
. Los métodos execute
y canExecute
que especifique tienen parámetros de ese tipo.
En la página Teclado decimal se ilustra esta técnica y se muestra cómo implementar un teclado numérico para escribir números decimales. El elemento BindingContext
para el objeto Grid
es un elemento DecimalKeypadViewModel
. La propiedad Entry
de este modelo de vista se enlaza a la propiedad Text
de un elemento Label
. Todos los objetos Button
están enlazados a varios comandos del modelo de vista: ClearCommand
, BackspaceCommand
y DigitCommand
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.DecimalKeypadPage"
Title="Decimal Keyboard">
<Grid WidthRequest="240"
HeightRequest="480"
ColumnSpacing="2"
RowSpacing="2"
HorizontalOptions="Center"
VerticalOptions="Center">
<Grid.BindingContext>
<local:DecimalKeypadViewModel />
</Grid.BindingContext>
<Grid.Resources>
<ResourceDictionary>
<Style TargetType="Button">
<Setter Property="FontSize" Value="32" />
<Setter Property="BorderWidth" Value="1" />
<Setter Property="BorderColor" Value="Black" />
</Style>
</ResourceDictionary>
</Grid.Resources>
<Label Text="{Binding Entry}"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3"
FontSize="32"
LineBreakMode="HeadTruncation"
VerticalTextAlignment="Center"
HorizontalTextAlignment="End" />
<Button Text="CLEAR"
Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding ClearCommand}" />
<Button Text="⇦"
Grid.Row="1" Grid.Column="2"
Command="{Binding BackspaceCommand}" />
<Button Text="7"
Grid.Row="2" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="7" />
<Button Text="8"
Grid.Row="2" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="8" />
<Button Text="9"
Grid.Row="2" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="9" />
<Button Text="4"
Grid.Row="3" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="4" />
<Button Text="5"
Grid.Row="3" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="5" />
<Button Text="6"
Grid.Row="3" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="6" />
<Button Text="1"
Grid.Row="4" Grid.Column="0"
Command="{Binding DigitCommand}"
CommandParameter="1" />
<Button Text="2"
Grid.Row="4" Grid.Column="1"
Command="{Binding DigitCommand}"
CommandParameter="2" />
<Button Text="3"
Grid.Row="4" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="3" />
<Button Text="0"
Grid.Row="5" Grid.Column="0" Grid.ColumnSpan="2"
Command="{Binding DigitCommand}"
CommandParameter="0" />
<Button Text="·"
Grid.Row="5" Grid.Column="2"
Command="{Binding DigitCommand}"
CommandParameter="." />
</Grid>
</ContentPage>
Los 11 botones para los 10 dígitos y el separador decimal comparten un enlace a DigitCommand
. CommandParameter
distingue entre estos botones. El valor establecido en CommandParameter
suele ser el mismo que el texto que se muestra en el botón, excepto por el separador decimal que, para una mayor claridad, se muestra con un carácter de punto central.
Este es el programa en acción:
Observe que el botón para el separador decimal en las tres capturas de pantalla está deshabilitado porque el número introducido ya contiene un separador decimal.
DecimalKeypadViewModel
define una propiedad Entry
de tipo string
(que es la única que desencadena un evento PropertyChanged
) y tres propiedades de tipo ICommand
:
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
string entry = "0";
public event PropertyChangedEventHandler PropertyChanged;
···
public string Entry
{
private set
{
if (entry != value)
{
entry = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Entry"));
}
}
get
{
return entry;
}
}
public ICommand ClearCommand { private set; get; }
public ICommand BackspaceCommand { private set; get; }
public ICommand DigitCommand { private set; get; }
}
El botón correspondiente a ClearCommand
siempre está habilitado y simplemente establece la entrada en "0":
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
ClearCommand = new Command(
execute: () =>
{
Entry = "0";
RefreshCanExecutes();
});
···
}
void RefreshCanExecutes()
{
((Command)BackspaceCommand).ChangeCanExecute();
((Command)DigitCommand).ChangeCanExecute();
}
···
}
Como el botón siempre está habilitado, no es necesario especificar un argumento canExecute
en el constructor de Command
.
La lógica para escribir números y el retroceso es un poco complicada, porque si no se ha especificado ningún dígito, la propiedad Entry
es la cadena "0". Si el usuario escribe más ceros, Entry
todavía contiene un solo cero. Si el usuario escribe cualquier otro dígito, ese dígito reemplaza al cero. Pero si el usuario escribe un separador decimal antes de cualquier otro dígito, Entry
es la cadena "0.".
El botón Backspace (Retroceso) solo se habilita cuando la longitud de la entrada es mayor que 1, o bien si Entry
no es igual a la cadena "0":
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
BackspaceCommand = new Command(
execute: () =>
{
Entry = Entry.Substring(0, Entry.Length - 1);
if (Entry == "")
{
Entry = "0";
}
RefreshCanExecutes();
},
canExecute: () =>
{
return Entry.Length > 1 || Entry != "0";
});
···
}
···
}
La lógica para la función execute
para el botón Backspace garantiza que Entry
es al menos una cadena de "0".
La propiedad DigitCommand
está enlazada a 11 botones, que se identifican a sí mismos con la propiedad CommandParameter
de forma individual. Se podría establecer DigitCommand
en una instancia de la clase Command
normal, pero es más fácil usar la clase Command<T>
genérica. Cuando se usa la interfaz de comandos con XAML, las propiedades CommandParameter
suelen ser cadenas, y ese es el tipo del argumento genérico. Las funciones execute
y canExecute
tienen argumentos de tipo string
:
public class DecimalKeypadViewModel : INotifyPropertyChanged
{
···
public DecimalKeypadViewModel()
{
···
DigitCommand = new Command<string>(
execute: (string arg) =>
{
Entry += arg;
if (Entry.StartsWith("0") && !Entry.StartsWith("0."))
{
Entry = Entry.Substring(1);
}
RefreshCanExecutes();
},
canExecute: (string arg) =>
{
return !(arg == "." && Entry.Contains("."));
});
}
···
}
El método execute
anexa el argumento de cadena a la propiedad Entry
. Pero si el resultado comienza con un cero (pero no un cero y un separador decimal), ese cero inicial se debe quitar mediante la función Substring
.
El método canExecute
devuelve false
solo si el argumento es el separador decimal (lo que indica que se presiona el separador decimal) y Entry
ya contiene un separador decimal.
Todos los métodos execute
llaman a RefreshCanExecutes
, que llama a ChangeCanExecute
para DigitCommand
y ClearCommand
. Esto garantiza que los botones de retroceso y separador decimal se habilitan o deshabilitan en función de la secuencia actual de dígitos especificados.
Comandos asincrónicos para menús de navegación
El comando es cómodo para implementar menús de navegación. Este es un fragmento de MainPage.xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:DataBindingDemos"
x:Class="DataBindingDemos.MainPage"
Title="Data Binding Demos"
Padding="10">
<TableView Intent="Menu">
<TableRoot>
<TableSection Title="Basic Bindings">
<TextCell Text="Basic Code Binding"
Detail="Define a data-binding in code"
Command="{Binding NavigateCommand}"
CommandParameter="{x:Type local:BasicCodeBindingPage}" />
<TextCell Text="Basic XAML Binding"
Detail="Define a data-binding in XAML"
Command="{Binding NavigateCommand}"
CommandParameter="{x:Type local:BasicXamlBindingPage}" />
<TextCell Text="Alternative Code Binding"
Detail="Define a data-binding in code without a BindingContext"
Command="{Binding NavigateCommand}"
CommandParameter="{x:Type local:AlternativeCodeBindingPage}" />
···
</TableSection>
</TableRoot>
</TableView>
</ContentPage>
Al usar comandos con XAML, las propiedades CommandParameter
normalmente se establecen en cadenas. Pero en este caso, se usa una extensión de marcado XAML para que CommandParameter
sea de tipo System.Type
.
Cada propiedad Command
se enlaza a una propiedad denominada NavigateCommand
. Esa propiedad se define en el archivo de código subyacente, MainPage.xaml.cs:
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
NavigateCommand = new Command<Type>(
async (Type pageType) =>
{
Page page = (Page)Activator.CreateInstance(pageType);
await Navigation.PushAsync(page);
});
BindingContext = this;
}
public ICommand NavigateCommand { private set; get; }
}
El constructor establece la propiedad NavigateCommand
en un método execute
que crea una instancia del parámetro System.Type
y, después, navega hasta ella. Como la llamada a PushAsync
requiere un operador await
, el método execute
debe estar marcado como asincrónico. Esto se consigue con la palabra clave async
por delante de la lista de parámetros.
El constructor establece también el elemento BindingContext
de la página en sí mismo para que los enlaces hagan referencia a NavigateCommand
en esta clase.
El orden del código de este constructor establece una diferencia: la llamada a InitializeComponent
hace que se analice el código XAML, pero en ese momento, el enlace a una propiedad denominada NavigateCommand
no se puede resolver porque BindingContext
está establecido en null
. Si se establece BindingContext
en el constructor antes de establecer NavigateCommand
, el enlace se puede resolver cuando se establezca BindingContext
, pero en ese momento, NavigateCommand
sigue siendo null
. Establecer NavigateCommand
después de BindingContext
no tendrá ningún efecto en el enlace porque un cambio en NavigateCommand
no desencadena un evento PropertyChanged
y el enlace no sabe que NavigateCommand
ahora es válido.
El establecimiento de NavigateCommand
y BindingContext
(en cualquier orden) antes de llamar a InitializeComponent
funcionará porque los dos componentes del enlace se establecen cuando el analizador XAML encuentra la definición de enlace.
En ocasiones, los enlaces de datos pueden resultar complicados, pero como ha visto en esta serie de artículos, son eficaces y versátiles, y ayudan considerablemente a organizar el código mediante la separación de la lógica subyacente de la interfaz de usuario.