miércoles, 13 de enero de 2010

Objetos que notifican sus cambios de propiedades (1/3): La intercepción

Nota: Este post es el primer post de la serie Objetos que notifican sus cambios de propiedades.

En este post vamos a ver como configurar la intercepción de Unity, para poder inyectar nuestro código cada vez que se modifiquen las propiedades de un objeto.

Los que desarrolléis en WPF sabréis que existe una interfaz llamada INotifyPropertyChanged, que se puede implementar para notificar a la interfaz de usuario de que las propiedades de un objeto (generalmente ligado a la interfaz) han modificado, y que por lo tanto la interfaz debe actualizar sus datos.

Esta interfaz define un solo evento, llamado PropertyChanged que debe lanzarse para informar del cambio de propiedad. Es responsabilidad de cada clase lanzar el evento cuando sea oportuno:

public class A : INotifyPropertyChanged
{
private string _name;

// Evento definido por la interfaz
public event PropertyChangedEventHandler PropertyChanged;

// Lanza el evento "PropertyChanged"
private void NotifyPropertyChanged(string info)
{
var handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(info));
}
}
// Propiedad que informa de sus cambios
public string Name
{
get { return _name; }
set
{
if (_name != value)
{
_name = value;
NotifyPropertyChanged("Name");
}
}
}
}





Este código es pesado de realizar en clases con muchas propiedades y es fácil cometer errores… además no podemos utilizar las auto-propiedades. Vamos a ver como con el sistema de intercepción de Unity podemos hacer que este evento se lance de forma automática.



1. Configurando el sistema de intercepción de Unity



Para usar el sistema de intercepción de Unity, debéis añadir los assemblies Microsoft.Practices.ObjectBuilder2, Microsoft.Practices.Unity y Microsoft.Practices.Unity.Interception a vuestro proyecto (los tres assemblies forman parte de Unity).



El primer paso es crear una clase que implemente la interfaz ICallHandler, esta clase es la encargada de proporcionarnos un punto dónde inyectar el código:




class AutoPropertyChangedHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
// Aquí podremos inyectar nuestro código
IMethodReturn msg = getNext()(input, getNext);
return msg;
}
public int Order { get; set; }
}





El siguiente paso es crear un atributo que permita indicar a Unity que ICallHandler debe usar cuando se opere con objetos de la clase. Esta clase debe derivar de HandlerAttribute y debe redefinir el método CreateHandler para devolver una instancia del ICallHandler a utilizar:




class AutoPropertyChangedAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new AutoPropertyChangedHandler();
}
}





El código es trivial, eh?? Simplemente devuelve un AutoPropertyChangeHandler, de esa manera Unity usará este AutoPropertyChangeHandler para todas las clases que estén decoradas con el atributo AutoPropertyChangedAttribute. De esta manera es como le indicamos a Unity qué ICallHandler debe usar por cada tipo de clase.



Finalmente queda configurar el propio contenedor. Para ello debemos debemos añadir la extensión de intercepción a Unity y indicarle que interceptor queremos utilizar para cada clase:




IUnityContainer container = new UnityContainer();
container.AddNewExtension<Interception>();
container.Configure<Interception>().SetInterceptorFor<A2>(new VirtualMethodInterceptor());





Le he indicado a Unity que para la clase A2 utilice el interceptor VirtualMethodInterceptor. Este interceptor puede interceptar todas las llamadas a métodos virtuales.



Finalmente ya podemos definir la clase A2:




[AutoPropertyChanged()]
public class A2 : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public virtual string Name { get; set; }
}





Fijaos en las diferencias entre A2 y la clase A antigua:




  1. A2 está decorada con el atributo AutoPropertyChangedAttribute que hemos definido antes.


  2. La propiedad Name es virtual


  3. No tenemos ningún código adicional para lanzar el evento PropertyChanged.



Con eso el mecanismo de intercepción está listo. Si obteneis una instancia de A2 usando el método Resolve del contenedor y miráis con el debugger de que tipo real es el objeto, veréis que no es de tipo A2, sinó de un tipo raro: el proxy que crea Unity para poder interceptar los métodos virtuales (comparad el wach para a2 y a2newed:



image



2. Implementando el código en nuestro Handler



Vamos a modificar el método Invoke de AutoNotifyPropertyHandler para que lance el evento cada vez que se llame a un set de una propiedad… La clase completa queda tal y como sigue: Básicamente en el método Invoke, miramos si el nombre del método que se ha llamado empieza por “set_”, y si es el caso asumimos que es el Setter de una propiedad. Recogemos el valor actual de la propiedad usando reflection y si no son el mismo, lanzamos el evento PropertyChanged usando reflection. Y listos! :)




class AutoPropertyChangedHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
bool raiseEvt = false;
string propName = null;
INotifyPropertyChanged inpc = input.Target as INotifyPropertyChanged;
if (inpc != null)
{
// Si el nombre del método empieza por "set_" es un Setter de propiedad
if (input.MethodBase.Name.StartsWith("set_"))
{
propName = input.MethodBase.Name.Substring(4);
MethodInfo getter = input.Target.GetType().GetProperty(propName).GetGetMethod();
object oldValue = getter.Invoke(input.Target, null);
object newValue = input.Arguments[0];
// Si los valores de newValue y oldValue son distintos
// debemos lanzar el evento (la comparación la hacemos por
// Equals si es posible).
raiseEvt = newValue == null ?
newValue != oldValue :
!newValue.Equals(oldValue);
}
}
IMethodReturn msg = getNext()(input, getNext);
// Si el setter no produce excepción, lanzamos el evento
if (raiseEvt && msg.Exception == null)
{
RaiseEvent(inpc, propName);
}
return msg;
}

public int Order { get; set; }

// Método que lanza el evento PropertyChanged usando reflection
private void RaiseEvent(INotifyPropertyChanged inpc, string propertyName)
{
Type type = inpc.GetType();
FieldInfo evt = null;
// Buscamos el evento (no estará en el propio Type ya que el propio Type
// será un proxy, pero iteraremos por los tipos base hasta encontrarlo)
do
{
evt = type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
type = type.BaseType;
} while (evt == null && type.BaseType != null);
// Invocamos el evento
if (evt != null)
{
MulticastDelegate mcevt = evt.GetValue(inpc) as MulticastDelegate;
if (mcevt != null)
{
mcevt.DynamicInvoke(inpc, new PropertyChangedEventArgs(propertyName));
}
}
}
}





Ahora podemos comprobar como se lanza el evento automáticamente al modificar una propiedad de A2.



Un saludo!!!!



PD: Dije en el post introductorio que no pondría código, pero finalmente os incluyo el zip con el código de este post (en SkyDrive).



PD2: Echad un post a INotifyPropertyChanged with Unity Interception AOP del blog de Dmitry Shechtman que me ha servido de inspiración para el ejemplo (yo tenía pensado uno mucho más tonto en este primer post).



PD3: Esto es un crosspost desde mi blog en geeks.ms!

No hay comentarios: