Проверяем наличие подписчиков у событий при помощи Reflection

    • Silverlight
    • .NET
    • WCF
    • Reflection
  • modified:
  • reading: 9 minutes

Передо мной встала задача, нужно было бы собрать все события объекта и подписаться на них (подписать определенный метод), только в том случае, если на это событие не подписан кто-то еще. Я могу даже больше сказать, делал я это для классов-оберток, которые генерирует Visual Studio на Add Reference Service…. Используем мы их по стандартному, как и все, наверное. Используем в коде не сами классы-обертки, которые генерирует Visual Studio, а используем свои реализации, которые нам предоставляют возможность тестировать наши модели, подставляя Mock и Stub объекты вместо самих реализаций. Давайте лучше покажу на примере.

Заголовок для этой заметки достаточно сложно было придумать. Проверка наличия подписчиков – это все-таки малая сердцевина того, что я тут описываю, но я думаю это просто самая интересная часть. В целом, я тут описываю вариант обработки результатов от методов сервисов в Silverlight приложении.

Пускай на сервере есть самый обычный WCF Service, в котором есть всего один метод:

public class MyService : IMyService
{    public bool DoWork(string str)
    {        throw new NotImplementedException();
    }
}

В Silverlight проекте мы сделаем Reference на этот сервис. Visual Studio сгенерирует нам класс обертку. Напишем небольшой базовый метод для всех клиентских классов-оберток для сервисов:

public abstract class ServiceBase<TClient> where TClient : class, new()
{    protected ServiceBase()
    {        Client = new TClient();
    }
     public TClient Client { get; set; }
     protected void ProcessResult<T>(AsyncCompletedEventArgs eventArgs, T result)
    {        Action<AsyncResult<T>> action = eventArgs.UserState as Action<AsyncResult<T>>;
         if (action == null)
            throw new NotSupportedException("Unexpected type in UserState.");
         action(new AsyncResult<T>() { Result = result });
    }
     protected void ProcessResult(AsyncCompletedEventArgs eventArgs)
    {        Action<AsyncResult> actionVoidResponce = eventArgs.UserState as Action<AsyncResult>;
         if (actionVoidResponce == null)
            throw new NotSupportedException("Unexpected type in UserState.");
        actionVoidResponce(new AsyncResult());
    }
     protected void ProcessError<T>(AsyncCompletedEventArgs eventArgs)
    {        Action<AsyncResult<T>> action = eventArgs.UserState as Action<AsyncResult<T>>;
         if (action == null)
            throw new NotSupportedException("Unexpected type in UserState.");
         action(new AsyncResult<T>() { Exception = eventArgs.Error });
    }
     protected void ProcessError(AsyncCompletedEventArgs eventArgs)
    {        Action<AsyncResult> actionVoidResponce = eventArgs.UserState as Action<AsyncResult>;
         if (actionVoidResponce == null)
            throw new NotSupportedException("Unexpected type in UserState.");
         actionVoidResponce(new AsyncResult() { Exception = eventArgs.Error });
    }
}

Где AsyncResult – это небольшой класс, при помощи которого мы будем передавать результат выполнения метода:

public class AsyncResult<T> : AsyncResult
{    public T Result { get; set; }
}
 public class AsyncResult
{    public bool IsFault
    {        get { return Exception != null; }
    }
     public Exception Exception { get; set; }
}

Ну и теперь наш пример MyService, для него сделаем интерфейс IMyServiceProxy и класс MyServiceProxy, который реализует этот интерфейс:

public interface IMyServiceProxy
{    void DoWorkAsync(string str, Action<AsyncResult<bool>> callback);
}
 public class MyServiceProxy : ServiceBase<MyServiceClient.MyServiceClient>, IMyServiceProxy
{    public MyServiceProxy()
    {
        Client.DoWorkCompleted += ClientDoWorkCompleted;
    }    public void DoWorkAsync(string str, Action<AsyncResult<bool>> callback)
    {
        Client.DoWorkAsync(str, callback);
    }
     void ClientDoWorkCompleted(object sender, DoWorkCompletedEventArgs e)
    {        if (e.Error != null)
        {            ProcessError<bool>(e);
        }        else
        {            ProcessResult<bool>(e, e.Result);
        }
    }
}

У каждого асинхронного вызова метода есть возможность передавать userState, переменную любого типа (object), обычно куда все и передают callback функцию, этим мы и воспользовались. Дальше очень просто используется эта реализация:

private void Button_Click(object sender, RoutedEventArgs e)
{
    IMyServiceProxy proxy = IoC.Instance.Resolve<IMyServiceProxy>();    proxy.DoWorkAsync("Test", (result) =>
                                  {                                      if (result.IsFault)
                                      {                                          MessageBox.Show("Fault");
                                      }                                      else
                                      {
                                        MessageBox.Show(result.Result.ToString());
                                      }
                                  });
}

Соответственно в IoC реализация MyServiceProxy должна быть зарегистрирована для типа IMyServiceProxy.

Но, у этого способа есть большие недостатки, необходимо на каждый метод сервиса писать метод-обертку Completed, причем она всегда одинаковая, за исключением того, что в аргументах разные типы у свойств Result. Иногда выставишь не верные generic параметры, причем если это ProcessError, то об этом может быть даже никогда и не узнаешь. У меня, соответственно, родилась идея, что было бы хорошо при помощи Reflection пройтись по классам оберткам, которые генерирует Visual Studio, собрать все события, и если подписки на это событие еще нет, тогда подписаться и выполнить тоже самое, что делает метод ClientDoWorkCompleted из примера. Проверять нет ли подписок я решил потому, что иногда, в некоторых случаях своя реализация для Completed все-таки нужна.

Итак, начал я разбираться (заодно и в твиттере озвучил проблему, услышал несколько “никак”, но не сдался), как можно проверить есть ли подписчики у события, если мы пишем внутри класса, у которого есть событие, то сделать это достаточно просто:

public class Foo
{    public event Action FooEvent;
     public bool HaSubscribers()
    {        return FooEvent == null;
    }
}

А вот если вне его, то проверить так нельзя:

Method

Что ж, нужно посмотреть через Reflector и разобраться, что к чему:

reflector

Ага, дальше можно и не изучать, все понятно. Генерируется свойство и поле. Вот для того, чтобы узнать есть ли подписчики, нужно обратиться к полю. Как оказалось, в .NET это сделать очень просто при помощи Reflection:

Foo foo = new Foo();
 var eventInfo = foo.GetType().GetField("FooEvent", BindingFlags.Instance | BindingFlags.NonPublic);
 Console.WriteLine(eventInfo.GetValue(foo) == null);
 foo.FooEvent += () => Console.WriteLine("boom!");
 Console.WriteLine(eventInfo.GetValue(foo) == null);

Чуть позже того, как я нашел решение, получил ответ и в твиттере к своему вопросу (кстати, у @controlflow достаточно интересный блог). Но как оказалось с Silverlight такой фокус не пройдет, там более жесткий security по отношению к Reflection, и обращаться так просто к private полям и методам не разрешено. Погуглил и набрел на статью Accessing private methods in silverlight, как оказалось вызывать private свойства и методы возможно, но только через DynamicMethod (на MSDN даже есть пример, как это сделать). Пока я гуглил и пытался реализовать это в Silverlight, @controlflow опередил меня и предоставил решение для Silverlight (Кто спрашивал зачем нужен твиттер?). В общем, идею получил, приступил к реализации. Для того, чтобы не завязываться на имена методов через строки, я решил написать также MethodExtract метод, который из лямбда выражения мог бы вырывать MethodInfo, получилось очень просто:

public static class MethodSupport
{    public static MethodInfo ExtractMethod<T>(Expression<T> methodExpression)
    {        if (methodExpression == null)
            throw new ArgumentNullException("methodExpression");
         MethodCallExpression methodCallExpression = methodExpression.Body as MethodCallExpression;
         if (methodCallExpression == null)
            throw new ArgumentException(@"Parameter should be a method call expression", "methodExpression");
 
        MethodInfo methodInfo = methodCallExpression.Method;        if (methodInfo.IsGenericMethod && !methodInfo.IsGenericMethodDefinition)
            methodInfo = methodInfo.GetGenericMethodDefinition();
         return methodInfo;
    }
}

Причем, так как у меня часто используются generic методы, то я добавил проверку и на этот случай. В methodExpression лямбда выражение приходит с конкретной реализацией generic метода, то есть все типы уже определены, но мне необходимо было получить именно определения generic методов (GetGenericMethodDefinition()), чтобы на их основе сделать методы с другими определениями типов.

Так же я написал класс EventSupport, у которого есть два метода, первый проверяет наличие подписчиков, второй подписывает на определенный eventInfo определенный methodInfo:

public static class EventSupport
{    public static bool IsEventFieldIsNull(object obj, EventInfo eventInfo)
    {        if (obj == null)
            throw new ArgumentNullException("obj");
         if (eventInfo == null)
            throw new ArgumentNullException("eventInfo");
 
        Type type = obj.GetType();
 
        FieldInfo delegField = type.GetField(eventInfo.Name, BindingFlags.NonPublic | BindingFlags.Instance);
         if (delegField == null)
            throw new NotSupportedException(string.Format("Can't get field for current event '{0}'.", eventInfo));
         Expression body = Expression.Equal(Expression.Field(Expression.Constant(obj), delegField), Expression.Constant(null));
         Func<bool> getter = Expression.Lambda<Func<bool>>(body).Compile();
         return getter();
    }
     public static void Subscribe(object sender, EventInfo eventInfo, object receiver, MethodInfo methodInfo)
    {        if (sender == null)
            throw new ArgumentNullException("sender");
         if (eventInfo == null)
            throw new ArgumentNullException("eventInfo");
         if (receiver == null)
            throw new ArgumentNullException("receiver");
         if (methodInfo == null)
            throw new ArgumentNullException("methodInfo");
 
        Type handlerType = eventInfo.EventHandlerType;
         ParameterInfo[] eventParams = handlerType.GetMethod("Invoke").GetParameters();
        ParameterExpression[] parameters = eventParams.Select(p => Expression.Parameter(p.ParameterType)).ToArray();
 
        Expression body = Expression.Call(Expression.Constant(receiver), methodInfo, parameters);
        Delegate compiledMethod = Expression.Lambda(body, parameters).Compile();
         Delegate handler = Delegate.CreateDelegate(handlerType, compiledMethod, "Invoke", false);
        eventInfo.AddEventHandler(sender, handler);
    }
}

Для начала рассмотрим метод IsEventFieldIsNull (проверка на подписчиков), мы берем FieldInfo у типа объекта с именем полностью совпадающим с EventInfo (так делает .NET), затем при помощи Expression мы описываем метод, вроде bool Expression() { return obj.eventInfoField == null; }: у константы, нашего obj (Expression.Constant(obj)), обращаемся к полю, которое взяли до этого, и сравниваем это значение с константой null. Компилируем метод и вызываем.

Второй метод Subscribe немного посложнее, и сделал я его только потому, что захотелось иметь внешний метод Subscribe, а в случае, если у нас MethodInfo опять имеет уровень доступа private/protected (или даже internal), то чтобы нам к нему иметь доступ, нам нужно так же использовать DynamicMethod, на этот раз мы создаем метод, в который передаются параметры, такие как определены у типа делегата Event, и соответственно вызываем этот MethodInfo с этими параметрами внутри нашего метода, ну и подписываем наш новый метод на событие.

Переходим теперь к реализации, которая у нас будет в базовом классе ServiceBase<TClient>. Я туда добавил два поля:

private readonly MethodInfo _mInfoProcessResult;
private readonly MethodInfo _mInfoProcessError;

Это информация о методах ProcessResult<T> и ProcessError<T> (просто чтобы не искать их на каждый вызов),  ну и добавил в конструктор инициализацию этих полей при помощи описанного выше ExtractMethod метода:

_mInfoProcessResult =    MethodSupport.ExtractMethod<Action<AsyncCompletedEventArgs, object>>((args, obj) => ProcessResult<object>(args, obj));
_mInfoProcessError =    MethodSupport.ExtractMethod<Action<AsyncCompletedEventArgs>>((args) => ProcessError<object>(args));

Дальше пишу метод, который берет все события (фильтрует по имени, заканчивающимся на Completed, исключая Open и Close Completed) и подписывается:

protected void SubscribeEventHandlers()
{
    MethodInfo mInfoCompletedMethod =        MethodSupport.ExtractMethod<Action<object, AsyncCompletedEventArgs>>((obj, args) => OnCompletedHandler(obj, args));
 
    Type clientType = Client.GetType();    foreach (var eventInfo in clientType.GetEvents().Where(x => x.Name.EndsWith("Completed") && x.Name != "OpenCompleted" && x.Name != "CloseCompleted"))
    {        if (EventSupport.IsEventFieldIsNull(Client, eventInfo))
        {            EventSupport.Subscribe(Client, eventInfo, this, mInfoCompletedMethod);
        }
    }
}

В самом начале метода беру информацию о методе OnCompletedHandler, который и будет у меня все обрабатывать:

protected void OnCompletedHandler(object sender, AsyncCompletedEventArgs eventArgs)
{
    Type eventArgsType = eventArgs.GetType();
     if (eventArgs.UserState != null)
    {
        Type userStateType = eventArgs.UserState.GetType();
         if (!userStateType.IsGenericType
            || userStateType.GetGenericTypeDefinition() != typeof(Action<>)
            || userStateType.GetGenericArguments().Length != 1)
        {            throw new NotSupportedException("UserState should be null or Action<AsyncResult<T>> or Action<AsyncResult>.");
        }
 
        Type asyncResponceType = userStateType.GetGenericArguments()[0];
         PropertyInfo propertyInfo = eventArgsType.GetProperty("Result");
         //It is Void Response
        if (asyncResponceType == typeof(AsyncResult))
        {            if (propertyInfo != null)
                throw new NotSupportedException("Call void response for response with defined Result type.");
             if (eventArgs.Error == null)
                ProcessResult(eventArgs);            else
                ProcessError(eventArgs);
        }        // Response with result
        else
        {            // Check that it is AsyncResult<T> type
            if (!userStateType.IsGenericType
            || asyncResponceType.GetGenericTypeDefinition() != typeof(AsyncResult<>)
            || asyncResponceType.GetGenericArguments().Length != 1)
            {                throw new NotSupportedException("UserState should be null or Action<AsyncResult<T>> or Action<AsyncResult>.");
            }
            
            Type argument = asyncResponceType.GetGenericArguments()[0];
             if (propertyInfo == null)
                throw new NotSupportedException("Call not void response for response with void result.");
             if (eventArgs.Error == null)
            {                _mInfoProcessResult.MakeGenericMethod(argument).Invoke(this,
                                                                       new[] {eventArgs, propertyInfo.GetValue(eventArgs, null)});
            }            else
            {                _mInfoProcessError.MakeGenericMethod(argument).Invoke(this, new object[] { eventArgs });
            }
        }
    }
}

В этом методе мы сначала проверяем наличие UserState, затем проверяем, что это callback которого мы ожидаем (типа Action<>), дальше проверка, какой именно ответ мы ожидаем Void или с определенным результатом, если с Void то методы можно вызвать напрямую, а вот если с результатом, то опять проверяем что тип это Action<AsyncResult<T>>, берем этот тип T, строим при помощи его generic методы и вызываем определенный метод (ProcessResult или ProcessError).

Ну а MyServiceProxy пример теперь переписывается очень просто:

public class MyServiceProxy : ServiceBase<MyServiceClient.MyServiceClient>, IMyServiceProxy
{    public MyServiceProxy()
    {
        SubscribeEventHandlers();
    }    public void DoWorkAsync(string str, Action<AsyncResult<bool>> callback)
    {
        Client.DoWorkAsync(str, callback);
    }
}

Вот и все. Если хочется иметь все-таки какую-то специальную обработку на Completed, то нужно просто перед вызовом метода SubscribeEventHandlers произвести ее. Пример со всем исходным кодом как всегда можно скачать с assembla.

See Also