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:

Controles con estilo

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:

Controles que usan la extensión de marcado x:Static

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 en null, establécelo en la extensión de marcado {x:Null}.
  • Si una propiedad es de tipo Type, puedes asignarla a un objeto Type 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 denominado Type 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:

Diseño relativo mediante restricciones

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.