public interface ITestViewModel
{
int Foo1 { get; set; }
int Foo2 { get; set; }
}
//Модель представления
public class TestViewModel : ITestViewModel, INotifyPropertyChanged
{
private int _foo1;
public int Foo1
{
get
{
return _foo1;
}
set
{
if(_foo1 != value)
{
_foo1 = value;
OnPropertyChanged("Foo1");
}
}
}
private int _foo2;
public int Foo2
{
get
{
return _foo2;
}
set
{
if(_foo2 != value)
{
_foo2 = value;
OnPropertyChanged("Foo2");
}
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if(handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
* This source code was highlighted with Source Code Highlighter.
Необходимость генерации события PropertyChanged при каждом изменении свойств делает невозможным использование авто-свойств, из-за чего код быстро «разбухает» за счёт вспомогательного кода.
Упростить подобный код позволит применение подходов аспектно-ориентированного программирования, которые реализованы в Microsoft Unity Application Block при помощи перехватов (interception) вызовов методов.
Реализуются перехваты вызовов для зарегистрированных в контейнере типов одним из трёх способов.
Перехват при помощи генерации для объекта прозрачного прокси (TransparentProxyInterceptor).
В данном случае класс, чьи методы подлежат перехвату, должен быть наследником MarshalByRefObject. Для объекта генерируется прозрачный прокси, через который и осуществляется работа.
Преимущества: возможность перехвата любых свойств и методов класса.
Недостатки: необходимость наследования от MarshalByRefObject, более медленный вызов перехватываемых методов по сравнению с другими способами.
Перехват при помощи интерфейса (InterfaceInterceptor)
Для исходного класса, реализующего указанный интерфейс, генерируется на лету тип, владеющий экземпляром исходного класса и реализующим тот же самый интерфейс (паттерн Decorator).
Преимущества: более быстрый вызов перекрываемых свойств и методов.
Недостатки: перехватываются только те методы и свойства, объявление которых определено в интерфейсе.
Перехват при помощи виртуальной функции (VirtualMethodInterceptor)
В данном случае перехвату подлежат только виртуальные методы и свойства класса.
Преимущества: такие же, как и у перехвата при помощи интерфейса.
Недостаткн: необходимость делать перехватываемый метод или свойство виртуальными.
Реализовать перехват в Microsoft Unity Application Block можно
• при помощи атрибутов, наследующих тип HandlerAttribute,
• при помощи поведенческих классов, наследующих интерфейс IInterceptionBehavior.
В первом случае можно перехватить только вызов метода, для которого задан атрибут.
//Класс атрибута, определяющий перехватчик для метода
public class LogAttribute : HandlerAttribute
{
//Класс перехватчика метода
private class LogHandler : ICallHandler
{
public LogHandler()
{
Order = 1;
}
#region ICallHandler Members
//Реализация перехвата вызова метода
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
Console.WriteLine("calling {0}\n", input.MethodBase.Name); //Вывод имени перехватываемого метода на консоль
return getNext()(input, getNext); //Вызов остальных перехватчиков и перехватываемого метода
}
public int Order {get; set;} //Порядок вызова перехватчика (чем меньше, тем раньше он вызовется)
#endregion
}
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new LogHandler();
//Лучше, конечно, так
//return container.Resolve<LogHandler>();
}
}
* This source code was highlighted with Source Code Highlighter.
Применение
public class Foo : MarshalByRefObject
{
[Log] //Атрибут перехватчика
public void Bar() //Вызовы метода Bar теперь будут протоколироваться на консоль
{
}
}
class Program
{
static void Main(string[] args)
{
IUnityContainer container = new UnityContainer();
container.AddNewExtension<Interception>();
container.RegisterType<Foo>(new Interceptor<TransparentProxyInterceptor>()); //Перехват через генерацию прозрачного прокси
Foo foo = container.Resolve<Foo>();
foo.Bar(); //Вызов Bar будет перехвачен
}
}
* This source code was highlighted with Source Code Highlighter.
Второй же способ позволяет не только перехватить методы объекта, но внедрить в объект «на лету» поддержку одного или нескольких интерфейсов.
public class NotifyPropertyChangedBehavior : IInterceptionBehavior
{
private PropertyChangedEventHandler _handler;
private readonly object _handlerLock = new object();
#region IInterceptionBehavior Members
//Возвращаем "внедряемые" интерфейсы
public virtual IEnumerable<Type> GetRequiredInterfaces()
{
return new[] { typeof(INotifyPropertyChanged) };
}
//Перехватываем методы и свойства
public virtual IMethodReturn Invoke(IMethodInvocation input, GetNextInterceptionBehaviorDelegate getNext)
{
var methodReturn = InvokeINotifyPropertyChangedMethod(input);
if (methodReturn != null)
{
return methodReturn;
}
//Обрабатываем изменение значения свойства (вызов set_PropertyName)
var propertyName = (input.MethodBase.IsSpecialName && input.MethodBase.Name.StartsWith("set_")) ? input.MethodBase.Name.Substring(4) : null;
object propertyValue = null;
Func<object, object> propertyGetAccessor = null;
if (propertyName != null)
{
//Иcпользуем для доступа к значению свойств делегаты вместо Reflection для более быстрой работы
propertyGetAccessor = PropertyAccessor.GetPropertyValueAccessor(input.MethodBase.DeclaringType, input.Target, propertyName);
propertyValue = propertyGetAccessor(input.Target); //значение свойства до изменения значения
}
methodReturn = getNext()(input, getNext); //изменяем значение свойства
if (propertyName != null)
{
try
{
//Если значение свойства изменилось, генерируем событие PropertyChanged
var newValue = input.Arguments[0];
IComparable comparable = newValue as IComparable;
if (comparable != null)
{
if (comparable.CompareTo(propertyValue) != 0)
{
OnPropertyChanged(input, propertyName);
}
}
else if (newValue != propertyValue)
{
OnPropertyChanged(input, propertyName);
}
}
catch (Exception e)
{
return input.CreateExceptionMethodReturn(e);
}
}
return methodReturn;
}
public bool WillExecute
{
get { return true; } //выполнять всегда
}
#endregion
protected virtual void OnPropertyChanged(IMethodInvocation input, string propertyName)
{
//Генерация события PropertyChangedу перекрываемого объекта
var currentHandler = _handler;
if (currentHandler != null)
{
currentHandler(input.Target, new PropertyChangedEventArgs(propertyName));
}
}
//Вызов INotifyPropertyChanged
protected IMethodReturn InvokeINotifyPropertyChangedMethod(IMethodInvocation input)
{
if (input.MethodBase.DeclaringType == typeof(INotifyPropertyChanged))
{
switch (input.MethodBase.Name)
{
case "add_PropertyChanged":
lock (_handlerLock)
{
_handler = (PropertyChangedEventHandler)Delegate.Combine(_handler, (Delegate)input.Arguments[0]);
}
break;
case "remove_PropertyChanged":
lock (_handlerLock)
{
_handler = (PropertyChangedEventHandler)Delegate.Remove(_handler, (Delegate)input.Arguments[0]);
}
break;
default:
return input.CreateExceptionMethodReturn(new InvalidOperationException());
}
return input.CreateMethodReturn(null);
}
return null;
}
}
* This source code was highlighted with Source Code Highlighter.
Исходный текст класса PropertyAccessor
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Unity.InterceptionExtension
{
internal static class PropertyAccessor
{
private static int GetKey(Type target, string propertyName)
{
return (new { V1 = target.GetHashCode(), V2 = propertyName.GetHashCode() }).GetHashCode();
}
private static Dictionary<int, Func<object, object>> _getPropertyCache;
private static Dictionary<int, Func<object, object>> GetPropertyCache
{
get
{
if (_getPropertyCache == null)
{
_getPropertyCache = new Dictionary<int, Func<object, object>>();
}
return _getPropertyCache;
}
}
private static Dictionary<int, Action<object, object>> _setPropertyCache;
private static Dictionary<int, Action<object, object>> SetPropertyCache
{
get
{
if (_setPropertyCache == null)
{
_setPropertyCache = new Dictionary<int, Action<object, object>>();
}
return _setPropertyCache;
}
}
public static Func<object, object> GetPropertyValueGetter(Type type, object target, string propertyName)
{
Func<object, object> Accessor;
var key = GetKey(type, propertyName);
if (!GetPropertyCache.TryGetValue(key, out Accessor))
{
var parameter = Expression.Parameter(typeof(object), "obj");
var expression = Expression.Lambda<Func<object, object>>
(
Expression.Convert(Expression.Property(Expression.Convert(parameter, type), propertyName), typeof(object)), parameter
);
Accessor = expression.Compile(); //(obj)=>(object)((T)obj).PropertyName
GetPropertyCache.Add(key, Accessor);
}
return Accessor;
}
public static Action<object, object> GetPropertyValueSetter(Type type, object target, string propertyName)
{
return GetPropertyValueSetter(target, type.GetProperty(propertyName));
}
public static Action<object, object> GetPropertyValueSetter(object target, PropertyInfo property)
{
Action<object, object> Accessor;
var type = property.DeclaringType;
var key = GetKey(type, property.Name);
if (!SetPropertyCache.TryGetValue(key, out Accessor))
{
var parameter = Expression.Parameter(typeof(object), "obj");
var value = Expression.Parameter(typeof(object), "value");
var expression = Expression.Lambda<Action<object, object>>
(
Expression.Call(Expression.Convert(parameter, type), property.GetSetMethod(), Expression.Convert(value, property.PropertyType)), parameter, value
);
Accessor = expression.Compile(); //(obj, value)=>((T)obj).PropertyName = (PropertyType)value Accessor = expression.Compile(); //(obj)=>(object)((T)obj).PropertyName
SetPropertyCache.Add(key, Accessor);
}
return Accessor;
}
}
}
* This source code was highlighted with Source Code Highlighter.
И теперь приведённую в самом начале статьи модель представления можно будет представить так.
public class TestViewModel : ITestViewModel
{
//Нет необходимости в INotifyPropertyChanged и в связанном с ним вспомогательном коде
public int Foo1 {get; set;}
public int Foo2 {get; set;}
}
* This source code was highlighted with Source Code Highlighter.
В коде регистрации типов в контейнере нужно будет добавить следующее
container.AddNewExtension<Interception>();
container.RegisterType<ITestViewModel, TestViewModel>(new Interceptor<InterfaceInterceptor>(), new InterceptionBehavior<NotifyPropertyChangedBehavior>()); //Перехват через интерфейс и использование поведенческого класса NotifyPropertyChangedBehavior
//Перехватываться будут только методы и свойства интерфейса ITestViewModel
* This source code was highlighted with Source Code Highlighter.
Для создания модели представления, как и раньше, используется вызов Resolve у контейнера. Ниже приведён тест работы с моделью представления.
var test = container.Resolve<ITestViewModel>();
((INotifyPropertyChanged)test).PropertyChanged += (s, e) => Console.WriteLine(e.PropertyName);
test.Foo1 = 11; //На консоль выведется строка Foo1
test.Foo1 = 11; //На консоль ничего не выведётся
test.Foo1 = 12; //На консоль выведется строка Foo1
* This source code was highlighted with Source Code Highlighter.
Таким образом, используя механизм перехвата вызовов методов Microsoft Unity Application Block возможно:
• определить дополнительный код, выполняемый при вызове определённых методов;
• реализовать «на лету» в объекте поддержку одного или нескольких интерфейсов.
Перехват Microsoft Unity Application Block осуществляется во время выполнения и накладывает дополнительные требования к перехватываемым методам или объектам (наследование MarshalByRefObject или виртуальность перехватываемых методов).
Существующие на сегодняшний день специальные инструменты для аспектно-ориентированного программирования (PostSharp, NAspect и др.) позволяют, конечно же, справиться с подобными задачами более эффективно. Но лёгкость дистрибутива Microsoft Unity Application Block, его бесплатность, открытость исходных текстов, реализация IoС и DI-контейнеров из коробки (изначально Unity Application Block для этого и создавался), простая расширяемость, делают его хорошим выбором для повседневных задач.
2 комментария:
Спасибо за статью.
хочется только узнать код на:
PropertyAcceptor.GetPropertyValueAcceptor
AProoks, спасибо за замечание. Внёс исправления в статью. Acceptor я спутал с Accessor.
Отправить комментарий