Parte 3. Extensiones de marcado XAML
Las extensiones de marcado XAML constituyen una característica importante en XAML que permite establecer propiedades en objetos o valores a los que se hace referencia indirectamente desde otros orígenes. Las extensiones de marcado XAML son especialmente importantes para compartir objetos y hacer referencia a constantes usadas en toda una aplicación, pero encuentran su mayor utilidad en los enlaces de datos.
Extensiones de marcado XAML
En general, se usa XAML para establecer propiedades de un objeto en valores explícitos, como una cadena, un número, un miembro de enumeración o una cadena que se convierte en un valor en segundo plano.
A veces, sin embargo, las propiedades deben hacer referencia a valores definidos en otro lugar o que podrían requerir un poco de procesamiento por parte del código en tiempo de ejecución. Para estos fines, existen extensiones de marcado XAML.
Estas extensiones de marcado XAML no son extensiones de XML. XAML es XML completamente legal. Se denominan "extensiones" porque están respaldadas por código en clases que implementan IMarkupExtension
. Puede escribir sus propias extensiones de marcado personalizadas.
En muchos casos, las extensiones de marcado XAML se reconocen instantáneamente en los archivos XAML porque aparecen como valores de atributo delimitados por llaves: { y }, pero a veces las extensiones de marcado aparecen en el marcado como elementos convencionales.
Recursos compartidos
Algunas páginas XAML contienen varias vistas con propiedades establecidas en los mismos valores. Por ejemplo, gran parte de la configuración de las propiedades de estos objetos Button
es la misma:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SharedResourcesPage"
Title="Shared Resources Page">
<StackLayout>
<Button Text="Do this!"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
BorderWidth="3"
Rotation="-15"
TextColor="Red"
FontSize="24" />
<Button Text="Do that!"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
BorderWidth="3"
Rotation="-15"
TextColor="Red"
FontSize="24" />
<Button Text="Do the other thing!"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
BorderWidth="3"
Rotation="-15"
TextColor="Red"
FontSize="24" />
</StackLayout>
</ContentPage>
Si es necesario cambiar una de estas propiedades, puede que prefieras hacer el cambio una sola vez en lugar de tres veces. Si fuera código, es probable que estuvieras usando constantes y objetos estáticos de solo lectura para ayudar a mantener estos valores coherentes y fáciles de modificar.
En XAML, una solución popular es almacenar estos valores u objetos en un diccionario de recursos. La clase VisualElement
define una propiedad llamada Resources
de tipo ResourceDictionary
, que es un diccionario con claves de tipo string
y valores de tipo object
. Puedes colocar objetos en este diccionario y, después, hacer referencia a ellos desde el marcado, todo en XAML.
Para usar un diccionario de recursos en una página, incluya un par de etiquetas elemento-propiedad Resources
. Es más conveniente colocarlo en la parte superior de la página:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SharedResourcesPage"
Title="Shared Resources Page">
<ContentPage.Resources>
</ContentPage.Resources>
...
</ContentPage>
También es necesario incluir explícitamente etiquetas ResourceDictionary
:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SharedResourcesPage"
Title="Shared Resources Page">
<ContentPage.Resources>
<ResourceDictionary>
</ResourceDictionary>
</ContentPage.Resources>
...
</ContentPage>
Ahora se pueden agregar objetos y valores de varios tipos al diccionario de recursos. Estos tipos deben poder instanciarse. No pueden ser clases abstractas, por ejemplo. Estos tipos también deben tener un constructor sin parámetros público. Cada elemento requiere una clave de diccionario especificada con el atributo x:Key
. Por ejemplo:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SharedResourcesPage"
Title="Shared Resources Page">
<ContentPage.Resources>
<ResourceDictionary>
<LayoutOptions x:Key="horzOptions"
Alignment="Center" />
<LayoutOptions x:Key="vertOptions"
Alignment="Center"
Expands="True" />
</ResourceDictionary>
</ContentPage.Resources>
...
</ContentPage>
Estos dos elementos son valores del tipo de estructura LayoutOptions
y cada uno tiene una clave única y una o dos propiedades establecidas. En el código y el marcado, es mucho más común usar los campos estáticos de LayoutOptions
, pero aquí es más cómodo establecer las propiedades.
Ahora es necesario establecer las propiedades HorizontalOptions
y VerticalOptions
de estos botones en estos recursos, lo que se realiza con la extensión de marcado XAML StaticResource
:
<Button Text="Do this!"
HorizontalOptions="{StaticResource horzOptions}"
VerticalOptions="{StaticResource vertOptions}"
BorderWidth="3"
Rotation="-15"
TextColor="Red"
FontSize="24" />
La extensión de marcado StaticResource
siempre está delimitada con llaves e incluye la clave de diccionario.
El nombre StaticResource
lo distingue de DynamicResource
, que también admite Xamarin.Forms. DynamicResource
es para las claves de diccionario asociadas a valores que pueden cambiar durante el tiempo de ejecución, mientras que StaticResource
accede a los elementos del diccionario solo una vez cuando se construyen los elementos de la página.
Para la propiedad BorderWidth
, es necesario almacenar un doble en el diccionario. XAML define cómodamente etiquetas para tipos de datos comunes como x:Double
y x:Int32
:
<ContentPage.Resources>
<ResourceDictionary>
<LayoutOptions x:Key="horzOptions"
Alignment="Center" />
<LayoutOptions x:Key="vertOptions"
Alignment="Center"
Expands="True" />
<x:Double x:Key="borderWidth">
3
</x:Double>
</ResourceDictionary>
</ContentPage.Resources>
No es necesario ponerlo en tres líneas. Esta entrada de diccionario para este ángulo de rotación solo ocupa una línea:
<ContentPage.Resources>
<ResourceDictionary>
<LayoutOptions x:Key="horzOptions"
Alignment="Center" />
<LayoutOptions x:Key="vertOptions"
Alignment="Center"
Expands="True" />
<x:Double x:Key="borderWidth">
3
</x:Double>
<x:Double x:Key="rotationAngle">-15</x:Double>
</ResourceDictionary>
</ContentPage.Resources>
Se puede hacer referencia a estos dos recursos de la misma manera que los valores LayoutOptions
:
<Button Text="Do this!"
HorizontalOptions="{StaticResource horzOptions}"
VerticalOptions="{StaticResource vertOptions}"
BorderWidth="{StaticResource borderWidth}"
Rotation="{StaticResource rotationAngle}"
TextColor="Red"
FontSize="24" />
En el caso de los recursos de tipo Color
, puedes usar las mismas representaciones de cadena que se usan al asignar directamente atributos de estos tipos. Los convertidores de tipos se invocan cuando se crea el recurso. Este es un recurso de tipo Color
:
<Color x:Key="textColor">Red</Color>
A menudo, los programas establecen una propiedad FontSize
en un miembro de la enumeración NamedSize
, como Large
. La clase FontSizeConverter
funciona en segundo plano para convertirlo en un valor dependiente de la plataforma mediante el método Device.GetNamedSized
. Sin embargo, al definir un recurso de tamaño de fuente, tiene más sentido usar un valor numérico, que se muestra aquí como un tipo x:Double
:
<x:Double x:Key="fontSize">24</x:Double>
Ahora todas las propiedades excepto Text
las definen los valores de recursos:
<Button Text="Do this!"
HorizontalOptions="{StaticResource horzOptions}"
VerticalOptions="{StaticResource vertOptions}"
BorderWidth="{StaticResource borderWidth}"
Rotation="{StaticResource rotationAngle}"
TextColor="{StaticResource textColor}"
FontSize="{StaticResource fontSize}" />
También es posible usar OnPlatform
dentro del diccionario de recursos para definir valores diferentes para las plataformas. Este es el modo en que un objeto OnPlatform
puede formar parte del diccionario de recursos para diferentes colores de texto:
<OnPlatform x:Key="textColor"
x:TypeArguments="Color">
<On Platform="iOS" Value="Red" />
<On Platform="Android" Value="Aqua" />
<On Platform="UWP" Value="#80FF80" />
</OnPlatform>
Observe que OnPlatform
obtiene un atributo x:Key
porque es un objeto del diccionario y un atributo x:TypeArguments
porque es una clase genérica. Los atributos iOS
, Android
y UWP
se convierten en valores Color
cuando se inicializa el objeto.
Este es el archivo XAML completo final con tres botones que acceden a seis valores compartidos:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.SharedResourcesPage"
Title="Shared Resources Page">
<ContentPage.Resources>
<ResourceDictionary>
<LayoutOptions x:Key="horzOptions"
Alignment="Center" />
<LayoutOptions x:Key="vertOptions"
Alignment="Center"
Expands="True" />
<x:Double x:Key="borderWidth">3</x:Double>
<x:Double x:Key="rotationAngle">-15</x:Double>
<OnPlatform x:Key="textColor"
x:TypeArguments="Color">
<On Platform="iOS" Value="Red" />
<On Platform="Android" Value="Aqua" />
<On Platform="UWP" Value="#80FF80" />
</OnPlatform>
<x:Double x:Key="fontSize">24</x:Double>
</ResourceDictionary>
</ContentPage.Resources>
<StackLayout>
<Button Text="Do this!"
HorizontalOptions="{StaticResource horzOptions}"
VerticalOptions="{StaticResource vertOptions}"
BorderWidth="{StaticResource borderWidth}"
Rotation="{StaticResource rotationAngle}"
TextColor="{StaticResource textColor}"
FontSize="{StaticResource fontSize}" />
<Button Text="Do that!"
HorizontalOptions="{StaticResource horzOptions}"
VerticalOptions="{StaticResource vertOptions}"
BorderWidth="{StaticResource borderWidth}"
Rotation="{StaticResource rotationAngle}"
TextColor="{StaticResource textColor}"
FontSize="{StaticResource fontSize}" />
<Button Text="Do the other thing!"
HorizontalOptions="{StaticResource horzOptions}"
VerticalOptions="{StaticResource vertOptions}"
BorderWidth="{StaticResource borderWidth}"
Rotation="{StaticResource rotationAngle}"
TextColor="{StaticResource textColor}"
FontSize="{StaticResource fontSize}" />
</StackLayout>
</ContentPage>
Las capturas de pantalla comprueban el estilo coherente y el estilo dependiente de la plataforma:
Aunque es más común definir la colección Resources
en la parte superior de la página, tenga en cuenta que la propiedad Resources
está definida por VisualElement
y puede tener colecciones Resources
en otros elementos de la página. Por ejemplo, intente agregar una a StackLayout
en este ejemplo:
<StackLayout>
<StackLayout.Resources>
<ResourceDictionary>
<Color x:Key="textColor">Blue</Color>
</ResourceDictionary>
</StackLayout.Resources>
...
</StackLayout>
Descubrirá que el color de texto de los botones es ahora azul. Básicamente, cada vez que el analizador XAML encuentra una extensión de marcado StaticResource
, busca en el árbol visual y usa el primer ResourceDictionary
que encuentra que contiene esa clave.
Uno de los tipos de objetos más comunes almacenados en diccionarios de recursos es Style
de Xamarin.Forms, que define una colección de valores de propiedad. Los estilos se describen en el artículo Estilos.
A veces, los desarrolladores nuevos en XAML se preguntan si pueden colocar un elemento visual como Label
o Button
en un ResourceDictionary
. Aunque seguramente es posible, no tiene mucho sentido. El propósito de ResourceDictionary
es compartir objetos. No se puede compartir un elemento visual. La misma instancia no puede aparecer dos veces en una sola página.
Extensiones de marcado x:Static
A pesar de las similitudes de sus nombres, x:Static
y StaticResource
son muy diferentes. StaticResource
devuelve un objeto de un diccionario de recursos mientras x:Static
accede a uno de los siguientes elementos:
- un campo estático público
- una propiedad estática pública
- un campo de constante público
- un miembro de la enumeración
La extensión de marcado StaticResource
es compatible con las implementaciones de XAML que definen un diccionario de recursos, mientras que x:Static
es una parte intrínseca de XAML, como muestra el prefijo x
.
Estos son algunos ejemplos que muestran cómo x:Static
puede hacer referencia explícitamente a campos estáticos y miembros de enumeración:
<Label Text="Hello, XAML!"
VerticalOptions="{x:Static LayoutOptions.Start}"
HorizontalTextAlignment="{x:Static TextAlignment.Center}"
TextColor="{x:Static Color.Aqua}" />
Hasta ahora, esto no impresiona mucho. Pero la extensión de marcado x:Static
también puede hacer referencia a campos estáticos o propiedades desde su propio código. Por ejemplo, esta es una clase AppConstants
que contiene algunos campos estáticos que se pueden usar en varias páginas en toda una aplicación:
using System;
using Xamarin.Forms;
namespace XamlSamples
{
static class AppConstants
{
public static readonly Thickness PagePadding;
public static readonly Font TitleFont;
public static readonly Color BackgroundColor = Color.Aqua;
public static readonly Color ForegroundColor = Color.Brown;
static AppConstants()
{
switch (Device.RuntimePlatform)
{
case Device.iOS:
PagePadding = new Thickness(5, 20, 5, 0);
TitleFont = Font.SystemFontOfSize(35, FontAttributes.Bold);
break;
case Device.Android:
PagePadding = new Thickness(5, 0, 5, 0);
TitleFont = Font.SystemFontOfSize(40, FontAttributes.Bold);
break;
case Device.UWP:
PagePadding = new Thickness(5, 0, 5, 0);
TitleFont = Font.SystemFontOfSize(50, FontAttributes.Bold);
break;
}
}
}
}
Para hacer referencia a los campos estáticos de esta clase en el archivo XAML, necesitará alguna manera de indicar dentro del archivo XAML donde se encuentra este archivo. Esto se hace con una declaración de espacio de nombres XML.
Recuerde que los archivos XAML creados como parte de la plantilla XAML estándar Xamarin.Forms contienen dos declaraciones de espacio de nombres XML: una para acceder a las clases Xamarin.Forms y otra para hacer referencia a etiquetas y atributos intrínsecos a XAML:
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
Necesitará declaraciones de espacio de nombres XML adicionales para acceder a otras clases. Cada declaración de espacio de nombres XML adicional define un nuevo prefijo. Para acceder a las clases locales de la biblioteca de .NET Standard de la aplicación compartida, como AppConstants
, los programadores de XAML suelen usar el prefijo local
. La declaración de espacio de nombres debe indicar el nombre del espacio de nombres CLR (Common Language Runtime), también conocido como nombre de espacio de nombres .NET, que es el nombre que aparece en una definición de C# namespace
o en una directiva using
:
xmlns:local="clr-namespace:XamlSamples"
También puede definir declaraciones de espacio de nombres XML para espacios de nombres de .NET en cualquier ensamblado al que haga referencia la biblioteca de .NET Standard. Por ejemplo, este es un prefijo sys
para el espacio de nombres estándar de .NET System
, que se encuentra en el ensamblado netstandard. Dado que se trata de otro ensamblado, también debe especificar el nombre del ensamblado, en este caso netstandard:
xmlns:sys="clr-namespace:System;assembly=netstandard"
Observe que la palabra clave clr-namespace
va seguida de dos puntos y, a continuación, el nombre del espacio de nombres de .NET, seguido de un punto y coma, la palabra clave assembly
, un signo igual y el nombre del ensamblado.
Sí, los dos puntos van después de clr-namespace
pero el signo igual va después de assembly
. La sintaxis se definió de esta manera deliberadamente: la mayoría de las declaraciones de espacio de nombres XML hacen referencia a un URI que comienza un nombre de esquema de URI como http
, que siempre va seguido de dos puntos. La parte clr-namespace
de esta cadena está pensada para imitar esa convención.
Ambas declaraciones de espacio de nombres se incluyen en el ejemplo StaticConstantsPage. Observe que las dimensiones BoxView
se establecen en Math.PI
y Math.E
, pero se escalan según un factor de 100:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
xmlns:sys="clr-namespace:System;assembly=netstandard"
x:Class="XamlSamples.StaticConstantsPage"
Title="Static Constants Page"
Padding="{x:Static local:AppConstants.PagePadding}">
<StackLayout>
<Label Text="Hello, XAML!"
TextColor="{x:Static local:AppConstants.BackgroundColor}"
BackgroundColor="{x:Static local:AppConstants.ForegroundColor}"
Font="{x:Static local:AppConstants.TitleFont}"
HorizontalOptions="Center" />
<BoxView WidthRequest="{x:Static sys:Math.PI}"
HeightRequest="{x:Static sys:Math.E}"
Color="{x:Static local:AppConstants.ForegroundColor}"
HorizontalOptions="Center"
VerticalOptions="CenterAndExpand"
Scale="100" />
</StackLayout>
</ContentPage>
El tamaño del BoxView
resultante relativo a la pantalla depende de la plataforma:
Otras extensiones de marcado estándar
Varias extensiones de marcado son intrínsecas a XAML y se admiten en los archivos Xamarin.Forms de XAML. Algunas no se usan con mucha frecuencia, pero son esenciales cuando las necesitas:
- Si una propiedad tiene un valor no
null
de forma predeterminada, pero quieres establecerlo ennull
, establécelo en la extensión de marcado{x:Null}
. - Si una propiedad es de tipo
Type
, puedes asignarla a un objetoType
con la extensión de marcado{x:Type someClass}
. - Puede definir matrices en XAML usando la extensión de marcado
x:Array
. Esta extensión de marcado tiene un atributo obligatorio denominadoType
que indica el tipo de los elementos de la matriz. - La extensión de marcado
Binding
se describe en la parte 4. Conceptos básicos del enlace de datos. - La extensión de marcado
RelativeSource
se describe en Enlaces relativos.
Extensión de marcado ConstraintExpression
Las extensiones de marcado pueden tener propiedades, pero no se establecen como atributos XML. En una extensión de marcado, los valores de propiedad están separados por comas y no aparecen comillas dentro de las llaves.
Esto se puede ilustrar con la extensión de marcado Xamarin.Forms denominada ConstraintExpression
, que se usa con la clase RelativeLayout
. Puede especificar la ubicación o el tamaño de una vista secundaria como una constante, o en relación con una vista primaria u otra vista con nombre. La sintaxis de ConstraintExpression
permite establecer la posición o el tamaño de una vista mediante un Factor
de multiplicación de la propiedad de otra vista, además de una Constant
. Todo lo que sra más complejo que eso requiere código.
A continuación se muestra un ejemplo:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="XamlSamples.RelativeLayoutPage"
Title="RelativeLayout Page">
<RelativeLayout>
<!-- Upper left -->
<BoxView Color="Red"
RelativeLayout.XConstraint=
"{ConstraintExpression Type=Constant,
Constant=0}"
RelativeLayout.YConstraint=
"{ConstraintExpression Type=Constant,
Constant=0}" />
<!-- Upper right -->
<BoxView Color="Green"
RelativeLayout.XConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Width,
Factor=1,
Constant=-40}"
RelativeLayout.YConstraint=
"{ConstraintExpression Type=Constant,
Constant=0}" />
<!-- Lower left -->
<BoxView Color="Blue"
RelativeLayout.XConstraint=
"{ConstraintExpression Type=Constant,
Constant=0}"
RelativeLayout.YConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Height,
Factor=1,
Constant=-40}" />
<!-- Lower right -->
<BoxView Color="Yellow"
RelativeLayout.XConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Width,
Factor=1,
Constant=-40}"
RelativeLayout.YConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Height,
Factor=1,
Constant=-40}" />
<!-- Centered and 1/3 width and height of parent -->
<BoxView x:Name="oneThird"
Color="Red"
RelativeLayout.XConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Width,
Factor=0.33}"
RelativeLayout.YConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Height,
Factor=0.33}"
RelativeLayout.WidthConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Width,
Factor=0.33}"
RelativeLayout.HeightConstraint=
"{ConstraintExpression Type=RelativeToParent,
Property=Height,
Factor=0.33}" />
<!-- 1/3 width and height of previous -->
<BoxView Color="Blue"
RelativeLayout.XConstraint=
"{ConstraintExpression Type=RelativeToView,
ElementName=oneThird,
Property=X}"
RelativeLayout.YConstraint=
"{ConstraintExpression Type=RelativeToView,
ElementName=oneThird,
Property=Y}"
RelativeLayout.WidthConstraint=
"{ConstraintExpression Type=RelativeToView,
ElementName=oneThird,
Property=Width,
Factor=0.33}"
RelativeLayout.HeightConstraint=
"{ConstraintExpression Type=RelativeToView,
ElementName=oneThird,
Property=Height,
Factor=0.33}" />
</RelativeLayout>
</ContentPage>
Quizás la lección más importante que debe tomar de este ejemplo es la sintaxis de la extensión de marcado: no debe aparecer ninguna comilla dentro de las llaves de una extensión de marcado. Al escribir la extensión de marcado en un archivo XAML, es normal que quiera incluir los valores de las propiedades entre comillas. Resista la tentación.
Esta es la ejecución del programa:
Resumen
Las extensiones de marcado XAML que se muestran aquí proporcionan compatibilidad importante con los archivos XAML. Pero quizás la extensión de marcado XAML más valiosa es Binding
, que se trata en la siguiente parte de esta serie, la parte 4. Conceptos básicos del enlace de datos.