Control templates en Xamarin Forms

  • xamarin-forms

Gracias a las control templates, en Xamarin Forms, podemos cambiar el aspecto de ContentViews y ContentPages en tiempo de ejecución. De esta manera, podemos tener distintas visualizaciones de un mismo control o una misma página, sin que eso suponga tirar muchas líneas de código o crear complejos algoritmos con los que decidir qué aspecto dar a un elemento.

Hay que tener cuidado, ya que las plantillas hay que utilizarlas cuando queramos visualizaciones distintas que impliquen el uso de controles diferentes o cambios en la disposición de éstos en la pantalla. Ya que podemos caer en la tentación de usar ControlTemplates en situaciones en las que podemos usar estilos.

ControlTemplate en un ContentView

Para ver el funcionamiento de este caso, vamos a ver un ejemplo. Partimos del siguiente control InfoView, para el que no se ha definido interfaz, ya que la definiremos usando ControlTemplates

public class InfoView : ContentView
{
    public static readonly BindableProperty TitleProperty = BindableProperty.Create(nameof(Title), typeof(string), typeof(InfoView), string.Empty);
    public static readonly BindableProperty DescriptionProperty = BindableProperty.Create(nameof(Description), typeof(string), typeof(InfoView), string.Empty);
    public static readonly BindableProperty BorderColorProperty = BindableProperty.Create(nameof(BorderColor), typeof(Color), typeof(InfoView), Color.Black);
    public static readonly BindableProperty FillColorProperty = BindableProperty.Create(nameof(FillColor), typeof(Color), typeof(InfoView), Color.White);

    public string Title
    {
        get => (string)GetValue(TitleProperty);
        set => SetValue(TitleProperty, value);
    }

    public string Description
    {
        get => (string)GetValue(DescriptionProperty);
        set => SetValue(DescriptionProperty, value);
    }

    public Color BorderColor
    {
        get => (Color)GetValue(BorderColorProperty);
        set => SetValue(BorderColorProperty, value);
    }

    public Color FillColor
    {
        get => (Color)GetValue(FillColorProperty);
        set => SetValue(FillColorProperty, value);
    }
}

A continuación, se ha creado una página que muestra 4 tarjetas con información del InfoView anterior. No todas las tarjetas tienen la misma representación visual, ya que se han usado dos ControlTemplates diferentes.

Las ControlTemplates, se declaran como recursos, y se les asigna un key, para poder utilizarlas más adelante. Además, hay propiedades que se pueden asociar a propiedades del modelo, utilizando TemplateBinding.

<ContentPage.Resources>
    <ControlTemplate x:Key="InfoViewRowsTemplate">
        <Frame BackgroundColor="{TemplateBinding FillColor}"
                BorderColor="{TemplateBinding BorderColor}"
                CornerRadius="10"
                HasShadow="True"
                Padding="8"
                HorizontalOptions="Center"
                VerticalOptions="Center">
            <Grid RowDefinitions="Auto, Auto" >
                <Label Grid.Row="0"
                        Text="{TemplateBinding Title}"
                        FontAttributes="Bold"
                        VerticalTextAlignment="Center"
                        HorizontalTextAlignment="Start" />
                <Label Grid.Row="1"
                        Text="{TemplateBinding Description}"
                        VerticalOptions="Center"
                        HorizontalOptions="Start" />
            </Grid>
        </Frame>
    </ControlTemplate>

    <ControlTemplate x:Key="InfoViewColumnsTemplate">
        <Frame BackgroundColor="{TemplateBinding FillColor}"
                BorderColor="{TemplateBinding BorderColor}"
                CornerRadius="10"
                HasShadow="True"
                Padding="8"
                HorizontalOptions="Center"
                VerticalOptions="Center">
            <Grid ColumnDefinitions="*, *" >
                <Label Grid.Column="0"
                        Text="{TemplateBinding Title}"
                        FontAttributes="Bold"
                        VerticalTextAlignment="Center"
                        HorizontalTextAlignment="Start" />
                <Label Grid.Column="1"
                        Text="{TemplateBinding Description}"
                        VerticalOptions="Center"
                        HorizontalOptions="Start" />
            </Grid>
        </Frame>
    </ControlTemplate>
</ContentPage.Resources>

En el ejemplo anterior, se aprecia que se han creado 2 templates para representar un InfoView. Uno que muestra la información en filas (InfoViewRowsTemplate) y otro en columnas (InfoViewColumnsTemplate).

En ambos casos, se observa que se asocian las propiedades FillColor, BorderColor, Title y Description usando TemplateBinding.

Para usar el template en el control InfoView, basta con asignar el ControlTemplate de dicho control.

<StackLayout Padding="10" Spacing="10">
    <controls:InfoView Title="Jorge"
                        Description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla elit dolor, convallis non interdum."
                        BorderColor="Red"
                        FillColor="Green"
                        ControlTemplate="{StaticResource InfoViewRowsTemplate}" />

    <controls:InfoView Title="Jorge"
                        Description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla elit dolor, convallis non interdum."
                        BorderColor="Yellow"
                        FillColor="White"
                        ControlTemplate="{StaticResource InfoViewColumnsTemplate}" />

    <controls:InfoView Title="{Binding InfoTitle}"
                        Description="{Binding InfoDescription}"
                        BorderColor="Red"
                        FillColor="Green"
                        ControlTemplate="{StaticResource InfoViewRowsTemplate}" />

    <controls:InfoView Title="{Binding InfoTitle}"
                        Description="{Binding InfoDescription}"
                        BorderColor="Yellow"
                        FillColor="White"
                        ControlTemplate="{StaticResource InfoViewColumnsTemplate}" />
</StackLayout>

El resultado de la página creada es el siguiente

Teléfono en el que se muestran 4 tarjetas

Además de los TemplateBindings, se pueden utilizar Bindings estandar para realizar la asociación de propiedades, pero hay que tener en cuenta que hay que establecer el contexto para cada control y, así, evitar posibles errores. Si todos los controles del ControlTemplate comparten el mismo contexto, como suele ser habitual, éste se puede asociar al elemento raíz, tal y como se muestra a continuación.

<ControlTemplate x:Key="InfoViewRowsTemplate">
    <Frame BindingContext="{Binding Source={RelativeSource TemplatedParent}}"
            BackgroundColor="{Binding FillColor}"
            BorderColor="{Binding BorderColor}"
            CornerRadius="10"
            HasShadow="True"
            Padding="8"
            HorizontalOptions="Center"
            VerticalOptions="Center">
        <Grid RowDefinitions="Auto, Auto" >
            <Label Grid.Row="0"
                    Text="{Binding Title}"
                    FontAttributes="Bold"
                    VerticalTextAlignment="Center"
                    HorizontalTextAlignment="Start" />
            <Label Grid.Row="1"
                    Text="{Binding Description}"
                    VerticalOptions="Center"
                    HorizontalOptions="Start" />
        </Grid>
    </Frame>
</ControlTemplate>

<ControlTemplate x:Key="InfoViewColumnsTemplate">
    <Frame BindingContext="{Binding Source={RelativeSource TemplatedParent}}"
            BackgroundColor="{Binding FillColor}"
            BorderColor="{Binding BorderColor}"
            CornerRadius="10"
            HasShadow="True"
            Padding="8"
            HorizontalOptions="Center"
            VerticalOptions="Center">
        <Grid ColumnDefinitions="*, *" >
            <Label Grid.Column="0"
                    Text="{Binding Title}"
                    FontAttributes="Bold"
                    VerticalTextAlignment="Center"
                    HorizontalTextAlignment="Start" />
            <Label Grid.Column="1"
                    Text="{Binding Description}"
                    VerticalOptions="Center"
                    HorizontalOptions="Start" />
        </Grid>
    </Frame>
</ControlTemplate>

Por último, es necesario indicar que los ControlTemplate también se pueden aplicar a un control utilizando estilos. La ventaja de usar estilos, es que se puede aprovechar el mismo estilo para setear más propiedades, además de las establecidas en la plantilla.

Para aplicar un ControlTemplate a través de un estilo, hay que declarar la plantilla como recurso, tal y como hemos visto en los ejemplos anteriores. Una vez creada la plantilla, hay que crear el estilo, cuyo destino sea el tipo del ContentView sobre el que aplicaremos el estilo y asignar la propiedad ControlTemplate.

<ContentPage.Resources>
    <Style x:Key="InfoViewColumnsStyle" TargetType="controls:InfoView">
        <Setter Property="ControlTemplate" Value="{StaticResource InfoViewColumnsTemplate}" />
    </Style>
</ContentPage.Resources>

...

<controls:InfoView Title="Jorge"
                   Description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla elit dolor, convallis non interdum."
                   BorderColor="Yellow"
                   FillColor="White"
                   Style="{StaticResource InfoViewColumnsStyle}" />

ControlTemplate en un ContentPage

Del mismo modo que se pueden aplicar plantillas a un ContentView, también se pueden aplicar a un ContentPage. Algo realmente interesante cuando queremos que una misma página tenga dos visualizaciones diferentes. Para comprenderlo bien, he creado un ejemplo que muestra una barra de navegación, la cual se puede posicionar en la parte superior o inferior de la pantalla. Para conseguirlo he creado una página con dos plantillas y sin ningún contenido adicional.

<?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:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
             x:Class="XFCustomTemplates.Controls.BasePage"
             ios:Page.UseSafeArea="true">
    <ContentPage.Resources>
        <ControlTemplate x:Key="TopToolBarTemplate">
            <Grid RowDefinitions="80, *"
                  ColumnDefinitions="80, *, 80">

                <BoxView Grid.ColumnSpan="3" Color="{TemplateBinding ToolBarColor}" />
                <ImageButton Source="back.png"
                             Aspect="AspectFit"
                             Clicked="BackButton_Clicked"
                             HeightRequest="48" WidthRequest="48"
                             HorizontalOptions="Center" VerticalOptions="Center" />
                <Label Grid.Column="1"
                       Text="{TemplateBinding Title}"
                       TextColor="White"
                       VerticalOptions="Center"
                       HorizontalOptions="Center"/>
                <ImageButton Grid.Column="2"
                             Source="change.png"
                             Aspect="AspectFit"
                             Clicked="ChangeButton_Clicked"
                             HeightRequest="48" WidthRequest="48"
                             HorizontalOptions="Center" VerticalOptions="Center" />

                <ContentPresenter Grid.Row="1" Grid.ColumnSpan="3" />
            </Grid>
        </ControlTemplate>

        <ControlTemplate x:Key="BottomToolBarTemplate">
            <Grid RowDefinitions="*, 80"
                  ColumnDefinitions="80, *, 80">

                <ContentPresenter Grid.ColumnSpan="3" />

                <BoxView Grid.Row="1" Grid.ColumnSpan="3" Color="{TemplateBinding ToolBarColor}" />
                <ImageButton Grid.Row="1" Source="back.png"
                             Aspect="AspectFit"
                             Clicked="BackButton_Clicked"
                             HeightRequest="48" WidthRequest="48"
                             HorizontalOptions="Center" VerticalOptions="Center" />
                <Label Grid.Row="1" Grid.Column="1"
                       Text="{TemplateBinding Title}"
                       TextColor="White"
                       VerticalOptions="Center"
                       HorizontalOptions="Center"/>
                <ImageButton Grid.Row="1" Grid.Column="2"
                             Source="change.png"
                             Aspect="AspectFit"
                             Clicked="ChangeButton_Clicked"
                             HeightRequest="48" WidthRequest="48"
                             HorizontalOptions="Center" VerticalOptions="Center" />


            </Grid>
        </ControlTemplate>
    </ContentPage.Resources>
</ContentPage>

En estas plantillas aparece una propiedad que no hemos visto antes. Ésta es ContentPresenter. Sirve para indicar que ahí aparecerá el contenido de la página. En el siguiente ejemplo, sería el StackLayout con el Label.

<?xml version="1.0" encoding="utf-8" ?>
<controls:BasePage  xmlns:controls="clr-namespace:XFCustomTemplates.Controls"
                    xmlns="http://xamarin.com/schemas/2014/forms"
                    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                    x:Class="XFCustomTemplates.Views.PageTemplateExamplePage"
                    Title="Testing templates"
                    ControlTemplate="{StaticResource TopToolBarTemplate}">
    <ContentPage.Content>
        <StackLayout>
            <Label Text="Welcome to Xamarin.Forms!"
                VerticalOptions="CenterAndExpand"
                HorizontalOptions="CenterAndExpand" />
        </StackLayout>
    </ContentPage.Content>
</controls:BasePage>

Tal y como se muestra en el código anterior, la asignación de la plantilla que se desea usar se hace de la misma manera que en ContentViews, es decir, asignando la propiedad ControlTemplate

Gif en el que se muestra la misma pantalla en la que se cambia la barra de navegación de la parte superior a la parte inferior

Como siempre, aquí os dejo un enlace al repositorio del proyecto de ejemplo que he creado, para que podáis descargarlo y seguir explorando las posibilidades que ofrecen los ControlTemplates https://github.com/jorgediegocrespo/XamarinCustomTemplates