У многих .NET разработчиков, использовавших в своей практике WPF, Silverlight или Metro UI, так или иначе возникал вопрос «а как можно упростить себе реализацию интерфейса INotifyPropertyChanged и свойств, об изменениях которых нужно сигнализировать?».
Самый простой «классический» вариант описания свойства, поддерживающего оповещение о своем изменении, выглядит так:
public string Name
{
get { return _name; }
set
{
if(_name != value)
{
_name = value;
NotifyPropertyChanged(“Name”);
}
}
}
Чтобы не повторять в каждом сеттере похожие строки, можно сделать вспомогательную функцию. Более того – начиная с .NET 4.5 в ней можно использовать атрибут [CallerMemberName], чтобы явно не указывать имя вызывающего ее свойства. Но основной проблемы это не решает – все равно для каждого нового свойства необходимо явно описывать поле и геттер с сеттером. Такая механическая работа неинтересна, утомительна и может приводить к ошибкам при копировании и вставке.
Хотелось бы немного «магии», которая позволит небольшими усилиями (например, одной строчкой кода) сделать вот такой класс совместимым с INotifyPropertyChanged:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime Birth { get; set; }
}
Следует отметить, что это не единственная задача, в которой хотелось бы упростить себе жизнь при добавлении сквозной функциональности. Типовые задачи логирования вызванных методов определенного класса, замеров их времени выполнения, проверка наличия прав для вызова – это все, что требует общего решения, избавляющего от надобности вписывать похожие куски кода в каждую место где это нужно.
Более-менее опытный разработчик тут же скажет – «Это же аспектно-ориентированное программирование!» и будет прав. И если начать перечислять уже существующие библиотеки под .NET платформу, в которых в той или иной мере имеется возможность использовать АОП, то список будет не такой уж и короткий: PostSharp, Unity, Spring .NET, Castle Windsor, Aspect .NET… И это далеко не все, но тут следует задуматься о механизмах, реализующих вставку сквозной функциональности, о их достоинствах и недостатках. Можно выделить два основных способа:
- Подстановка во время компиляции (PostSharp)
- Генерация прокси-классов во время выполнения (Unity, Spring .NET, Castle Windsor)
Подстановка во время компиляции – наиболее выгодный способ, так как не требует никаких дополнительных затрат вычислительной мощности при выполнении программы, что особенно важно для мобильных устройств. Привычная генерация прокси-классов хоть и проще в реализации, но помимо вычислительных затрат еще и имеет ограничения – методы или свойства должны содержаться в интерфейсе или быть виртуальными, чтобы их можно было перехватывать через прокси-класс.
PostSharp предлагает очень большие возможности по использованию аспектно-ориентированного программирования, но это коммерческий продукт, что для многих проектов может быть неприемлемо. В качестве альтернативы мы разработали и продолжаем совершенствовать Aspect Injector – фреймворк, позволяющий применять аспекты на этапе компиляции и обладающий простым, но в то же время гибким интерфейсом.
Простейший пример использования Aspect Injector – логирование вызовов методов. Сперва необходимо описать класс аспекта:
public class MethodTraceAspect
{
[Advice(InjectionPoints.Before, InjectionTargets.Method)]
public void Trace([AdviceArgument(AdviceArgumentSource.TargetName)] string methodName)
{
Console.WriteLine(methodName);
}
}
Данное описание говорит о том, что при применении этого аспекта к любому другому классу, в начале каждого его публичного метода будет добавлен вызов Trace() с именем самого метода в качестве параметра.
[Aspect(typeof(MethodTraceAspect))]
public class Target
{
public void Create() { /* … */ }
public void Update() { /* … */ }
public void Delete() { /* … */ }
}
После такого объявления Create, Update, Delete будут печатать в консоль свои названия при каждом вызове. Следует отметить, что атрибут Aspect можно применять не только к классам, но и к конкретным членам класса, если нужна «точечная врезка».
Если вернуться к исходной задаче реализации интерфейса INotifyPropertyChanged – с помощью Aspect Injector можно создать и успешно использовать следующий аспект:
[AdviceInterfaceProxy(typeof(INotifyPropertyChanged))]
public class NotifyPropertyChangedAspect : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged = (s, e) => { };
[Advice(InjectionPoints.After, InjectionTargets.Setter)]
public void RaisePropertyChanged(
[AdviceArgument(AdviceArgumentSource.Instance)] object targetInstance,
[AdviceArgument(AdviceArgumentSource.TargetName)] string propertyName)
{
PropertyChanged(targetInstance, new PropertyChangedEventArgs(propertyName));
}
}
У всех публичных свойств всех классов, к которым будет привязан этот аспект, в конце сеттера выполнится RaisePropertyChanged. Причем интерфейс, указанный в атрибуте AdviceInterfaceProxy будет добавлен к классу самим фреймворком во время компиляции.
На странице проекта можно найти более детальную информацию об атрибутах и их параметрах, доступных на данный момент. Будем благодарны за любые отзывы и предложения по развитию Aspect Injector!
Автор: YuriyIvon