Reputation: 405
I'm not sure if you're familiar with the NerdDinner application. It 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.
What I'd like to do is create a HandleRuleViolationExceptionAttribute, similar to the HandleExceptionAttribute (which is part of the MVC Framework). The problem is that this attribute has to repopulate the View's Modelstate.
A view can have any object type for its model. The code that throws the RuleViolationException fills sets the RuleViolationException.Object to the View's Model.
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 HandleRuleViolationExceptionAttribute
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 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
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
To fill the View's Model 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, possibly resulting in security problems.
Is there a better way of constructing the ViewData object in the OnException method, that works similarly to the UpdateModel-method of the controller? Is there a way to invoke the UpdateModel-method from the ExceptionHandlerAttribute?
Thanks, Guillaume Hanique
Upvotes: 1
Views: 2300
Reputation: 405
Got it!
Dim methodInfo = GetType(Controller).GetMethod("View", _
Reflection.BindingFlags.NonPublic Or Reflection.BindingFlags.Instance, Nothing, _
New Type() {GetType(Object)}, Nothing)
Dim controller = DirectCast(filterContext.Controller, Controller)
Dim viewResult As ViewResult = _
CType(methodInfo.Invoke(controller, New Object() {ex.Object}), ViewResult)
Dim viewData = viewResult.ViewData
For Each ruleViolation In ex.Object.GetRuleViolations()
viewData.ModelState.AddModelError( _
ruleViolation.PropertyName, ruleViolation.ErrorMessage)
Next
filterContext.Result = viewResult
In my case I know that filterContext.Controller always derives from Controller when this HandleRuleViolationsAttribute is used. In the Controller the ModelState is set by calling return View(theObject). The View method is protected, though, so in the HandleRuleViolationsAttribute I invoke it using reflection, which gives me a ViewResult instance with the ModelState correctly initialized. I can then add the RuleViolations to the ModelState using the AddModelError-method. I assign that viewResult to filterContext.Result to have it displayed.
Upvotes: 0
Reputation: 1053
Couple quick points:
1. You actually want to update the Controller's ModelState (which the View has access to as a property)
2. You want to set the result to a View where you pass in the model object even though it's invalid
From what you describe, it seems that you should be calling the UpdateModel method of the controller. You can do this from your OnException method by doing this:
filterContext.Controller.UpdateModel(ex.Object)
...
For Each ruleViolation In ex.Object.GetRuleViolations()
filterContext.Controller.ModelState.AddModelError(ruleViolation.PropertyName, ruleViolation.ErrorMessage)
Next
...
filterContext.Result = filterContext.Controller.View(ex.Object)
You might consider exposing a property called "ViewName" on the attribute so the user can specify an alternate View to use in the case of an exception:
<HandleRuleViolationException(ViewName:="SomeErrorViewForThisControllerOrAction")>
This is a pretty neat idea. Please come back and update the post, mark answer, or comment as to the outcome. I'm very curious as to how this works out!
Upvotes: 1