Pull to refresh

Yet another AOP in .NET

Reading time 4 min
Views 16K
У многих .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 можно применять не только к классам, но и к конкретным членам класса, если нужна «точечная врезка». Вот пример декомпилированного кода, полученного после компиляции примера, указанного выше:

public class Target
{
    private readonly MethodTraceAspect __a$_MethodTraceAspect;
    public void Create()
    {
        this.__a$_MethodTraceAspect.Trace("Create");
    }
    public void Update()
    {
        this.__a$_MethodTraceAspect.Trace("Update");
    }
    public void Delete()
    {
        this.__a$_MethodTraceAspect.Trace("Delete");
    }
    public Target()
    {
        this.__a$_MethodTraceAspect = new MethodTraceAspect();
    }
}

Тут также можно заметить, что атрибут Aspect был удален во время генерации кода, что позволяет результирующей сборке не ссылаться на сборку Aspect Injector.

Если вернуться к исходной задаче реализации интерфейса 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!
Tags:
Hubs:
+16
Comments 50
Comments Comments 50

Articles