Проверяем наличие подписчиков у событий при помощи 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;
}
}
А вот если вне его, то проверить так нельзя:
Что ж, нужно посмотреть через 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
- RESTful WCF Service – How to get browser version at server code
- RESTful WCF Service - Узнаем версию браузера в методе сервиса
- Wrox–Professional WCF 4–Windows Communication Foundation with .NET 4
- Wrox–Professional WCF 4–Windows Communication Foundation with .NET 4
- Встраиваем MetaWeblog API на свой сайт