views:

185

answers:

5

Suppose I have a method which changes the state of an object, and fires an event to notify listeners of this state change:

public class Example
{
   public int Counter { get; private set; }

   public void IncreaseCounter()
   {
      this.Counter = this.Counter + 1;
      OnCounterChanged(EventArgs.Empty);
   }

   protected virtual void OnCounterChanged(EventArgs args)
   {
      if (CounterChanged != null)
         CounterChanged(this,args);
   }

   public event EventHandler CounterChanged;
}

The event handlers may throw an exception even if IncreaseCounter successfully completed the state change. So we do not have strong exception safety here:

The strong guarantee: that the operation has either completed successfully or thrown an exception, leaving the program state exactly as it was before the operation started.

Is it possible to have strong exception safety when you need to raise events?

A: 

Will not simply swapping the order of the two lines in IncreaseCounter enforcestrong exception safety for you?

Or if you need to increment Counter before you raise the event:

public void IncreaseCounter()
{
    this.Counter += 1;

    try
    {
        OnCounterChanged(EventArgs.Empty);
    }
    catch
    {
        this.Counter -= 1;

        throw;
    }
}

In a situation where you have more complex state changes (side effects) going on, then it becomes rather more complicated (you might have to clone certain objects for example), but in for this example it would seem quite easy.

Noldorin
the only problem with this is that the value of the counter will be as it was before it was incremented (which would not be the expected value for most developers)
Alan McBee
@Alan: Yeah, I just considered that. See my edited version.
Noldorin
Also, don't forget to propagate the exception. Currently this neither completes successfully or raises an error to a caller.
workmad3
+2  A: 

The general pattern you will see for frameworks is:

public class Example
{
   public int Counter { get; private set; }

   public void IncreaseCounter()
   {
      OnCounterChanging(EventArgs.Empty);
      this.Counter = this.Counter + 1;
      OnCounterChanged(EventArgs.Empty);
   }

   protected virtual void OnCounterChanged(EventArgs args)
   {
      if (CounterChanged != null)
         CounterChanged(this, args);
   }

   protected virtual void OnCounterChanging(EventArgs args)
   {
      if (CounterChanging != null)
         CounterChanging(this, args);
   }

   public event EventHandler<EventArgs> CounterChanging;
   public event EventHandler<EventArgs> CounterChanged;
}

If a user would like to throw an exception to prevent the changing of the value then they should be doing it in the OnCounterChanging() event instead of the OnCounterChanged(). By definition of the name (past tense, suffix of -ed) that implies the value has been changed.

Edit:

Note that you generally want to stay away from copious amounts of extra try..catch/finally blocks as exception handlers (including try..finally) are expensive depending on the language implementation. i.e. The win32 stack framed model or the PC-mapped exception model both have their pros and cons, however if there are too many of these frames they will both be costly (either in space or execution speed). Just another thing to keep in mind when creating a framework.

Adam Markowitz
This would be no better for ensuring strong exception safety than a convention saying 'dont throw exceptions in the event handlers'. It doesn't actually stop someone from breaking the system by throwing exceptions where they shouldn't. Whether that's truly an issue or not is different :)
workmad3
Exceptions are a useful mechanism, and the cost of try/catch is minimal when there are no exceptions. Exceptions should never be used for normal control flow, but I don't think that's what the OP is asking.
Robert Paulson
try..catch cost is minimal for stack frame based exceptions when there are no exceptions. PC-mapped exceptions have a size cost associated with them so copious amounts of these types of exception handlers could start to bloat your DS.
Adam Markowitz
just nit picking, but you should save the EventHandler<T> to a local variable first to prevent a race condition. var localHandler = CounterChanging; if( handler != null ) handler(this, args);
Robert Paulson
A: 

In this case, I think you should take a cue from Workflow Foundation. In the OnCounterChanged method, surround the call to the delegate with a generic try-catch block. In the catch handler, you'll have to do what is effectively a compensating action -- rolling back the counter.

Alan McBee
+1  A: 

Well, you could compensate if it was critical:

public void IncreaseCounter()
{
  int oldCounter = this.Counter;
  try {
      this.Counter = this.Counter + 1;
      OnCounterChanged(EventArgs.Empty);
  } catch {
      this.Counter = oldCounter;
      throw;
  }
}

Obviously this would be better if you can talk to the field (since assigning a field won't error). You could also wrap this up in bolierplate:

void SetField<T>(ref T field, T value, EventHandler handler) {
    T oldValue = field;
    try {
         field = value;
         if(handler != null) handler(this, EventArgs.Empty);
    } catch {
         field = oldField;
         throw;
    }
}

and call:

SetField(ref counter, counter + 1, CounterChanged);

or something similar...

Marc Gravell
I've considered the rollback solution but if there are event handlers who already successfully handled the change, then they would have to know that it was rolled back. Then I would have to raise the event again for the rollback, but then the same exception could be thrown again etc.
Wim Coenen
+3  A: 

To prevent an exception in a handler from propagating to the event generator, the answer is to manually invoke each item in the MultiCast Delegate (i.e. the event handler) inside of a try-catch

All handlers will get called, and the exception won't propagate.

public EventHandler<EventArgs> SomeEvent;

protected void OnSomeEvent(EventArgs args)
{
    var handler = SomeEvent;
    if (handler != null)
    {
        foreach (EventHandler<EventArgs> item in handler.GetInvocationList())
        {
            try
            {
                item(this, args);
            }
            catch (Exception e)
            {
                // handle / report / ignore exception
            }
        }                
    }
}

What remains is for you to implement the logic for what do do when one or more event recipients throws and the others don't. The catch() could catch a specific exception as well and roll back any changes if that is what makes sense, allowing the event recipient to signal the event source that an exceptional situation has occurred.

As others point out, using exceptions as control flow isn't recommended. If it's truly an exceptional circumstance, then by all means use an exception. If you're getting a lot of exceptions you probably want to use something else.

Robert Paulson
In other words, achieving strong exception safety in methods that raise events comes at the cost of having to swallow/log exceptions. That's almost always a bad thing, so I concluded that achieving strong exception safety is probably not worth it.
Wim Coenen