C# の動的イベント サブスクリプション
-
09-06-2019 - |
質問
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(オブジェクトo、t args)の形式である場合、tがeventargsに由来する場合、Delegate Constravarianceを使用してそれを逃れることができます。次のようになります (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
}
私は最近、単体テスト イベントについて説明する一連のブログ投稿を書きました。そのテクニックの 1 つで動的イベント サブスクリプションについて説明しています。動的側面にはリフレクションと 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);
});
}
私が実装したパターンの特徴の 1 つは、イベント ハンドラーの呼び出しにイベント名を挿入するので、どのイベントが発生したかがわかることです。単体テストに非常に役立ちます。
このブログ記事はイベント単体テスト手法を説明しているため非常に長くなりますが、完全なソース コードとテストが提供されており、動的イベント サブスクリプションがどのように実装されたかについては、最後の投稿で詳しく説明されています。
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 で機能します。