문제
이벤트 이름이 포함된 개체 인스턴스와 문자열 이름이 주어지면 해당 이벤트를 구독하고 해당 이벤트가 발생했을 때 작업(예: 콘솔에 쓰기)을 수행하도록 C# 이벤트를 동적으로 구독하려면 어떻게 해야 할까요?
Reflection을 사용하는 것은 불가능해 보이며 가능하다면 Reflection.Emit을 사용하지 않고 싶습니다. 현재로서는 이것이 유일한 방법인 것 같습니다.
/편집하다: 행사에 필요한 대리인의 서명을 모르겠습니다. 이것이 문제의 핵심입니다.
/편집 2: 대리자 반공변성은 좋은 계획처럼 보이지만 이 솔루션을 사용하는 데 필요한 가정을 할 수 없습니다.
해결책
인수 없이 void 메서드를 모든 유형의 이벤트에 대한 이벤트 처리기로 사용하도록 식 트리를 컴파일할 수 있습니다.다른 이벤트 핸들러 유형을 수용하려면 이벤트 핸들러의 매개변수를 이벤트에 매핑해야 합니다.
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
class ExampleEventArgs : EventArgs
{
public int IntArg {get; set;}
}
class EventRaiser
{
public event EventHandler SomethingHappened;
public event EventHandler<ExampleEventArgs> SomethingHappenedWithArg;
public void RaiseEvents()
{
if (SomethingHappened!=null) SomethingHappened(this, EventArgs.Empty);
if (SomethingHappenedWithArg!=null)
{
SomethingHappenedWithArg(this, new ExampleEventArgs{IntArg = 5});
}
}
}
class Handler
{
public void HandleEvent() { Console.WriteLine("Handler.HandleEvent() called.");}
public void HandleEventWithArg(int arg) { Console.WriteLine("Arg: {0}",arg); }
}
static class EventProxy
{
//void delegates with no parameters
static public Delegate Create(EventInfo evt, Action d)
{
var handlerType = evt.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (object x0, EventArgs x1) => d()
var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x"));
var body = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"));
var lambda = Expression.Lambda(body,parameters.ToArray());
return Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
}
//void delegate with one parameter
static public Delegate Create<T>(EventInfo evt, Action<T> d)
{
var handlerType = evt.EventHandlerType;
var eventParams = handlerType.GetMethod("Invoke").GetParameters();
//lambda: (object x0, ExampleEventArgs x1) => d(x1.IntArg)
var parameters = eventParams.Select(p=>Expression.Parameter(p.ParameterType,"x")).ToArray();
var arg = getArgExpression(parameters[1], typeof(T));
var body = Expression.Call(Expression.Constant(d),d.GetType().GetMethod("Invoke"), arg);
var lambda = Expression.Lambda(body,parameters);
return Delegate.CreateDelegate(handlerType, lambda.Compile(), "Invoke", false);
}
//returns an expression that represents an argument to be passed to the delegate
static Expression getArgExpression(ParameterExpression eventArgs, Type handlerArgType)
{
if (eventArgs.Type==typeof(ExampleEventArgs) && handlerArgType==typeof(int))
{
//"x1.IntArg"
var memberInfo = eventArgs.Type.GetMember("IntArg")[0];
return Expression.MakeMemberAccess(eventArgs,memberInfo);
}
throw new NotSupportedException(eventArgs+"->"+handlerArgType);
}
}
static class Test
{
public static void Main()
{
var raiser = new EventRaiser();
var handler = new Handler();
//void delegate with no parameters
string eventName = "SomethingHappened";
var eventinfo = raiser.GetType().GetEvent(eventName);
eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,handler.HandleEvent));
//void delegate with one parameter
string eventName2 = "SomethingHappenedWithArg";
var eventInfo2 = raiser.GetType().GetEvent(eventName2);
eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,handler.HandleEventWithArg));
//or even just:
eventinfo.AddEventHandler(raiser,EventProxy.Create(eventinfo,()=>Console.WriteLine("!")));
eventInfo2.AddEventHandler(raiser,EventProxy.Create<int>(eventInfo2,i=>Console.WriteLine(i+"!")));
raiser.RaiseEvents();
}
}
다른 팁
완전히 일반적인 해결책은 아니지만 모든 이벤트가 void foo (Object O, T Args) 형식이라면 T가 EventArgs에서 파생 된 경우 대의원 불밀을 사용하여 도망 갈 수 있습니다.다음과 같습니다(KeyDown의 서명이 Click의 서명과 동일하지 않음).
public Form1()
{
Button b = new Button();
TextBox tb = new TextBox();
this.Controls.Add(b);
this.Controls.Add(tb);
WireUp(b, "Click", "Clickbutton");
WireUp(tb, "KeyDown", "Clickbutton");
}
void WireUp(object o, string eventname, string methodname)
{
EventInfo ei = o.GetType().GetEvent(eventname);
MethodInfo mi = this.GetType().GetMethod(methodname, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic);
Delegate del = Delegate.CreateDelegate(ei.EventHandlerType, this, mi);
ei.AddEventHandler(o, del);
}
void Clickbutton(object sender, System.EventArgs e)
{
MessageBox.Show("hello!");
}
Reflection을 사용하여 이벤트를 구독할 수 있습니다.
var o = new SomeObjectWithEvent;
o.GetType().GetEvent("SomeEvent").AddEventHandler(...);
http://msdn.microsoft.com/en-us/library/system.reflection.eventinfo.addeventhandler.aspx
이제 여기에 당신이 해결해야 할 문제가 있을 것입니다.각 이벤트 처리기에 필요한 대리자는 서로 다른 서명을 갖습니다.이러한 메서드(아마도 Reflection.Emit를 의미함)를 동적으로 생성하려면 다른 방법을 찾아야 합니다. 또는 컴파일된 코드로 처리할 수 있도록 자체를 특정 대리자로 제한해야 합니다.
도움이 되었기를 바랍니다.
LinFu를 사용해 보세요. 런타임 시 모든 이벤트에 바인딩할 수 있는 범용 이벤트 핸들러가 있습니다.예를 들어, 동적 버튼의 Click 이벤트에 핸들러를 바인딩할 수 있는 방법은 다음과 같습니다.
// Note: The CustomDelegate signature is defined as: // public delegate object CustomDelegate(params object[] args); CustomDelegate handler = delegate { Console.WriteLine("Button Clicked!"); return null; }; Button myButton = new Button(); // Connect the handler to the event EventBinder.BindToEvent("Click", myButton, handler);
LinFu를 사용하면 대리자 서명에 관계없이 핸들러를 모든 이벤트에 바인딩할 수 있습니다.즐기다!
여기에서 찾을 수 있습니다:http://www.codeproject.com/KB/cs/LinFuPart3.aspx
public TestForm()
{
Button b = new Button();
this.Controls.Add(b);
MethodInfo method = typeof(TestForm).GetMethod("Clickbutton",
BindingFlags.NonPublic | BindingFlags.Instance);
Type type = typeof(EventHandler);
Delegate handler = Delegate.CreateDelegate(type, this, method);
EventInfo eventInfo = cbo.GetType().GetEvent("Click");
eventInfo.AddEventHandler(b, handler);
}
void Clickbutton(object sender, System.EventArgs e)
{
// Code here
}
나는 최근에 단위 테스트 이벤트를 설명하는 일련의 블로그 게시물을 작성했으며, 내가 논의하는 기술 중 하나는 동적 이벤트 구독에 대해 설명합니다.동적 측면을 위해 리플렉션과 MSIL(코드 방출)을 사용했지만 이는 모두 훌륭하게 마무리되었습니다.DynamicEvent 클래스를 사용하면 다음과 같이 이벤트를 동적으로 구독할 수 있습니다.
EventPublisher publisher = new EventPublisher();
foreach (EventInfo eventInfo in publisher.GetType().GetEvents())
{
DynamicEvent.Subscribe(eventInfo, publisher, (sender, e, eventName) =>
{
Console.WriteLine("Event raised: " + eventName);
});
}
내가 구현한 패턴의 기능 중 하나는 이벤트 핸들러 호출에 이벤트 이름을 삽입하여 어떤 이벤트가 발생했는지 알 수 있다는 것입니다.단위 테스트에 매우 유용합니다.
블로그 글은 이벤트 단위 테스트 기법을 설명하기 때문에 꽤 길지만, 전체 소스 코드와 테스트가 제공되며, 동적 이벤트 구독이 어떻게 구현되었는지에 대한 자세한 설명은 마지막 게시물에 자세히 설명되어 있습니다.
http://gojisoft.com/blog/2010/04/22/event-sequence-unit-testing-part-1/
종속성 주입을 사용하면 원하는 것을 얻을 수 있습니다.예를 들어 Microsoft 복합 UI 앱 블록 당신이 설명한 것과 정확히 일치합니다
이 메소드는 메소드를 호출하는 동적 핸들러인 이벤트에 추가됩니다. OnRaised
, 이벤트 매개변수를 객체 배열로 전달합니다.
void Subscribe(object source, EventInfo ev)
{
var eventParams = ev.EventHandlerType.GetMethod("Invoke").GetParameters().Select(p => Expression.Parameter(p.ParameterType)).ToArray();
var eventHandler = Expression.Lambda(ev.EventHandlerType,
Expression.Call(
instance: Expression.Constant(this),
method: typeof(EventSubscriber).GetMethod(nameof(OnRaised), BindingFlags.NonPublic | BindingFlags.Instance),
arg0: Expression.Constant(ev.Name),
arg1: Expression.NewArrayInit(typeof(object), eventParams.Select(p => Expression.Convert(p, typeof(object))))),
eventParams);
ev.AddEventHandler(source, eventHandler.Compile());
}
OnRaised
다음 서명이 있습니다.
void OnRaised(string name, object[] parameters);
다음과 같은 뜻인가요?
//reflect out the method to fire as a delegate
EventHandler eventDelegate =
( EventHandler ) Delegate.CreateDelegate(
typeof( EventHandler ), //type of event delegate
objectWithEventSubscriber, //instance of the object with the matching method
eventSubscriberMethodName, //the name of the method
true );
이는 구독을 수행하지 않지만 호출할 메소드를 제공합니다.
편집하다:
이 답변 후에 게시물이 명확해졌습니다. 유형을 모르면 제 예가 도움이 되지 않습니다.
그러나 .Net의 모든 이벤트는 기본 이벤트 패턴을 따라야 하므로 이를 따르는 한 기본 EventHandler와 함께 작동합니다.