Apps, Binding, INotifyPropertyChanged y BindableBase | XAML | C#
Básico
Como recordarán en versiones anteriores de Visual Studio se solia incluir en los templates la clase BindableBase
. Esta clase nos ayudaba a agilizar la creación de Modelos que hicieran Binding con la UI. Sin embargo esta clase desaparecio en versiones posteriores y si, a muchos nos hace falta.
En este artículo veremos como crearla, y como es mi costumbre lo haremos paso a paso para aprender.
Si solo quieres utilizar BindableBase y ahorrarte toda la explicación acá lo tienes:
Código fuente de este artículo
El código fuente completo de este artículo se encuentra disponible en GitHub.
https://github.com/JuanKRuiz/BindingDemo
Binding
Al hacer Binding abrimos camino para que la UI tome los valores de nuestra clase modelo, y esta "copia" de datos se hace de manera automática por el runtime de XAML.
Hay varias formas de hacer Binding:
Única vez: si asignamos el Datacontext, al arrancar la UI hace Binding con el modelo, pero ningún cambio posterior se efectua sobre la UI.
Notificaciones: al arrancar la UI hace Binding con el modelo, pero si se cambian los datos del modelo la UI actualiza automáticamente los valores en el control. Modificando las propiedades del
Binding es posible hacerlo funcionar de manera similar a única vez.
Bidireccional: al arrancar la UI hace Binding con el modelo, si se cambian los datos del modelo la UI actualiza automáticamente los valores en el control y si se hacen cambios en el control el runtime automáticamente actualiza el modelo
He creado este ejemplo con el que trabajaremos todo este artículo. Esta hecho como Universal App para Windows Phone y Windows Store.
Así estan los fuentes inicialmente:
//DummyModel.cs
public class DummyModel
{
public bool EnableButton { get; set; }
public string TextoUnicaVez { get; set; }
public string TextNotificacionPorCambio { get; set; }
public string TextNotificacionBidireccional { get; set; }
public DummyModel()
{
EnableButton = true;
TextoUnicaVez = "Asignado por única vez";
TextNotificacionPorCambio = "Asignado cuando hay cambios en el modelo";
TextNotificacionBidireccional = "Cambiando el modelo ante cambios en la UI";
}
}
<!--MainPage.xaml-->
<Page
x:Class="BindingDemo.MainPage_INotify"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:BindingDemo"
xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:vm="using:BindingDemo.Models"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.DataContext >
<vm:DummyModel_INotify/>
</Page.DataContext>
<Page.Resources>
<Style TargetType="TextBox">
<Setter Property="FontSize" Value="30"/>
<Setter Property="Margin" Value="20"/>
</Style>
</Page.Resources>
<Grid>
<StackPanel>
<StackPanel>
<TextBox x:Name="txtUnicaVez"
PlaceholderText="Texto Unica Vez"
Text="{Binding TextoUnicaVez, Mode=OneTime}"/>
<TextBox x:Name="txtNotificacionPorCambio"
PlaceholderText="Text Notificacion Por Cambio"
Text="{Binding TextNotificacionPorCambio, Mode=OneWay}"/>
<TextBox x:Name="txtNotificacionBidireccional"
PlaceholderText="Text Notificacion Bidireccional"
Text="{Binding TextNotificacionBidireccional, Mode=TwoWay}"/>
<Button Margin="20" Padding="10"
FontSize="30"
IsEnabled="{Binding ElementName=tgsEnable, Path=IsOn, Mode=TwoWay}"
Click="Button_Click"
>Dummy Button</Button>
<ToggleSwitch x:Name="tgsEnable" Margin="20" Padding="10"
IsOn="{Binding EnableButton}"
FontSize="30">Bloquear Botón</ToggleSwitch>
</StackPanel>
</StackPanel>
</Grid>
</Page>
Todos los campos del modelo mapeados en la UI hacen Binding defecto por única vez.
Así se ve la App:
Para hacer cambios en la UI al cambiar el modelo programaremos el evento click de DummyButton
y afectaremos el campo TextNotificacionPorCambio
del modelo:
//MainPage.xaml.cs
private void Button_Click(object sender, RoutedEventArgs e)
{
var modelo = DataContext as DummyModel;
if (modelo != null)
{
modelo.TextNotificacionPorCambio = "Valor asignado al presionar botón";
}
}
Al ejecutar la aplicación y presionar el botón NO FUNCIONA!. Porque el modelo no puede enviar notificaciones de cambio a la UI.
Notificación de cambios en el modelo con INotifyPropertyChanged
Creamos una copia de nuestra Vista MainPage.xaml
y del modelo DummyModel.cs
aquí haremos las modificaciones necesarias.
- DummyModel_INotify
- MainPage_INotify : Modificamos para que el DataContext sea ahora
DummyModel_INotify
Debemos implementar la interfaz INotifyPropertyChanged
, el runtine automáticamente busca si el modelo implementa la interfaz y la usa para cambiar la UI cuando cambie el modelo.
La implementación básica de DummyModel_INotify
con INotifyPropertyChanged
se ve así:
//DummyModel.cs
public class DummyModel_INotify : INotifyPropertyChanged
{
public bool EnableButton { get; set; }
public string TextoUnicaVez { get; set; }
public string TextNotificacionPorCambio { get; set; }
public string TextNotificacionBidireccional { get; set; }
public DummyModel_INotify()
{
EnableButton = true;
TextoUnicaVez = "Asignado por única vez";
TextNotificacionPorCambio = "Asignado cuando hay cambios en el modelo";
TextNotificacionBidireccional = "Cambiando el modelo ante cambios en la UI";
}
public event PropertyChangedEventHandler PropertyChanged;
}
Nótese que ninguna de las propiedades esta haciendo uso de esta funcionalidad así que el segundo paso es agregar la funcionalidad en las propidades que nos interesa.
Le aplicaremos esta funcionalidad al campo TextNotificacionPorCambio
, para ello seguimos los siguientes pasos:
- Convertirla a full property por lo cual requerimos definir un back store para la propiedad (_textNotificacionPorCambio)
- Al asignar la propiedad debemos llamar al evento de notificación definido anteriormente
- Antes de llamar al evento se debe deberificar que este instanciado (podría no)
- Como argumento hay que enviar el nombre de la propiedad actual
//fragmento DummyModel.cs
private string _textNotificacionPorCambio;
public string TextNotificacionPorCambio
{
get { return _textNotificacionPorCambio; }
set
{
_textNotificacionPorCambio = value;
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs("TextNotificacionPorCambio")
);
}
}
Ahora hay que replicar esto mismo en las demás propiedades que queremos que notifiquen cambios, de inmediato notaremos que el código de asignación es prácticamente idéntico entre unas y otras así que para no hacerlo tan complejo podemos crear un método que incorpore toda esta lógica incluyendo las validaciones del evento y así eliminar el código redundante. Tengamos en cuenta:
- La asignación del nuevo valor de la propiedad la podemos hacer pasando la propiedad por referencia, esto no solo nos permitirá escribir menos código sino que nos será útil más adelante.
- Si bien para este caso podemos hacer uso de un tipo string para asignar la propiedad, podemos aprovechar y utilizar un tipo genérico, de tal forma que nos funcione sin importar el tipo de dato.
//fragmento DummyModel.cs
public void SetProperty<T>(ref T propertyBackStore, T newValue, string propertyName)
{
propertyBackStore = newValue;
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName)
);
}
Podemos hacer más?
Funcionalidad Genérica
Una muy buena optimización es que desde Framework 4.5 / WinRT podemos hacer uso del atributo CallerMemberName
el cual asigna cmo valor por defecto, a un parámetro de tipo cadena, el nombre del método/property que lo ha invocado. Esto nos es útil para evitar un parámetro en el llamado, quedando así:
//fragmento DummyModel.cs
public void SetProperty<T>(ref T propertyBackStore, T newValue,
[CallerMemberName] string propertyName = "")
{
propertyBackStore = newValue;
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName)
);
}
Solo notificar cuando se asigne un valor nuevo
Tal como esta la propiedad notifica los cambios incluso si el valor asignado es exactamente igual que el valor anterior. Valiéndonos de lo que ya tenemos montado podemos hacer esto:
- Solo disparar el evento si realmente se asignó un valor diferente
- Retornar true o false dependiendo de si la asignación se efectuó o no.
public bool SetProperty<T>(ref T propertyBackStore, T newValue,
[CallerMemberName] string propertyName = "")
{
if (Equals(propertyBackStore, newValue))
return false;
propertyBackStore = newValue;
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName)
);
return true;
}
La propiedad re-escrita y las demas propiedades que se requieren quedan de la siguiente forma
//fragmento DummyModel.cs
public string TextoUnicaVez { get; set; }
private bool _enableButton;
public bool EnableButton
{
get { return _enableButton; }
set
{
SetProperty(ref _enableButton, value);
}
}
private string _textNotificacionPorCambio;
public string TextNotificacionPorCambio
{
get { return _textNotificacionPorCambio; }
set
{
SetProperty(ref _textNotificacionPorCambio, value);
}
}
private string _textNotificacionBidireccional;
public string TextNotificacionBidireccional
{
get { return _textNotificacionPorCambio; }
set
{
SetProperty(ref _textNotificacionBidireccional, value);
}
}
Antes de verificar la ejecución recuerda modificar App.xaml.cs para que arranque con el page indicado y a la vez modificar MainPage_INotify.xaml.cs para que el evento del botón haga ahora casting con DummyModel_INotify.
Ejecutamos la nuestra Universal App y...
Notamos un par de cosas interesantes
- Al presionar Dummy Button el valor del segundo TextBox se actualiza automáticamente (ahora si), recordemos que no hemos modificado el objeto gráfico, pero hemos modificado el modelo al cual se encuentra 'bindeado'.
- Alternando la posición del ToggleSwitch modificamos el campo
EnableButton
del modelo - La propiedad IsEnable del Dummy Button desde un comienzo estaba haciendo Binding con la propiedad IsOn del ToggleSwitch así que al mover este último el botón cambia su estado, por si lo perdieron de vista ver líneas 3 y 7:
<Button Margin="20" Padding="10"
FontSize="30"
IsEnabled="{Binding ElementName=tgsEnable, Path=IsOn, Mode=TwoWay}"
Click="Button_Click"
>Dummy Button</Button>
<ToggleSwitch x:Name="tgsEnable" Margin="20" Padding="10"
IsOn="{Binding EnableButton}"
FontSize="30">Bloquear Botón</ToggleSwitch>
Notificación de cambios en el modelo con BindableBase
Como ven la solución anterior para Binding es por decir lo menos: "hermosa", pero aún podemos hacerlo mejor.
Esta solución puede convertirse en una completamente genérica, en la ya clásica BindableBase.
Así que procedemos a generalizar lo que hemos hecho en una clase de la cual futuros modelos puedan deribar.
Creamos una copia de nuestra Vista MainPage_INotify.xaml
y del modelo DummyModel_INotify.cs
aquí haremos las modificaciones necesarias.
- DummyModel_Bindable
- MainPage_Bindable : Modificamos para que el DataContext sea ahora
DummyModel_Bindable
//BindableBase.cs
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace System.ComponentModel
{
public class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public bool SetProperty<T>(ref T propertyBackStore, T newValue,
[CallerMemberName] string propertyName = "")
{
if (Equals(propertyBackStore, newValue))
return false;
propertyBackStore = newValue;
if (PropertyChanged != null)
PropertyChanged(this,
new PropertyChangedEventArgs(propertyName)
);
return true;
}
}
}
Ya una vez realizado esto, nuestro modelo queda así:
using System.ComponentModel;
namespace BindingDemo.Models
{
public class DummyModel_Bindable : BindableBase
{
public string TextoUnicaVez { get; set; }
private bool _enableButton;
public bool EnableButton
{
get { return _enableButton; }
set
{
SetProperty(ref _enableButton, value);
}
}
private string _textNotificacionPorCambio;
public string TextNotificacionPorCambio
{
get { return _textNotificacionPorCambio; }
set
{
SetProperty(ref _textNotificacionPorCambio, value);
}
}
private string _textNotificacionBidireccional;
public string TextNotificacionBidireccional
{
get { return _textNotificacionPorCambio; }
set
{
SetProperty(ref _textNotificacionBidireccional, value);
}
}
public DummyModel_Bindable()
{
EnableButton = true;
TextoUnicaVez = "Asignado por única vez";
TextNotificacionPorCambio = "Asignado cuando hay cambios en el modelo";
TextNotificacionBidireccional = "Cambiando el modelo ante cambios en la UI";
}
}
}
Antes de verificar la ejecución recuerda modificar App.xaml.cs para que arranque con el page indicado y a la vez modificar MainPage_Binding.xaml.cs para que el evento del botón haga ahora casting con DummyModel_Binding.
Al ejecutar la aplicación el resultado será el mismo, solo que ahora tenemos una BindableBase reutilizable en cualquiera de nuestros proyectos.
Quieres Aprender Más de Binding?
Estos videos están esperando por tí: