How do I catch an exception raised on my ASCX control (not the code-behind)?
I have a large ASPX page with many ASCX controls. If a control throws an exception, it should log the exception and hide only itself. All the other controls should still render.
How do I handle exceptions on individual ASCX's raised from the front-end file开发者_开发知识库 (the ASCX and not the code-behind)? for example: a control trying to reference an invalid property using the <%= MethodThatThrowsANullReferenceException() %>
syntax.
Obviously using the generic error handler method in Global.asax won't solve the problem. I need to handle exceptions on individual controls.
Make all your UserControls inherit from a custom base class, like such:
public class CustomUserControl : UserControl
{
protected override void Render(HtmlTextWriter writer)
{
try
{
base.Render(writer);
}
catch (Exception e)
{
writer.Write("Could not load control. Sad face.");
}
}
}
I tried overriding Render method but this doesn't cover all exceptions.
For example, if some kind of exception is thrown during Page_Init, Load or Render, this will prevent the page from rendering.
We have different people working on different modules (controls) that can be loaded into a single Page, but I'm not responsible for the quality of the code of each module, so even if it's not best practice, i needed to catch exceptions and identify which control fails to load, because the application can't fail just because one module does.
For this particular scenario that is not so rare nowadays, neither custom, application or page error handling will work well.
The solution I've come up was:
Each Module (Control.ascx) when needs to be loaded into the Page (aspx) , is contained into a ModuleShell that will hold some specific features and will be responsible for helping the Page_Error handling to work properly.
This ModuleShell , instead of trying to trap the exception of its child control that failed, will just monitor in each life cycle stage if it managed to Load properly.
Here's an snippet of it:
protected void Page_Init(object sender, EventArgs e)
{
Modules.CurrentState = _mod;
}
protected void Page_Load(object sender, EventArgs e)
{
Modules.CurrentState = _mod;
}
protected void Page_PreRender(object sender, EventArgs e)
{
Modules.CurrentState = _mod;
}
Modules is a static class used to store session variables. CurrentState is a variable that ModuleShell use to record their names in.
The Page_Error located in the only aspx we got, will get the last recorded ModuleShell that tried to load. Since any exception will stop page rendering, the last ModuleShell to record its name to the main Page, it's probably the one that failed to load properly.
It's a sloppy solution but it's transparent to the Module Developer.
AFAIK, this is not possible (at least in an easy way).
Rich Custom Error Handling with ASP.NET:
When errors happen, an exception is raised or thrown. There are three layers at which you may trap and deal with an exception: in a
try...catch...finally
block, at thePage
level, or at theApplication
level. The first two happen right inside a page's code, and code for application events is kept insideglobal.asax
.The
Exception
object contains information about the error, and as the event bubbles up through the layers, it is wrapped in further detail. In rough terms, theApplication_Error
exception contains thePage_Error
exception, which expands on the baseException
, which triggered the bubbling in the first place.
If there is an exception occured inside the user control, the only way to catch it inside the user control is to handle it inside a try { } catch { }
block.
I think the lowest level when the exception like this could be caught is the next - Page_Error
level like this:
protected void Page_Error(object sender, EventArgs e)
{
// the control which throw an exception
var control = (Control)sender;
control.Visible = false;
// the exception itself
var exception = Server.GetLastError();
Context.ClearError();
}
the Context.ClearError()
method is even preventing an exception from bubbling up further on to Application_Error
. But unfortunatelly, then unhandled exception is thrown the page processing stops and error processing is started instead. This means the render of page will stop too (so you won't see the controls next to that which caused this exception).
You could wrap the method you are trying to call in your own method that will return the same type, but with a try {} catch {}
block.
public string MethodWrapper()
{
try
{
return MethodThatCanThrowException();
}
catch (SomeExceptionType)
{
//log exception
return string;
}
}
One option, as suggested by Jim Bolla was to make all controls inherit from the same base class and use a Try/Catch in the Render method. This would have worked. Unfortunately many of the controls I am dealing with already have different base classes.
This solution worked for me:
I added the following code to each user control (I'm sure this can be refactored further to reduce duplication):
#region Error Handling
public event EventHandler ControlCrashed;
private static readonly log4net.ILog Logger = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
protected override void RenderChildren(HtmlTextWriter writer)
{
try
{
base.RenderChildren(writer);
}
catch (Exception exc)
{
Logger.Error("Control failed to load. Hiding control. Message: " + exc, exc);
//Ignore and hide the control.
this.Visible = false;
if (ControlCrashed != null)
ControlCrashed(this, EventArgs.Empty);
}
}
#endregion
This catches any front-end rendering problems. The parent page can handle the ControlCrashed event if it wishes to display a nice error message.
精彩评论