The NerdDinner application adds a method GetRuleViolations() and a property IsValid to the Dinner object. When the object is saved, it checks whether the object is valid. If it isn't, it throws an exception. In the controller that exception is caught, the
ViewData's ModelState is filled with the rule violations and the view is redisplayed. The Html.Validation helpers highlight the errors.
In my application I don't use a SQL database, but I use the Db4o-assembly to store data. Furthermore, I created an interface IValidatable, that is implemented by all objects that must be stored, one of which is the Guest object with a FirstName and LastName
property. IValidatable has the GetRuleViolations() function and the IsValid property. I created a wrapper around the Db4o-container that only stores IValidatable objects. Before it stores an object, it checks if it is valid. If it isn't, an ObjectIsInvalidException
is thrown, which includes the object.
I could catch this exception in the post-Create action of the GuestsController and add the rule violations to the ModelState. However, wouldn't it be better if I could simply add a HandleRuleViolation-attribute to the action method that does do this for
me transparently for any model-object that implements IValidatable?
I looked up the code for the HandleExceptionAttribute in the MVC source code and modified it:
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Method, _
Inherited:=True, AllowMultiple:=False)> _
Public Class HandleRuleViolationAttribute
Inherits FilterAttribute
Implements IExceptionFilter
Private m_View As String
Private m_MasterPage As String
Public Property View() As String
Get
Return m_View
End Get
Set(ByVal value As String)
m_View = value
End Set
End Property
Public Property MasterPage() As String
Get
Return If(m_MasterPage, String.Empty)
End Get
Set(ByVal value As String)
m_MasterPage = value
End Set
End Property
Public Sub OnException(ByVal filterContext As System.Web.Mvc.ExceptionContext) _
Implements System.Web.Mvc.IExceptionFilter.OnException
If filterContext Is Nothing Then
Throw New ArgumentException("filterContext is null")
End If
'Ignore if the error is already handled.
If filterContext.ExceptionHandled Then Return
'Handle only ObjectIsInvalidExceptions.
If Not TypeOf filterContext.Exception Is ObjectIsInvalidException Then
Return
End If
Dim ex As ObjectIsInvalidException = DirectCast(filterContext.Exception, ObjectIsInvalidException)
'If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method),
'ignore it.
If (New HttpException(Nothing, ex).GetHttpCode()) <> 500 Then Return
Dim actionName As String = CStr(filterContext.RouteData.Values("action"))
Dim viewName As String = If(String.IsNullOrEmpty(View), actionName, View)
Dim viewData = filterContext.Controller.ViewData
viewData.Model = ex.Object
For Each ruleViolation In ex.Object.GetRuleViolations()
viewData.ModelState.AddModelError(ruleViolation.PropertyName, ruleViolation.ErrorMessage)
Next
filterContext.Result = New ViewResult() With _
{ _
.ViewName = viewName, _
.MasterName = MasterPage, _
.ViewData = viewData, _
.TempData = filterContext.Controller.TempData _
}
filterContext.ExceptionHandled = True
filterContext.HttpContext.Response.Clear()
filterContext.HttpContext.Response.StatusCode = 500
'Certain versions of IIS will sometimes use their own error page when
'they detect a server error. Setting this property indicates that we
'want it to try to render ASP.NET MVC's error page instead.
filterContext.HttpContext.Response.TrySkipIisCustomErrors = True
End Sub
End Class
I added this attribute to my Create-action:
<AcceptVerbs(HttpVerbs.Post)> _ <HandleRuleViolation()> _ Function Create(<Bind(exclude:="ID")> ByVal guest As Guest) As ActionResult myRepository.AddGuest(guest) Return RedirectToAction("Index") End Function
If I save the guest object with an empty FirstName for example, the OnException method is actually called! The ViewData is retrieved from the FilterContext, the model (the guest-instance) is assigned to ViewData's Model-property and the RuleViolations are added to the ViewData's ModelState-property. A new ViewResult is generated with this ViewData, which results in redisplaying the Create-view. This all works quite well until line 3 in the Create-view:
Executing this line results in a NullReferenceException. I linked the MVC source code to my project and eventually found that line 4 generates the exception (file: LinkExtensions.cs):
internal object GetModelStateValue(string key, Type destinationType) { ModelState modelState; if (ViewData.ModelState.TryGetValue(key, out modelState)) { return modelState.Value.ConvertTo(destinationType, null/* culture */); } return null; }
TryGetValue(key, out modelState) succeeds because I added a ModelError for it. modelState.Value.ConvertTo(..) does not, because it's null! The reason is that filterContext.Controller.ViewData contains 0 items. I can add the ModelState Errors to it, but the model's fields are not!
I eventually solved this with the following code in the OnException method (specifically lines 3, 4 and 5):
Dim viewData = filterContext.Controller.ViewData viewData.Model = ex.Object For Each item As String In filterContext.HttpContext.Request.Form.Keys viewData.Add(item, filterContext.HttpContext.Request.Form.Item(item)) Next For Each ruleViolation In ex.Object.GetRuleViolations() viewData.ModelState.AddModelError(ruleViolation.PropertyName, ruleViolation.ErrorMessage) Next
I iterate over the request's form keys and add the key and its value to the ViewData instance. It now works, however, I don't believe this is the way to do it. In the Controller's Action method I could update the model with the UpdateModel-method. This also
updates the viewStates ModelState. I can include an array of strings with the property names that must be updated, or, when having the model as an Action parameter, I could use the Bind-attribute to in- or exclude some properties (as I do in the create-action
above). My method does not adhere to this.
Is there a better way of constructing the ViewData object in the OnException method, that works similarly to the UpdateModel-method of the controller? (Any other comments are very welcome too!)
ansvlug
Member
50 Points
11 Posts
How to create ViewData for an Exception Filter
Jun 16, 2009 07:28 PM|LINK
The NerdDinner application adds a method GetRuleViolations() and a property IsValid to the Dinner object. When the object is saved, it checks whether the object is valid. If it isn't, it throws an exception. In the controller that exception is caught, the ViewData's ModelState is filled with the rule violations and the view is redisplayed. The Html.Validation helpers highlight the errors.
In my application I don't use a SQL database, but I use the Db4o-assembly to store data. Furthermore, I created an interface IValidatable, that is implemented by all objects that must be stored, one of which is the Guest object with a FirstName and LastName property. IValidatable has the GetRuleViolations() function and the IsValid property. I created a wrapper around the Db4o-container that only stores IValidatable objects. Before it stores an object, it checks if it is valid. If it isn't, an ObjectIsInvalidException is thrown, which includes the object.
I could catch this exception in the post-Create action of the GuestsController and add the rule violations to the ModelState. However, wouldn't it be better if I could simply add a HandleRuleViolation-attribute to the action method that does do this for me transparently for any model-object that implements IValidatable?
I looked up the code for the HandleExceptionAttribute in the MVC source code and modified it:
<AttributeUsage(AttributeTargets.Class Or AttributeTargets.Method, _ Inherited:=True, AllowMultiple:=False)> _ Public Class HandleRuleViolationAttribute Inherits FilterAttribute Implements IExceptionFilter Private m_View As String Private m_MasterPage As String Public Property View() As String Get Return m_View End Get Set(ByVal value As String) m_View = value End Set End Property Public Property MasterPage() As String Get Return If(m_MasterPage, String.Empty) End Get Set(ByVal value As String) m_MasterPage = value End Set End Property Public Sub OnException(ByVal filterContext As System.Web.Mvc.ExceptionContext) _ Implements System.Web.Mvc.IExceptionFilter.OnException If filterContext Is Nothing Then Throw New ArgumentException("filterContext is null") End If 'Ignore if the error is already handled. If filterContext.ExceptionHandled Then Return 'Handle only ObjectIsInvalidExceptions. If Not TypeOf filterContext.Exception Is ObjectIsInvalidException Then Return End If Dim ex As ObjectIsInvalidException = DirectCast(filterContext.Exception, ObjectIsInvalidException) 'If this is not an HTTP 500 (for example, if somebody throws an HTTP 404 from an action method), 'ignore it. If (New HttpException(Nothing, ex).GetHttpCode()) <> 500 Then Return Dim actionName As String = CStr(filterContext.RouteData.Values("action")) Dim viewName As String = If(String.IsNullOrEmpty(View), actionName, View) Dim viewData = filterContext.Controller.ViewData viewData.Model = ex.Object For Each ruleViolation In ex.Object.GetRuleViolations() viewData.ModelState.AddModelError(ruleViolation.PropertyName, ruleViolation.ErrorMessage) Next filterContext.Result = New ViewResult() With _ { _ .ViewName = viewName, _ .MasterName = MasterPage, _ .ViewData = viewData, _ .TempData = filterContext.Controller.TempData _ } filterContext.ExceptionHandled = True filterContext.HttpContext.Response.Clear() filterContext.HttpContext.Response.StatusCode = 500 'Certain versions of IIS will sometimes use their own error page when 'they detect a server error. Setting this property indicates that we 'want it to try to render ASP.NET MVC's error page instead. filterContext.HttpContext.Response.TrySkipIisCustomErrors = True End Sub End ClassI added this attribute to my Create-action:
If I save the guest object with an empty FirstName for example, the OnException method is actually called! The ViewData is retrieved from the FilterContext, the model (the guest-instance) is assigned to ViewData's Model-property and the RuleViolations are added to the ViewData's ModelState-property. A new ViewResult is generated with this ViewData, which results in redisplaying the Create-view. This all works quite well until line 3 in the Create-view:
Executing this line results in a NullReferenceException. I linked the MVC source code to my project and eventually found that line 4 generates the exception (file: LinkExtensions.cs):
TryGetValue(key, out modelState) succeeds because I added a ModelError for it. modelState.Value.ConvertTo(..) does not, because it's null! The reason is that filterContext.Controller.ViewData contains 0 items. I can add the ModelState Errors to it, but the model's fields are not!
I eventually solved this with the following code in the OnException method (specifically lines 3, 4 and 5):
I iterate over the request's form keys and add the key and its value to the ViewData instance. It now works, however, I don't believe this is the way to do it. In the Controller's Action method I could update the model with the UpdateModel-method. This also updates the viewStates ModelState. I can include an array of strings with the property names that must be updated, or, when having the model as an Action parameter, I could use the Bind-attribute to in- or exclude some properties (as I do in the create-action above). My method does not adhere to this.
Is there a better way of constructing the ViewData object in the OnException method, that works similarly to the UpdateModel-method of the controller? (Any other comments are very welcome too!)
Thanks,
Kind regards,
Ans Vlug
asp asp.net mvc actionfilters