我正在尝试创建一个自定义事件,其事件参数包含一个通用参数。我不确定这是否可以做到,或者我只是做得不正确。
这是一个非常简单的例子。它是一个对象类,当其属性之一即将更改时将引发事件。事件参数应该包含一个属性,该属性是正在更改的属性的当前值,以便用户可以在需要时取消操作。我明白你可以只看引起事件的对象,但请忽略它,更多地关注这个问题的机制。
同样,我知道这个例子不会编译,但它演示了我想要实现的目标。
首先,事件参数类:
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}'");
}
}