I have a class which is a disposable UI Control. It subscribes to changes of a moldel object to redraw it's content.
On the other hand, under circumstances, some special change of the same model object instructs the view containing this control to remove and dispose it (the control).
As a result a change in a model, depending on subscription order - first causes the control disposal and afterwards it's method invocation - which ends up with ObjectDisposedException
.
Question: Should the control be designed to safely ignore event callbacks or should we try to prevent this kind of invocation from other layers?
For those who prefer to see more code then words I have prepared a very simplified example:
//############################################
class View
{
private Control m_Control;
public View(Logic logic, Model model)
{
m_Control = new Control(model);
logic.Changed += LogicChanged;
}
private void LogicChanged(object sender, EventArgs e)
{
m_Control.Dispose();
m_Control = null;
}
}
//############################################
class Control : IDisposable
{
private readonly Model m_Model;
public Control(Model model)
{
m_Model = model;
m_Model.Changed += ModelOnChanged;
}
public bool IsDisposed { get; private set; }
public void Dispose()
{
m_Model.Changed -= ModelOnChanged;
IsDisposed = true;
}
private void ModelOnChanged(object sender, EventArgs e)
{
if (IsDisposed)
{
throw new ObjectDisposedException(ToString());
}
//Do something
}
}
//############################################
class Model
{
public event EventHandler<EventArgs> Changed;
private void OnChanged(EventArgs e)
{
EventHandler<EventArgs> handler = Changed;
if (handler != null)
handler(this, e);
}
public void Change()
{
OnChanged(null);
}
}
//############################################
class Logic
{
private readonly Model m_Model;
public Logic(Model model)
{
m_Model = model;
m_Model.Changed += ModelOnChanged;
}
private void ModelOnChanged(object sender, EventArgs e)
{
OnChanged(null);
}
public event EventHandler<EventArgs> Changed;
private void OnChanged(EventArgs e)
{
EventHandler<EventArgs> handler = Changed;
if (handler != null)
handler(this, e);
}
}
//############################################
class Program
{
private static void Main(string[] args)
{
var model = new Model();
var logic = new Logic(model);
var view = new View(logic, model);
model.Change();
//And crash!
}
}
Where would you propose a fix in given example? Model
and Logic
classes are just doing their business without knowing about the order of event subscription. I see also no design flaw in View
and Control
implementations.
Imagine there are three different teams implementing Model
, Logic
and UI
and there are not just these four components, but hundreds of them. The issue can occur everywhere.
What I am looking for is not a local fix in this particular case, but I want find a pattern to prevent that. For instance: "Controls must gracefully ignore event calls on disposed instances" or "Logic must prevent subscriptions on model, only UI is allowed to do so." etc.
In addition to accpeted answer
Yes, disposed object event callback should not throw an exception. Even more generally :
... event handlers are required to be robust in the face of being called even after the event has been unsubscribed.
There are number of reasons for that - see Eric Lippert’s wonderful article Events and Races