I am trying to create a custom event whose event args contains a generic parameter. I am not sure this can even be done, or if I am just doing it incorrectly.
Here is a very simple example. It is an object class that will raise an event when one of its properties is about to change. The event args should contain a property that is the current value of the changing property so that the user may cancel the action, if desired. I understand that you could simply look at the object that raised the event, but please ignore that and focus more on the mechanics of this question.
Also, I know that this example won't compile, but it demonstrates what I am trying to achieve.
First, the event args class:
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.
}
The delegate. This is part of the problem because if I add the type argument to the event args, I need to also add it to the event handler:
public delegate void FooPropertyChangingEventHandler<T>(object sender, FooPropertyChangingEventArgs<T> e);
This is my simple object class. Two properties with different data types:
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;
}
}
}
}
And, finally, the subscriber:
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;
}
}
}
}
I kind of get what may be wrong. That is, the subscriber can't possibly know what data type the event args property is. Is that correct? I thought that generics were supposed to solve that by determining the data type at runtime. And how can I specify that I want the event args to have a generic property without forcing that behavior on the event handler?
So, can this be done? Am I missing something?
Thanks in advance, Scott
CodePudding user response:
So, can this be done?
No, it cannot be done.
Am I missing something?
You should declare you events as
public event FooPropertyChangingEventHandler<int> OnMyIntChanging;
public event FooPropertyChangingEventHandler<string> OnMyStringChanging;
Because this is what you declared your delegate to look like.
But correct modeling of your classes, if I understood it correctly (You want to have one event for many property change), should be as below. I give short version of code as it is demo.
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;
}
}
And your delegate
public delegate void PropertyChangingEventHandler(
object sender, PropertyChangingEventArgs args);
Your class 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; }
}
}
}
And client class
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;
}
}
}
CodePudding user response:
You're fundamental issue is boiling down to the fact you wish to have a single method that can somehow take in an argument that is 2 different things at the same time.
This, of course, is not possible while also maintaining strong typing.
The course of action entirely depends on how you intend to consume the values. Depending on what you are doing with them, you can perhaps look into things like Strategy Pattern as a way to handle a variety of values.