.net MVC3 conditionally validating property which

2019-08-01 05:33发布

I have the following ViewModel:

public class StayDetails
{
    public int NumberOfRooms { get; set; }
    public IList<RoomDetail> Rooms { get;set; }
}

public class RoomDetail
{
    public int RoomNumber { get; set; }

    [MinIfRoomRequired("StayDetails.NumberOfRooms", "RoomNumber", 1]
    public int NumberOfAdults { get;set; }
}

What I am trying to do is create a custom validator which will validate the number of adults in a room and make sure that there is at least 1, but only if the current room is required. This is known by looking at the NumberOfRooms property on the StayDetails object.


My custom validator so far:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
    // get a reference to the depended properties
    var containerType = validationContext.ObjectInstance.GetType();
    var requiredRoomsField = containerType.GetProperty(RequiredRoomsPropertyName);
    var roomNumberField = containerType.GetProperty(RoomNumberPropertyName);

    if (requiredRoomsField != null && roomNumberField != null)
    {
        // get the value of the dependent properties
        var requiredRoomsValue = requiredRoomsField.GetValue(validationContext.ObjectInstance, null);
        var roomNumberValue = roomNumberField.GetValue(validationContext.ObjectInstance, null);

        ... (remaining logic to test values) ...

The problem I have is that I cannot access the NumberOfRooms property, the validationContext.ObjectInstance does not have any refernece to the parent object. I thought about adding a reference to the StayDetails object onto the RoomDetails object during object initialzation so I can reference the property from there but model binding wont allow that as the RoomDetail object does not have a parameterless constructor.

Any suggestions?

Many thanks,
David

3条回答
一夜七次
2楼-- · 2019-08-01 05:42

I was able to solve this problem by using custom binders. You will need to add a property to refer back to the parent object, such as, NumberOfRooms. In my case, I actually created a delegate that referred back to a routine in the parent object. Apologies in advance for the VB code and formatting issues I'm having with stackoverflow.

  1. Create the NumberOfRooms property on the child object.
  2. Create a custom model binder for the child object class. This model binder will do a couple of things: a) Insert a value for the NumberOfRooms (in my case, I set a delegate) b) Store the metadata/key information in the controller dictionary to be used later to revalidate.

For example:

Public Class RoomDetail_Binder
    Inherits DefaultModelBinder

    Protected Overrides Function CreateModel(controllerContext As ControllerContext, bindingContext As ModelBindingContext, modelType As Type) As Object
        Dim theObj As Quote_Equipment_Model = MyBase.CreateModel(controllerContext, bindingContext, modelType)
        Dim theParent As StayDetails= controllerContext.HttpContext.Items("StayDetails")
        If Not IsNothing(theParent) Then
            theObj.NumberOfRooms=theParent.NumberOfRooms
        End If
        Return theObj
    End Function

    Protected Overrides Sub OnModelUpdated(controllerContext As ControllerContext, bindingContext As ModelBindingContext)
        MyBase.OnModelUpdated(controllerContext, bindingContext)
        Dim theMetadataList As List(Of MetaDataPair)
        If Not controllerContext.HttpContext.Items.Contains("MetadataList") Then
            theMetadataList = New List(Of MetaDataPair)
            controllerContext.HttpContext.Items.Add("MetadataList", theMetadataList)
        Else
            theMetadataList = controllerContext.HttpContext.Items("MetadataList")
        End If
        theMetadataList.Add(New MetaDataPair With {.Metadata = bindingContext.ModelMetadata, .BindingModelName = bindingContext.ModelName})
    End Sub

End Class

Note that MetadataList is simply

Public Class MetaDataPair
    Public Property BindingModelName As String
    Public Property Metadata As ModelMetadata
End Class
  1. Next I create a custom binder for the parent object: This also does a couple things:

    a) Store the parent object in the controllercontext so that it can be used by the child object. b) Re-validate the child object.

    Public Class StayDetails_Binder Inherits DefaultModelBinder

    Protected Overrides Function CreateModel(controllerContext As ControllerContext, bindingContext As ModelBindingContext, modelType As Type) As Object
        Dim theObj As StayDetails = MyBase.CreateModel(controllerContext, bindingContext, modelType)
        controllerContext.HttpContext.Items("StayDetails") = theObj
        Return theObj
    End Function
    
    Public Overrides Function BindModel(controllerContext As ControllerContext, bindingContext As ModelBindingContext) As Object
        Dim theObj As StayDetails = MyBase.BindModel(controllerContext, bindingContext)
        Dim theMetadataList As List(Of MetaDataPair) = CType(controllerContext.HttpContext.Items("MetadataList"), List(Of MetaDataPair))
        For Each Metadata In theMetadataList
            For Each result As ModelValidationResult In ModelValidator.GetModelValidator(Metadata.Metadata, controllerContext).Validate(Nothing)
                Dim key As String = CreateSubPropertyName(Metadata.BindingModelName, result.MemberName)
                If Not bindingContext.ModelState(key).Errors.Any(Function(ent) ent.ErrorMessage = result.Message) Then
                    bindingContext.ModelState.AddModelError(key, result.Message)
                End If
            Next
        Next
        Return theObj
    End Function
    

    End Class

  2. Decorate your subclass appropriately

    <ModelBinder(GetType(RoomDetail_Binder))> public class RoomDetail

  3. Set your controller so that it will use the custom binder:

    <ModelBinder(GetType(StayDetails_Binder))> (your parameter name here)

  4. Make certain your validator is set to SUCCESS if you validate it without the NumberOfRooms property filled. The binding will execute your validators twice for your child class. First time before the property filled, and then again when the property after it's filled.

查看更多
手持菜刀,她持情操
3楼-- · 2019-08-01 05:47

You can try doing it with the FluentValidation PropertyValidator.
Write less do more...

查看更多
何必那么认真
4楼-- · 2019-08-01 05:51

You should define validation annotation on StayDetails class instead of RoomDetail. This way you will have all the values, NumberOfRooms, list of rooms and their respective RoomNumber and NumberOfAdults. Change your validator accordingly.

查看更多
登录 后发表回答