Mark Gilbert's Blog

Science and technology, served light and fluffy.

What is YOUR quest? ASP.NET MVC 2 Model Binding and Conditional Validation

In my MVC project, I had a model object that had a couple of foreign keys.  These were represented on the form as drop down lists that returned the ID of the item selected.  The one that was causing all of the problems was "Status", so I’ll simply refer to that one from here on out.

The form would post the ID of the Status selected.  My model class, however, wasn’t expecting an ID, though – it was a Castle ActiveRecord object, so it wanted an object of type "Status".  After a few failed attempts to try to convert the ID to an object in the controller action, I finally stumbled onto using a custom model binder.  This was a class that you attach to your model object (via a class-level attribute), and provides methods you can override to perform custom binding.

So, first, a simplified version of my model class:

 

Imports System.ComponentModel.DataAnnotations

<ModelBinder(GetType(EventBinder))> _
Public Class EventDetail

    Private _CurrentEvent As BusinessServices.CorporateEvent

    Public ReadOnly Property ID() As Long
        Get
            If (IsNothing(Me.CurrentEvent)) Then Return 0
            Return Me.CurrentEvent.ID
        End Get
    End Property

    Public Property CurrentEvent() As BusinessServices.CorporateEvent
        Get
            Return Me._CurrentEvent
        End Get
        Set(ByVal value As CorporateEvent)
            Me._CurrentEvent = value
        End Set
    End Property
End Class


And the initial model binder I used:

 

Public Class EventBinder
    Inherits DefaultModelBinder

    Public Overrides Function BindModel(ByVal controllerContext As System.Web.Mvc.ControllerContext, ByVal bindingContext As System.Web.Mvc.ModelBindingContext) As Object
        Dim MyModel As EventDetail

        MyModel = CType(MyBase.BindModel(controllerContext, bindingContext), EventDetail)

        If (controllerContext.HttpContext.Request.Form.AllKeys.Contains("CurrentEvent.EventStatus.StatusID")) Then
            MyModel.CurrentEvent.EventStatus = Status.GetByID(Convert.ToInt64(controllerContext.HttpContext.Request.Form("CurrentEvent.EventStatus.StatusID")))
        End If

        ' Do some other custom binding work here... 
        Return MyModel
    End Function
End Class

I set a breakpoint in both my controller action that handled the postback, and in EventBinder.BindModel.  What I found was that BindModel happened before my controller action was event invoked.  That explained why all of my attempts to capture this in the controller action failed (in particular, the object was always invalid by the time the controller action got ahold of it).  The binding was simply happening earlier in the call stack.  So, using the custom binder, I could translate ID to object.

And there was much rejoicing.

***

Fast forward a couple of weeks, and I found myself pressing my luck.  All of the validation that I had done up to this point was using the out-of-the-box "Required" attributes on the CorporateEvent business class properties.  For example:

 

Imports Castle.ActiveRecord
Imports System
Imports System.ComponentModel.DataAnnotations
Imports System.ComponentModel

<ActiveRecord(Table:="CorporateEvent"), _
 Serializable()> _
Public Class CorporateEvent
    Inherits ActiveRecordBase(Of CorporateEvent)

    Private _ID As Long
    Private _Title As String
    Private _OccursOn As DateTime
    Private _EventStatus As Status


    <PrimaryKey("ID")> _
    Public Property ID() As Long
        Get
            Return Me._ID
        End Get
        Set(ByVal value As Long)
            Me._ID = value
        End Set
    End Property

    <[Property](), _
     Required(ErrorMessage:="Title is required."), _
     StringLength(200, ErrorMessage:="Maximum event title length is 200 characters.")> _
    Public Property Title() As String
        Get
            Return Me._Title
        End Get
        Set(ByVal value As String)
            Me._Title = value
        End Set
    End Property

    <[Property](), _
     Required(ErrorMessage:="Occurs On date/time is required.")> _
    Public Property OccursOn() As DateTime
        Get
            Return Me._OccursOn
        End Get
        Set(ByVal value As DateTime)
            Me._OccursOn = value
        End Set
    End Property


    <BelongsTo("StatusID"), _
     DisplayName("Status")> _
    Public Property EventStatus() As Status
        Get
            Return Me._EventStatus
        End Get
        Set(ByVal value As Status)
            Me._EventStatus = value
        End Set
    End Property
End Class

Now what I wanted to do was have specific fields in my model BECOME required when the Status was switched to Published.  In other words, I wanted "conditional validation".  I found a wonderful post and code sample from Simon Ince that does just that.  He creates a custom attribute called RequiredIf that makes the property required if another property has the named value.  For example:

 

Imports Castle.ActiveRecord
Imports System
Imports System.ComponentModel.DataAnnotations
Imports System.ComponentModel
Imports ConditionalValidation.Validation

<ActiveRecord(Table:="CorporateEvent"), _
 Serializable()> _
Public Class CorporateEvent

    ...

    <[Property](), _
     RequiredIf("IsPublished", True, ErrorMessage:="Details Title is required for Published events."), _
     StringLength(100, ErrorMessage:="Maximum length is 100 characters.")> _
    Public Property TitleDetails() As String
        Get
            Return Me._TitleDetails
        End Get
        Set(ByVal value As String)
            Me._TitleDetails = value
        End Set
    End Property


    Public ReadOnly Property IsPublished() As Boolean
        Get
            If (IsNothing(Me.EventStatus)) Then Return False
            Return (Me.EventStatus.ID = Status.Statuses.Published)
        End Get
    End Property

End Class


So, I took his logic, and dropped it into a new project in my solution.  The property I wanted to evaluate was EventStatus.ID, but the attribute didn’t know how to evaluate "EventStatus.ID", so I created a boolean readonly property in CorporateEvent called "IsPublished" that did this evaluation for me.  I could now compare against True and False in the RequiredIf attribute.

Finally, I had to register the attribute in my Global.asax / Application_Start routine, like so:

 

DataAnnotationsModelValidatorProvider.RegisterAdapter(GetType(RequiredIfAttribute), GetType(RequiredIfValidator))


I wrote a unit test for one of the CorporateEvent properties (where EventStatus was Published, but my property wasn’t specified yet), and ran it.  Normally, the logic should have thrown it out with a validation error, but it did not.  Great!  My test failed!  I then wired up the RequiredIf attribute to that property, and re-ran the test.  It still failed.

Oh, duh.  I didn’t register the custom attribute in my test fixture – no wonder it wasn’t recognizing it.  I copied the same registration line I used in Application_Start to my TestFixtureSetUp method.  I ran the test again.  It still failed.

And so began two days of gesturing at the monitor.

I won’t bore you with all of my failed attempts and rabbit trails.  At the beginning of the second day, however, I realized that the only way I was going to get this thing to work is by reverting everything I had done up to that point, and starting over – this time dropping the pieces into the project in much smaller increments so I could see where it failed.  (Did I mention that I had dozens of tests breaking by the end of my initial pass at this, and I hadn’t the foggiest idea WHY they were breaking?!?)

By the time I had my breakthrough at the end of the second day, I was VERY close to scrapping the whole approach and doing the RequiredIf validation in the controller action.  I kept forcing myself forward, though, because I really wanted to do it right, and there didn’t seem to be any good reason why it shouldn’t have worked.

The breakthrough was a matter of timing.

In tracing through the logic, and inspecting the model each step of the way, I finally realized that my IsPublished property was never being set to True in time for the RequiredIf evaluation to happen.  As a result, IsPublished was always False, and so my custom validation was being skipped.  The reason it wasn’t being set in time was because EventStatus was being bound in EventBinder.BindModel, but the evaluation of RequiredIf was being done sometime before that point.  It was then that I started looking at what other functions I could override in EventBinder.  I overrode several that sounded like they might work, and put breakpoints in each to see the order that they get called and to inspect my model to see when things got bound.

What I found was that the CreateModel function got called first, and sets up the basic model object.  So, I tried moving the EventStatus logic from BindModel to CreateModel:

 

Public Class EventBinder
    Inherits DefaultModelBinder

    Protected Overrides Function CreateModel(ByVal controllerContext As System.Web.Mvc.ControllerContext, ByVal bindingContext As System.Web.Mvc.ModelBindingContext, ByVal modelType As System.Type) As Object
        Dim NewModel As Object

        ' Create the new model object from scratch
        NewModel = MyBase.CreateModel(controllerContext, bindingContext, modelType)
        NewModel.CurrentEvent = New CorporateEvent

        ' Set EventStatus here
        If (controllerContext.HttpContext.Request.Form.AllKeys.Contains("CurrentEvent.EventStatus.StatusID")) Then
            NewModel.CurrentEvent.EventStatus = Status.GetByID(Convert.ToInt64(controllerContext.HttpContext.Request.Form("CurrentEvent.EventStatus.StatusID")))
        End If

        Return NewModel
    End Function


    Public Overrides Function BindModel(ByVal controllerContext As System.Web.Mvc.ControllerContext, ByVal bindingContext As System.Web.Mvc.ModelBindingContext) As Object
        Dim MyModel As EventDetail

        MyModel = CType(MyBase.BindModel(controllerContext, bindingContext), EventDetail)

        ' Do some other custom binding work here...

        Return MyModel
    End Function
End Class

 

Now when it came time to evaluate my RequiredIf attribute, IsPublished had the right value, and could be properly evaluated.

And there was much rejoicing.

***

Despite the profound amount of grief this approach gave me (most of it being self-inflected), the RequiredIf attribute works very well and saves a lot of time when adding new fields to my model class.  I also learned quite a bit about how model binding and validation works in MVC, so looking back it really was worth the effort.

But man, what a quest.

About these ads

April 16, 2011 - Posted by | ASP.NET MVC

Sorry, the comment form is closed at this time.

Follow

Get every new post delivered to your Inbox.

%d bloggers like this: