13

(I know the title sounds easy, but hold on—this probably isn't the question you think it is.)

In VB.NET I was able to write custom events. For an example, I had a separate thread that would periodically raise an event and on that event the GUI would need to be updated. I didn't want the busy thread to bother with UI calculations and I didn't want to put Me.Invoke(Sub() ...) in the event handler since it was also called from the GUI thread.

I came up with this very useful bit of code. The GUI thread would set EventSyncInvoke = Me (the main form). The thread could then simply raise the event TestEvent as usual, no special code, and it would be seamlessly executed on the GUI thread:

Private TestEventDelegate As EventHandler
Public EventSyncInvoke As System.ComponentModel.ISynchronizeInvoke

Public Custom Event TestEvent As EventHandler
    AddHandler(value As EventHandler)
        TestEventDelegate = [Delegate].Combine(TestEventDelegate, value)
    End AddHandler

    RemoveHandler(value As EventHandler)
        TestEventDelegate = [Delegate].Remove(TestEventDelegate, value)
    End RemoveHandler

    RaiseEvent(sender As Object, e As System.EventArgs)
        If EventSyncInvoke IsNot Nothing Then
            EventSyncInvoke.Invoke(TestEventDelegate, {sender, e})
        Else
            TestEventDelegate.Invoke({sender, e})
        End If
    End RaiseEvent
End Event

Now in C# I can do this much:

public event EventHandler TestEvent
    add
    {
        testEventDelegate = (EventHandler)Delegate.Combine(testEventDelegate, value);
    }
    remove
    {
        testEventDelegate = (EventHandler)Delegate.Remove(testEventDelegate, value);
    }


}

But where is the ability to do custom raising?

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
jnm2
  • 7,960
  • 5
  • 61
  • 99
  • I'm starting to notice people doing the possible duplicate thing before reading the content of the question (please do!). The RaiseEvent keyword can define two very different things. – jnm2 Feb 18 '12 at 15:49

4 Answers4

10

The other answers told me the fact that I couldn't do this directly in C#, but not the rationale behind why I can't and why I wouldn't want to. It took me a while to understand how C# events worked in comparison to VB.NET. So this explanation is for others who don't have a good grasp on this to start thinking along the right lines.

Honestly, I was so used to the boilerplate OnTestEvent format that I didn't quite like the idea of making it different from the rest of the helper methods. :-) But now that I understand the rationale, I see that it is actually the best place to put this stuff.


VB.NET allows you to hide the background details of calling the delegates with the RaiseEvent keyword. RaiseEvent calls either the event delegate or your custom RaiseEvent section for a custom event.

In C#, there is no RaiseEvent. Raising an event is basically no more than calling a delegate. No custom RaiseEvent sections can be seamlessly called when all you're doing to raise it is calling a delegate. So for C#, custom events are like skeletons, implementing add and remove for events but not implementing the ability to raise them. It's like having to replace all your RaiseEvent TestEvent(sender, e) with the code from the custom RaiseEvent section.

For a normal event, raising looks roughly like NormalEvent(sender, e). But as soon as you put in a custom add and remove, you must use whatever variable you used in the add and remove because the compiler isn't doing it anymore. It's like automatic properties in VB.NET: once you put in a getter and setter manually, you have to declare and handle your own local variable. So instead of TestEvent(sender, e), use testEventDelegate(sender, e). That's where you rerouted the event delegates.


I compared moving from VB.NET to C# with having to replace each of your RaiseEvents with your custom RaiseEvent code. A RaiseEvent code section is basically an event and a helper function rolled together. It's actually standard to only have one instance of a RaiseEvent in either VB.NET or C# inside a protected OnTestEvent method and call that method to raise the event. This allows any code with access to the protected (or private or public) OnTestEvent to raise the event. For what you want to do, just putting it in the method is easier, simpler and performs slightly better. This is best practice.

Now if you really want to want (or need) somehow to mimic VB.NET's RaiseEvent nitty-gritty-hiding call SomeDelegate(sender, e) and have the magic happen, you can simply hide the nitty-gritty inside a second delegate:

NiceTestEvent = (sender, e) => eventSyncInvoke.Invoke(testEventDelegate, new object[] { sender, e });

Now you can call NiceTestEvent(sender, e). You won't be able to call TestEvent(sender, e) though. TestEvent is only for outside code to add and remove through, as Visual Studio will tell you.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
jnm2
  • 7,960
  • 5
  • 61
  • 99
  • 1
    +1 for taking the time to write about your findings. If you want to go a little deeper still, perhaps you're also interested in [.NET events special methods](http://stackoverflow.com/questions/4523511/net-events-special-methods-add-remove-raise-other "question here on Stack Overflow"). Basically, the CLR supports distinct `raise` accessor methods for events (as you might guess from VB.NET's way of handling custom events), but apparently it's a little-used feature in practice. VB.NET, it seems, *does* make use of this accessor. AFAIK, C# doesn't support it. – stakx - no longer contributing Feb 18 '12 at 17:56
  • 1
    Ah, I was wondering. I still don't quite understand the disconnect between the C# and VB.NET departments at Microsoft. – jnm2 Feb 18 '12 at 18:19
5

In C#, there isn't any RaiseEvent block. You would do the same thing by creating a method for raising your event.

Here is a working example. In the C# version, you do not even need to use the add and remove block -- you can use the default implementation for that and just create a custom raise method that raises your event.

Below is a working program (the form is just a Windows Forms form with a single button on it).

// Here is your event-raising class
using System;
using System.ComponentModel;

namespace ClassLibrary1
{
    public class Class1
    {
        public ISynchronizeInvoke EventSyncInvoke { get; set; }
        public event EventHandler TestEvent;


        private void RaiseTestEvent(EventArgs e)
        {
            // Take a local copy -- this is for thread safety.  If an unsubscribe on another thread
            // causes TestEvent to become null, this will protect you from a null reference exception.
            // (The event will be raised to all subscribers as of the point in time that this line executes.)
            EventHandler testEvent = this.TestEvent;

            // Check for no subscribers
            if (testEvent == null)
                return;

            if (EventSyncInvoke == null)
                testEvent(this, e);
            else
                EventSyncInvoke.Invoke(testEvent, new object[] {this, e});
        }

        public void Test()
        {
            RaiseTestEvent(EventArgs.Empty);
        }
    }
}

// Here is a form that tests it -- if you run it, you will see that the event is marshalled back to
// the main thread, as desired.
using System;
using System.Threading;
using System.Windows.Forms;

namespace ClassLibrary1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            this.TestClass = new Class1();
            this.TestClass.EventSyncInvoke = this;
            this.TestClass.TestEvent += new EventHandler(TestClass_TestEvent);
            Thread.CurrentThread.Name = "Main";
        }

        void TestClass_TestEvent(object sender, EventArgs e)
        {
            MessageBox.Show(this, string.Format("Event.  Thread: {0} Id: {1}", Thread.CurrentThread.Name, Thread.CurrentThread.ManagedThreadId));
        }

        private Class1 TestClass;

        private void button1_Click(object sender, EventArgs e)
        {
            // You can test with an "old fashioned" thread, or the TPL.
            var t = new Thread(() => this.TestClass.Test());
            t.Start();
            //Task.Factory.StartNew(() => this.TestClass.Test());
        }
    }
}
StayOnTarget
  • 11,743
  • 10
  • 52
  • 81
JMarsch
  • 21,484
  • 15
  • 77
  • 125
2

You simply can't. But since events can be raised only from inside the type that declares them, you can create a helper method that executes your specific raising code. And then just make sure you don't raise the event directly outside that method.

svick
  • 236,525
  • 50
  • 385
  • 514
1

AFAIK custom event raising as in VB.NET does not exist in C#. However, you could wrap the actual event handler delegates (passed to add as value) in a lambda and subscribe that lambda to the event instead of the original delegate:

add 
{ 
    testEventDelegate = Delegate.Combine(testEventDelegate, (s, e) => { ... } ) 
} 

(Above code untested, syntax might be slightly off. I'll fix it as soon as I can test it.)


Crude, but working example:

The following is a concrete example of the above. I am not convinced myself that the following is good, solid code, nor that it would work in all circumstances (such as multi-threading etc.)... nevertheless, here it is:

class Foo
{
    public Foo(SynchronizationContext context)
    {
        this.context = context ?? new SynchronizationContext();
        this.someEventHandlers = new Dictionary<EventHandler, EventHandler>();
    }

    private readonly SynchronizationContext context;
    // ^ could also use ISynchronizeInvoke; I chose SynchronizationContext
    //   for this example because it is independent from, but compatible with,
    //   Windows Forms.

    public event EventHandler SomeEvent
    {
        add
        {
            EventHandler wrappedHandler = 
                (object s, EventArgs e) =>
                {
                    context.Send(delegate { value(s, e); }, null);
                    // ^ here is where you'd call ISynchronizeInvoke.Invoke().
                };
            someEvent += wrappedHandler;
            someEventHandlers[value] = wrappedHandler;
        }
        remove
        {
            if (someEventHandlers.ContainsKey(value))
            {
                someEvent -= someEventHandlers[value];
                someEventHandlers.Remove(value);
            }
        }
    }
    private EventHandler someEvent = delegate {};
    private Dictionary<EventHandler, EventHandler> someEventHandlers;

    public void RaiseSomeEvent()
    {
        someEvent(this, EventArgs.Empty);
        // if this is actually the only place where you'd invoke the event,
        // then you'd have far less overhead if you moved the ISynchronize-
        // Invoke.Invoke() here and forgot about all the wrapping above...!
    }
}

(Note that I've used the C# 2 anonymous delegate {} syntax for brevity.)

stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
  • 2
    How would I remove the delegate? With a second lambda? – jnm2 Feb 18 '12 at 15:51
  • @jnm2, good point. You might have to keep a `Dictionary` for mapping delegates to lambda functions so that you can remove lambda from the event. – stakx - no longer contributing Feb 18 '12 at 15:53
  • Even without the dictionary,Me.Invoking each delegate separately is a lot of overhead. You gave me a good idea though. I should keep an non-Me.Invoked copy of the delegate and add and remove from that, and set testEventDelegate to a new lambda. – jnm2 Feb 18 '12 at 15:58
  • @jnm2, it might be worth testing whether the overhead is actually an issue or not (esp. if you feel that it might solve your problem). – stakx - no longer contributing Feb 18 '12 at 16:01
  • @jnm2, concerning your (now deleted) question comment: I believe this is the main point of JMarsch's and svick's answer... Since you can only raise an event inside the type that declares is, if you want that event to be publicly raiseable, you'll need a public method for doing just that. See also the last bit in my code example. – stakx - no longer contributing Feb 18 '12 at 16:21
  • I totally did not understand what was going on or what the custom add/remove was doing. I was still thinking of TestEvent as a delegate itself. Now that I've seen the light, I'm going to make sure that other poor souls get a round explanation. – jnm2 Feb 18 '12 at 17:06