在c#中,当EventArg具有泛型属性时,引发事件



我正在尝试创建一个自定义事件,其事件参数包含一个通用参数。我不确定这是否可以做到,或者我只是做得不正确。

这是一个非常简单的例子。它是一个对象类,当其属性之一即将更改时将引发事件。事件参数应该包含一个属性,该属性是正在更改的属性的当前值,以便用户可以在需要时取消操作。我明白你可以只看引起事件的对象,但请忽略它,更多地关注这个问题的机制。

同样,我知道这个例子不会编译,但它演示了我想要实现的目标。

首先,事件参数类:

public class FooPropertyChangingEventArgs<T> : System.EventArgs
{
public FooPropertyChangingEventArgs(string propertyName, T existingValue)
{
this.PropertyName = propertyName;
this.ExistingValue = existingValue;
}

public bool Cancel { get; set; }
public string PropertyName { get; }
public T ExistingValue { get; } // I know that ExistingValue could just be an object type, but I am more interested in finding a way to make it a generic type.
}

委派。这是问题的一部分,因为如果我将类型参数添加到事件参数中,我还需要将它添加到事件处理程序中:

public delegate void FooPropertyChangingEventHandler<T>(object sender, FooPropertyChangingEventArgs<T> e);

这是我的简单对象类。两个数据类型不同的属性:

public class Foo
{
// First problem.  Because I added the type argument to the event handler, I need to add it here.  However, I can't because at 
//  this level I have no way of knowing which property will be changed.  In fact, the event has to work for all of the properties.
public event FooPropertyChangingEventHandler OnFooPropertyChanging;

private int _myInt = 0; 
private string _myString = String.Empty;

public Foo(int myInt, string myString)
{
// Prevent event calls upon object initialization.
this._myInt = myInt;
this._myString = myString;
}

public int MyInt 
{ 
get { return this._myInt; }
set
{
// Here I want to pass an int to any listener.
FooPropertyChangingEventArgs<int> args = new FooPropertyChangingEventArgs<int>("MyInt", this._myInt);
this.OnFooPropertyChanging?.Invoke(this, args);
if(!args.Cancel)
{
this._myInt = value;
}
}
}

public string MyString 
{ 
get { return this._myString; }
set 
{
// Here I want to pass a string to any listener.
FooPropertyChangingEventArgs<string> args = new FooPropertyChangingEventArgs<string>("MyString", this._myString);
this.OnFooPropertyChanging?.Invoke(this, args);
if(!args.Cancel)
{
this._myString = value;             
}
}
}
}

最后,订阅者:

public class FooSubscriber
{
private Foo _foo;
private int _runningTotal = 0;
private string _runningString = String.Empty;

public FooSubscriber()
{
this._foo = new Foo();
this._foo.FooPropertyChanging += this._foo_PropertyChangingEventArgs;
}

// This is what should be called by a consumer to change the properties and raise the events.
public void ChangeFoo(int newMyInt, string newMyString)
{
this._foo.MyInt = newMyInt;
this._foo.MyString = newMyString;
}

// This is also part of the problem.  The compiler expects the event args to have a type argument (e.g., FooPropertyChangingEventArgs<T>), and I understand 
// that at this level there is no way to know which property is being raised.  Or is there a way I am missing?
private void _foo_PropertyChangingEventArgs(object sender, FooPropertyChangingEventArgs e)
{
// No changes allowed in June. Stupid, but this demonstrates how we want to cancel if "something" happens.  
if(DateTime.Now.Month == 6)
{
e.Cancel = true;
}
else
{
// If I could make this work, I would expect to be able to do something like this here:
switch(e.PropertyName)
{
case "MyInt":
// I would expect ExistingValue to be an int.
this._runningTotal += e.ExistingValue + 1;  
break;
case "MyString":
// I would expect ExistingValue to be a string.
this._runningString += e.ExistingValue.ToUpper();
break;
default:
this._runningString = 0;
this._runningString = String.Empty;
break;
}
}
}
}

有点知道哪里不对了。也就是说,订阅者不可能知道事件参数属性是什么数据类型。对吗?我认为泛型应该通过在运行时确定数据类型来解决这个问题。我怎么能指定,我希望事件参数有一个通用的属性,而不强迫事件处理程序的行为?

那么,这可以做到吗?我错过什么了吗?

提前感谢,斯科特

你的根本问题是归结为一个事实,你希望有一个单一的方法,可以以某种方式接受两个不同的参数在同一时间。

当然,在保持强类型的情况下,这是不可能的。

操作过程完全取决于您打算如何使用值。根据你对它们所做的事情,你也许可以看看像策略模式这样的东西,作为处理各种值的方法。

那么,这能做到吗?

不,这是不可能的。

我错过了什么吗?

你应该将你的事件声明为

public event FooPropertyChangingEventHandler<int> OnMyIntChanging;
public event FooPropertyChangingEventHandler<string> OnMyStringChanging;

因为这是你声明的委托的样子

但是,如果我理解正确的话,正确的类建模(您希望对许多属性更改使用一个事件)应该如下所示。我给出了简短的代码版本作为演示。

public class PropertyChangingEventArgs
{
string propertyName;
public string PropertyName => propertyName;
object newValue;
public object NewValue => newValue;
bool cancel;
public bool Cancel
{
get => cancel;
set => cancel = value;
}
internal PropertyChangingEventArgs(string propertyName, object newValue)
{
this.propertyName = propertyName;
this.newValue = newValue;
}
}

和你的委托

public delegate void PropertyChangingEventHandler(
object sender, PropertyChangingEventArgs args);

你的类Foo

public class Foo
{
public event PropertyChangingEventHandler PropertyChanging;
private int _myInt;
public int MyInt
{
get { return this._myInt; }
set
{
PropertyChangingEventArgs args = 
new PropertyChangingEventArgs(nameof(MyInt), value);
PropertyChanging?.Invoke(this, args);
if (!args.Cancel) { _myInt = value; }
}
}
private string _myString;
public string MyString
{
get { return _myString; }
set
{
PropertyChangingEventArgs args =
new PropertyChangingEventArgs(nameof(MyString), value);
PropertyChanging?.Invoke(this, args);
if (!args.Cancel) { _myString = value; }
}
}
}

和客户端类

public class FooSubscriber
{
private Foo _foo;
public FooSubscriber()
{
_foo = new Foo();
_foo.PropertyChanging += _foo_PropertyChangingEventArgs;
}
private void _foo_PropertyChangingEventArgs(
object sender, PropertyChangingEventArgs e)
{
var foo = sender as Foo;
switch (e.PropertyName)
{
case nameof(foo.MyInt):
var newMyInt = (int)e.NewValue;
//your code here
break;
case nameof(foo.MyString):
var newMyString = (string)e.NewValue;
//your code here
break;
}
}
}

正如其他人已经说过的那样,使用c#的事件机制可能没有内置的方法来做到这一点,但这并不意味着你想要实现的目标是不可能的。

它只需要一些自定义实现,我已经发布了一个简单的解决方案,这使得客户端端的代码看起来很优雅,没有开关或如果条件。客户端订阅使用泛型完成,所有的丑陋都隐藏在字典的引擎盖下。

类型安全是有保证的,只要没有人使用反射或其他东西篡改我们的私有字典。

如果这看起来不错,那么我可以提供删除和线程安全,使实现更完整。

using System;
using System.Collections;
using System.Collections.Generic;
public class Program
{
public class MainSubscriber
{
private IDictionary<Type, List<object>> myCallbacks = new Dictionary<Type, List<object>>();

public MainSubscriber(int theIn, string theStr)
{
}
public void NotifyChanged<T>(T theValue)
{
// Notify to all clients who are interested in this particular type.
foreach (var aObject in this.myCallbacks[typeof(T)])
{
Action<T> aAction = (Action<T>)aObject;
aAction.Invoke(theValue);
}
}

public void Add<T>(Action<T> theCallback)
{
// TODO : No remove method, you can remove from the list so callbacks will not be triggered.
// TODO : The code is not thread safe, in case of multi thread use case then locks are needed
// Just store the callback for later use
if (!this.myCallbacks.ContainsKey(typeof(T)))
{
this.myCallbacks.Add(typeof(T), new List<object>());
}
this.myCallbacks[typeof(T)].Add(theCallback);
}
}
public static void Main()
{
MainSubscriber aMainSubscriber = new MainSubscriber(1, "123");

//// Using custom Add/Subscribe method just like += So pretty!
aMainSubscriber.Add<int>(MyIntCallback);
aMainSubscriber.Add<string>(MyStringCallback);
//// Just for testing, you can trigger this from where you desire
aMainSubscriber.NotifyChanged(1231231);
aMainSubscriber.NotifyChanged("Some string");
}

// Client side callbacks, no switch or if's, just use the data :)
private static void MyStringCallback(string theObj)
{
Console.WriteLine($"Received {theObj}"); 
}
private static void MyIntCallback(int theObj)
{
Console.WriteLine($"Received '{theObj}'");
}
}

相关内容

  • 没有找到相关文章

最新更新