Attached Properties en Xamarin Forms

¡Hola!
Desde hace ya varios meses trabajando con Xamarin Forms y antes con WPF, nunca me había dado cuenta de lo extraño que era cómo se definen las filas y columnas de un elemento dentro de un Grid. Con extraño me refiero a que, seguramente porque lo hacía de forma natural, no me había dado cuenta de que la propiedad Grid.Row (entre otras) no está presente en todos los controles que utilizamos… me refiero a esto:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    <Grid
        HorizontalOptions="FillAndExpand"
        VerticalOptions="Center">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="50"/>
        </Grid.RowDefinitions>
 
        <BoxView
            Grid.Row="0"
            Grid.Column="0"
            BackgroundColor="Red"/>
 
        <BoxView
            Grid.Row="0"
            Grid.Column="1"
            BackgroundColor="YellowGreen"/>
 
        <BoxView
            Grid.Row="1"
            Grid.Column="0"
            Grid.ColumnSpan="2"
            BackgroundColor="Blue"/>      
    </Grid>

Si nos fijamos, en el último BoxView que hemos declarado, le estamos diciendo que nos lo pinte en la fila 1 (la última) y, empezando por la columna 0 se expanda dos columnas (es decir, se expanda hasta el final).
Todas las propiedades relacionadas con las filas y columnas que hemos indicado (Grid.Row, Grid.Column y Grid.ColumnSpan) y que podemos establecer para cualquier elemento que queramos pintar (realmente para cualquier BindableObject), no son propias del control. En el caso de nuestro BoxView, si intentamos buscar esas propiedades obviamente no van a existir, estas propiedades están definidas como Attached Properties (Propiedades Adjuntas).

¿Qué son las Propiedades Adjuntas (Attached Properties)?

Son un tipo especial de Bindable Properties, que se define en una clase pero pueden ser asignados desde otra, es más sencillo de lo que suena. En el caso de Grid.Row, la propiedad Row está definida dentro de la clase Grid (podría estar en cualquier otra), pero podemos establecer esa propiedad desde nuestro BoxView, es decir, podemos hacer que nuestro BoxView, a pesar de que no tiene esa propiedad, tenga el valor de 1 cuando se pregunte cuál es su Row. Esto me recuerda vagamente a los mixins que vimos hace unos días, al final te permite añadir funcionalidad a tu clase (nuestro BoxView) desde otra clase (Grid)… Dejando de lado los mixins, el asociar la propiedad Grid.Row a nuestro BoxView, va a permitir que cuando el Grid se encargue de pintar los elementos que está dentro de él (los BoxViews), pueda consultar qué valor de Row/Column/RowsSpan/ColumnSpan tienen para pintarlos donde corresponda. Ahora la pregunta es, si el BoxView no tiene realmente esas propiedades, ¿cómo el Grid las puede consultar? Veamos el siguiente ejemplo:

1
2
3
    <BoxView
        x:Name="MyBoxView"
        Grid.Row="1"/>

Tenemos un BoxView llamado MyBoxView en la fila 1. Para consultar en qué fila está (desde code-behind), como decíamos no es posible directamente desde el BoxView, pues no tiene esa propiedad. Lo que vamos a necesitar es pedirle el valor al método estático GetRow:

1
2
    var boxViewRow = Grid.GetRow(this.MyBoxView);
    //boxViewRow = 1

Así es como el Grid, cuando pinta los elementos, va a conseguir consultar dónde tiene que pintarlos.
Un poco más arriba comentaba que cualquier BindableObject tiene la capacidad establecer valores para Attached Properties, y es que, cuando haces un Grid.Row=»2″ lo que conseguimos es que nuestro BoxView (que es un BindableObject) almacene el valor 2 para la propiedad Row. Simplificando, los BindableObjects tienen una lista de todas las Bindable Properties (en nuestro caso será una Attached Property) que se han establecido, siguiendo el ejemplo, el BoxView almacenará en esa lista el valor 1 para la Attached Property (más bien Bindable Property) «Row» (más bien RowProperty). Si le damos un poco la vuelta al último fragmento de código que hemos visto, realmente el método Grid.GetRow(bindableObject) lo que va a hacer es pedirle a nuestro BoxView (al bindableObject) el valor que tiene para la propiedad adjunta Grid.RowProperty, por lo tanto el último fragmento de código sería equivalente al siguiente:

1
2
    var boxViewRow = (int)this.MyBoxView.GetValue(Grid.RowProperty);
    //boxViewRow = 1

Cualquier BindableObject tiene un método GetValue (y SetValue) en el que diciéndole la BindableProperty te devuelve el valor asociado. Después de todo este coñazo, todavía no hemos dicho ni cuándo, ni cómo utilizarlas, ahora mismo lo vemos.

¿En qué casos podemos usarlas?

Si has leído todo lo anterior, seguramente empezarás a entender lo bueno que tienen las Attached Properties, y es que nos permiten establecer propiedades desde fuera y así poder modificar algún aspecto de nuestros controles sin tener que modificar el control en sí (la clase). Hemos visto que vamos a poder modificar cómo se visualiza dentro de un Grid cualquier control, sin que ese control tenga ninguna referencia al Grid. Esto nos proporciona una capacidad de simplificar código y reutilizarlo muy grande. Con esto vamos a poder hacer (por ejemplo) que cualquier control lance una comando y una animación cuando sea pulsado (TapGestureRecognizer), sin tener que tocar el propio control y hacerlo para que sea totalmente reutilizable. Si conoces cómo funcionan los Effects (y si no los conoces la gente de DevsDna lo explica muy bien en este vídeo), nos permiten aplicar por ejemplo sombreados a los controles nativos, con lo que podríamos tener una Attached Property booleana para activar o desactivar ese sombreado (añadir o quitar el effect) y sin tocar ningún control, sólo añadiendo esa Attached Property, lo conseguiríamos. Otro ejemplo sería que tengamos ciertos roles de usuarios en nuestra aplicación y haya ciertas opciones que cuando se pulsen aparezca un mensaje indicando que no está disponible para tu perfil de usuario, o aparezca inhabilitado y, creando una Propiedad Adjunta, podríamos reutilizar esa funcionalidad o aspecto para cualquier elemento de nuestra página. Este proyecto permite añadir Badges a nuestras pestañas de una TabbedPage, vamos a ver un ejemplo de cómo lo estableceríamos:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    <TabbedPage 
        xmlns="http://xamarin.com/schemas/2014/forms"
        xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
        xmlns:plugin="clr-namespace:Plugin.Badge.Abstractions;assembly=Plugin.Badge.Abstractions" 
        x:Class="Plugin.Badge.Sample.TabXaml">
        <TabbedPage.Children>
            <ContentPage
                Title="Tab1" 
                Icon="icontab1.png"
                plugin:TabBadge.BadgeText="{Binding Count}">
                <StackLayout>...</StackLayout>
            </ContentPage>
                ...
        </TabbedPage.Children>
    </TabbedPage>

No nos obliga a que nuestras páginas hereden de una ContentPage personalizada, sino que se establecen los valores de esos Badges a través de Attached Properties y desde los Renderers de las diferentes plataformas se consultan esos valores para cada ContentPage. Es quizá uno de los mejores ejemplos de uso de las Attached Properties.

Ejemplo

Ahora sí, vamos a ver código de cómo podemos crear una Propiedad Adjunta. Nuestro objetivo va a ser que podamos añadir esa Attached Property a cualquier elemento y, siempre que se pulse el elemento, éste haga una animación de escalado simulando que se ha pulsado y se ejecute un comando.
Lo primero que vamos a crear es la clase que contendrá nuestra Attached Property y, dentro de ella, necesitamos especificar al menos 3 cosas, a saber, la propiedad estática de tipo BindableProperty y dos métodos también estáticos para hacer el get y set de nuestra Propiedad Adjunta.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public class TapAttached
    {
        public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached(
                                                                propertyName: "Command",
                                                                returnType: typeof(ICommand), 
                                                                declaringType: typeof(TapAttached),
                                                                defaultValue:(ICommand)null);
 
        public static void SetCommand(BindableObject view, ICommand command)
        {
            view.SetValue(CommandProperty, command);
        }
 
        public static ICommand GetCommand(BindableObject view)
        {
            return (ICommand)view.GetValue(CommandProperty);
        }
    }

Como vemos, estamos utilizando el método estático BindableProperty.CreateAttached para crear nuestra BindableProperty que será una Propiedad Adjunta, en ella definimos el nombre de la propiedad a la que se hará referencia «Command», el tipo ICommand, el tipo en que declaramos la propiedad TapAttached y el valor por defecto. Hay más cosas que podríamos definir, como el tipo de Binding (OneWay, TwoWay…), un método para validar el valor que establezca a la propiedad, etc, pero por ahora éstas son suficientes. Además de esto, también hemos creado dos métodos estáticos que harán las veces del Get/Set de nuestra propiedad, se llaman SetCommand y GetCommand. Estos nombres no son por casualidad, si los analizamos, empiezan por «Set» o «Get» y el string del nombre de la propiedad que indicamos en la BindableProperty, en nuestro caso «Command», en resumen, típicamente tendremos la BindableProperty que se llamará MiPropiedadProperty, que especificará como string del nombre de la propiedad MiPropiedad y tendrá un método estático para obtener el valor GetMiPropiedad y otro para asignarla SetMiPropiedad. Este tipo de convenciones a mí personalmente no me gustan, porque te equivocas en alguna letra y nadie te avisa explícitamente del error… pero así son las reglas del juego 🙂 De todas maneras yo suponía que esto era necesario para que desde el XAML se estableciese/obtuviese el valor de estas propiedades, pero realmente si pones un punto de interrupción en el SetCommand (por lo menos en la versión de Xamarin Forms que estoy usando (3.1.0.697729)) no pasa por ahí y sí se establece el Command (se lanza el propertychanged que veremos a continuación); realmente con esta convención de los nombres de los métodos Get/Set lo que hacemos es que nuestra propiedad sea reconocible a través de nuestro XAML. Ahora vamos a ver cómo podemos establecer la Attached Property que hemos creado a 3 controles diferentes (un BoxView, un Grid y una Label):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?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:XFSamples;assembly=XFSamples"
                 x:Class="XFSamples.MainPage">
 
        <Grid
            HorizontalOptions="FillAndExpand"
            VerticalOptions="Center">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="50"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="50"/>
                <RowDefinition Height="50"/>
            </Grid.RowDefinitions>
 
            <BoxView
                Grid.Row="0"
                Grid.Column="0"
                local:TapAttached.Command="{Binding Command1}"                BackgroundColor="Red"/>
 
            <Grid
                Grid.Row="0"
                Grid.Column="1"
                local:TapAttached.Command="{Binding Command2}"                BackgroundColor="YellowGreen"/>
 
            <Label
                Text="¡Bienvenido a Xamarin.Forms!"
                local:TapAttached.Command="{Binding Command3}"                HorizontalOptions="Center"
                Grid.Row="1"
                Grid.ColumnSpan="2"/>
 
        </Grid>
    </ContentPage>

Perfecto, ya hemos conseguido enlazar nuestra propiedad a tres controles diferentes, pero claro, todavía no hemos añadido funcionalidad real a nuestra Attached Property; vamos a ello:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
    public class TapAttached
    {
        public static readonly BindableProperty CommandProperty = BindableProperty.CreateAttached(
                                                                propertyName: "Command",
                                                                returnType: typeof(ICommand), 
                                                                declaringType: typeof(TapAttached),
                                                                defaultValue:(ICommand)null,
                                                                propertyChanged: CommandPropertyChanged); 
        public static void SetCommand(BindableObject view, ICommand command)
        {
            view.SetValue(CommandProperty, command);
        }
 
        public static ICommand GetCommand(BindableObject view)
        {
            return (ICommand)view.GetValue(CommandProperty);
        }
 
 
        private static void CommandPropertyChanged(BindableObject bindable, object oldvalue, object newvalue)
        {
            var view = bindable as View;
            var command = GetCommand(bindable);
            if (view == null || command == null)
                return;
 
            var tapGesture = view.GestureRecognizers.FirstOrDefault(g => g is TapGestureRecognizer);
            view.GestureRecognizers.Remove(tapGesture);
            view.GestureRecognizers.Add(CreateTapGesture(view,command));
        }
 
        private static TapGestureRecognizer CreateTapGesture(View view, ICommand command)
        {
            return new TapGestureRecognizer()
            {
                Command = new Command(() =>
                {
                    view.InputTransparent = true;
                    var parentAnimation = new Animation();
                    var scaleDownAnimation = new Animation(v => view.Scale = v, 1, 0.8d, Easing.SpringIn);
                    var scaleUpAnimation = new Animation(v => view.Scale = v, 0.8d, 1, Easing.SpringOut);
                    parentAnimation.Add(0, 0.5, scaleDownAnimation);
                    parentAnimation.Add(0.5, 1, scaleUpAnimation);
 
                    parentAnimation.Commit(view, "TapAnimation", 16, 500, null, (v, c) =>
                    {
                        if (command.CanExecute(null))
                            command.Execute(null);
                        view.InputTransparent = false;
                    });
                })
            };
        }
 
    }

Sin entrar mucho en la funcionalidad del código (es posible que no sea del todo correcta), podemos ver que hemos añadido en la creación de la BindableProperty un nuevo parámetro (propertyChanged) para que cuando se cambie el valor del comando se lance nuestro método CommandPropertyChanged. Dentro de nuestro nuevo método (CommandPropertyChanged) añadimos un nuevo gesto al control para detectar el Tap y, cuando esto ocurre, hacemos una animación y justo cuando finaliza lanzamos el comando que se había asociado al control. Lo interesante aquí es que desde el método CommandPropertyChanged obtenemos el BindableObject asociado (en nuestro caso el BoxView, por ejemplo) y gracias al método estático que habíamos definido GetCommand podemos recuperar el comando que se había establecido y ejecutarlo. Vemos el resultado a continuación.
Dejando de lado que la animación no queda demasiado bien, vemos que el resultado es que hemos logrado hacer que cualquier control ejecute un comando y lance una animación de forma genérica y reutilizable. Podríamos añadir dentro de la clase TapAttached más propiedades adjuntas para poder parametrizar la animación, o añadir un CommandParameter, pero creo que con este ejemplo queda bastante claro cómo podemos utilizarlas. Para establecer la propiedad que hemos creado en code-behind lo haríamos así:

1
    TapAttached.SetCommand(this.BoxView, this.Command1);

Realmente no es exactamente lo mismo, ya que en el caso de XAML estábamos haciendo un Binding y aquí no. No tengo muy claro cómo se haría este Binding en code-behind… es posible que esté un poco despistado hoy.

Conclusiones

Como hemos visto nos permiten reutilizar mucho código, reducir la verbosidad y desacoplar mucho la funcionalidad o el aspecto de nuestro controles. También hace que no definamos Bindable Properties cuando no son necesarias, es decir, sólo las inyectamos allí donde las necesitamos y vayamos a consumir y no creemos controles con Bindable Properties que luego no utilizamos ni la mitad de veces y que son un gasto de recursos innecesarios (en desarrollos móviles sí puede ser importante). Como he comentado las he re-descubierto (las consumía pero no las creaba) hace relativamente poco y seguramente cuando abuse de ellas me llevaré la h***** tendré más claras las desventajas, lo que está claro es que es un poco más complicado de entender su funcionamiento y puede que crees abstracciones innecesarias por si luego las necesitas.
¡Un saludo!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *