MVC 4 custom validation attribute firing twice on server side - asp.net-mvc

I'm using custom validation requiredconditional that inherits from RequiredAttribute to conditionally require certain fields. This works beautifully everywhere except in once case and I cannot figure out what is happening. It is calling the IsValid method twice for one property in the model (the client side validation works perfectly) The model has 2 properties using this attribute but only one has the issue. At first I thought it was because one of my conditions was a checkbox and it was posting back both the checked value and the hidden value but I tried using radio buttons instead and even a hidden value but had the same results in all cases. Here is my view (simplified for testing):
#ModelType List(Of eLADNETBusiness.AdditionalInterest)
#Code
ViewData("Title") = "Bind Coverage Entry"
End Code
<h2>Bind Coverage Entry</h2>
#Using Html.BeginForm()
#Html.AntiForgeryToken()
#Html.Hidden("hullValue", ViewBag.HullValue)
Dim currentCount As Integer = 0
#For count As Integer = 0 To Model.Count - 1
currentCount = count
#<div class="editor-label">
#Html.LabelFor(Function(model) model(currentCount).LienholderAmount)
</div>
#<div class="editor-field">
#Html.EditorFor(Function(model) model(currentCount).LienholderAmount)
#Html.ValidationMessageFor(Function(model) model(currentCount).LienholderAmount)
</div>
#<div>
#Html.EditorFor(Function(model) model(currentCount).Lienholder90Percent)
</div>
Next
#<p>
<input type="submit" value="Continue" />
</p>
End Using
And here is my model (simplified for testing):
<DataContract()> _
Public Class AdditionalInterest
<DataMember()> _
Public Property ID As Integer = 0
<RequiredConditional("Lienholder90Percent", False, ErrorMessage:="Enter Breach of Warranty lienamount or select 90 percent of insured value")> _
<Display(Name:="Lienholder Amount")> _
<DataMember()> _
Public Property LienholderAmount As Nullable(Of Integer)
<DataMember()> _
Public Property Lienholder90Percent As Boolean
End Class
And my requiredconditional attribute:
Imports System.Collections.Generic
Imports System.Linq
Imports System.ComponentModel.DataAnnotations
Imports System.Web.Mvc
Imports System.Collections
Imports System.Text
Public Class RequiredConditional
Inherits RequiredAttribute
Implements IClientValidatable
Private Property PropertyNames() As String()
Private Property DesiredValues() As Object()
Public Sub New(comparePropertyNames As String(), comparePropertyDesiredValues As Object())
PropertyNames = comparePropertyNames
DesiredValues = comparePropertyDesiredValues
End Sub
Public Sub New(comparePropertyNames As String, comparePropertyDesiredValues As Object)
PropertyNames = New String() {comparePropertyNames}
DesiredValues = New String() {comparePropertyDesiredValues}
End Sub
Protected Overrides Function IsValid(value As Object, context As ValidationContext) As ValidationResult
Dim instance As Object = context.ObjectInstance
Dim type As Type = instance.GetType()
Dim propertyvalue As Object
Dim trueConditions As Integer = 0
For count As Integer = 0 To PropertyNames.Count - 1
propertyvalue = type.GetProperty(PropertyNames(count)).GetValue(instance, Nothing)
If Not propertyvalue Is Nothing Then
If DesiredValues.Count >= count + 1 Then
If propertyvalue.ToString() = DesiredValues(count).ToString() Then
trueConditions += 1
End If
End If
End If
Next
'if all conditions are met, validate value
If trueConditions = PropertyNames.Count And trueConditions = DesiredValues.Count Then
Dim result As ValidationResult = MyBase.IsValid(value, context)
Return result
End If
Return ValidationResult.Success
End Function
Public Function GetClientValidationRules(metadata As System.Web.Mvc.ModelMetadata, context As System.Web.Mvc.ControllerContext) As System.Collections.Generic.IEnumerable(Of System.Web.Mvc.ModelClientValidationRule) _
Implements System.Web.Mvc.IClientValidatable.GetClientValidationRules
Dim results As New List(Of ModelClientValidationRule)
Dim rule = New ModelClientValidationRule With {.ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()), .ValidationType = "requiredif"}
Dim depProp As String = String.Empty
Dim sbProp As New StringBuilder()
Dim sbTarget As New StringBuilder()
Dim helper As New ValidationHelper
For count As Integer = 0 To PropertyNames.Count - 1
Dim prop As String = PropertyNames(count)
depProp = helper.BuildDependentPropertyName(metadata, TryCast(context, ViewContext), prop)
sbProp.AppendFormat("|{0}", depProp)
Dim targetValue As String = String.Empty
If DesiredValues.Count >= count + 1 Then
targetValue = (If(DesiredValues(count), "")).ToString()
End If
If DesiredValues(count).GetType() = GetType(Boolean) Then
targetValue = DesiredValues(count).ToString.ToLower
End If
sbTarget.AppendFormat("|{0}", targetValue)
Next
rule.ValidationParameters.Add("dependentproperty", sbProp.ToString().TrimStart("|"))
rule.ValidationParameters.Add("targetvalue", sbTarget.ToString().TrimStart("|"))
results.Add(rule)
Return results
End Function
End Class
So when I click submit and debug into the requiredconditional attribute, the lienholderamount property hits IsValid twice. The first time it hits, the value of Lienholder90Percent is False even though in the model it is true and was true on the form (and passed client side validation) so fails validation at that point. Then hits again and Lienholder90Percent is True (which is correct) and passes validation. But since it failed on the first one, it still fails and show the error message. You'll notice that the model is a list but for testing purposes, I'm only sending one and still getting the same results. Can't figure out why it is only happening with this one property. Hope it is something easy I just can't see. I've spent all day trying to figure this out. Like I said, I use this attribute quite a bit and works great. Can't find a difference for this case.

Inherit from ValidationAttribute instead of RequiredAttribute.
RequiredAttribute's IsValid is called right after the property is filled, whereas the ValidationAttribute's IsValid is called after the whole model is filled (which would make sense for more complex validation).

Related

A public action method 'FetchClaimsFor?pn=11111&cm=12&cy=2016' was not found on controller 'MyAPP.Controllers.ClaimMonthController'

I have a View that has an Http.Action to get a partial View.
My View has the following:
#Code
ViewData("Title") = "View Claims"
Dim actionUrl As String = "FetchClaimsFor?pn=" & ViewData("PersonnelNo") & "&cm=" & ViewData("ClaimMonth") & "&cy=" & ViewData("ClaimYear")
End Code
<div class="col-sm-12 col-md-6 col-lg-6">
#Html.Action(actionUrl, "ClaimMonth")
</div>
My ClaimMonthController has the following:
<HttpGet>
<ChildActionOnly>
Function FetchClaimsFor() As PartialViewResult
Dim pn As String = Request.QueryString("pn")
Dim cm As Integer = Request.QueryString("cm")
Dim cy As Integer = Request.QueryString("cy")
Dim ClaimsMade = New OvertimeClaims
ClaimsMade.GetClaimsFor(pn, cm, cy)
Select Case tsbClaimsMade.CoreDays
Case 0
Return PartialView("_TSB0", OvertimeClaims)
Case 5
Return PartialView("_TSB5", tsbClaimsMade)
Case 6
Return PartialView("_TSB6", tsbClaimsMade)
Case 8
Return PartialView("_TSB8", tsbClaimsMade)
Case Else
Return PartialView("_TSB0", tsbClaimsMade)
End Select
End Function
I did this approach as the partialview can be one of 4 types based on a value returned in the ClaimsMade object.
Unfortunately I'm getting the error...
A public action method 'FetchClaimsFor?pn=11111&cm=12&cy=2016' was not
found on controller 'MyAPP.Controllers.ClaimMonthController'.
Looking at other similar question in SO I have tried with and without <HttpGet> and with and without <ChildActionOnly> - each time I get the same error.
UPDATE
forgot to mention I also tried adding Public to the function
SOLUTION
View code should be:
#Html.Action("FetchClaimsFor", "ClaimMonth", New With { .pn = ViewData("PersonnelNo"), .cm = ViewData("ClaimMonth"), .cy = ViewData("ClaimYear") })
Controller code should be:
Function FetchClaimsFor(pn As String, cm As Integer, cy As Integer) As PartialViewResult
REMOVE the next three lines.
Dim pn As String = Request.QueryString("pn")
Dim cm As Integer = Request.QueryString("cm")
Dim cy As Integer = Request.QueryString("cy")
The first param to Html.Action is not a URL, but rather an action name. You're correctly being told that there's no action that matches FetchClaimsFor?pn=11111&cm=12&cy=2016. Instead, you should be doing:
#Html.Action("FetchClaimsFor", "ClaimMonth", New With { .pn = ViewData("PersonnelNo"), .cm = ViewData("ClaimMonth"), .cy = ViewData("ClaimYear") })
Note: I'm not fluent in VB, so the syntax may be off

VB.NET MVC Get vs Post how do i design my page

I have a VB.NET mvc view form that has 3 parameters and 2 buttons and I not sure how to design my page.
Button 1 is essentially a GET - suppose to run a sql query and return a dataset (multiple tables).
Button 2 is essentially a PUT - suppose to run a sql update statement (multiple updates).
CustomModel includes my 3 parameters as Properties and I have included the Required Attribute on all.
How do I design so that the Required attributes are respected.
I.e. my ViewInfo method is being reached even if the parameter inputs are blank.
Do I declare ViewInfo with the HttpGet and my UpdateInfo with HttpPut attribute. Do I need the parameter declarations in my ViewInfo method?
Is my #Html.BeginForm(...) declaration correct?
VB.NET Model class:
Public Class CustomModel
<Required(ErrorMessage:="Parameter 1 is required"),
Display(Name:="Parameter 1:")>
Public Property Parameter1 As String
<Required(ErrorMessage:="Parameter 2 is required"),
Display(Name:="Parameter 2:")>
Public Property Parameter2 As String
<Required(ErrorMessage:="Parameter 3 is required"),
Display(Name:="Parameter 3:")>
Public Property Parameter3 As Integer
Public Sub New(Parameter1 As String, Parameter2 As String, Parameter3 As Integer)
Me.Parameter1 = Parameter1
Me.Parameter2 = Parameter2
Me.Parameter3 = Parameter3
End Sub
Public Property Info As DataSet = New DataSet
End Class
VB.NET Controller:
Function Index() As ActionResult
ViewBag.ShowView = False
Return View()
End Function
Function ViewInfo(Parameter1 As String, Parameter2 As String, Parameter3 As Integer)
Dim result As New CustomModel(Parameter1, Parameter2, Parameter3)
If ModelState.IsValid Then
result.Info = GetInfo(Parameter1, Parameter2, Parameter3)
End If
ViewBag.ShowView = True
Return View("Index", result)
End Function
Function UpdateInfo(Parameter1 As String, Parameter2 As String, Parameter3 As Integer)
Dim result As New CustomModel(Parameter1, Parameter2, Parameter3)
If ModelState.IsValid Then
result.Info = UpdateInfo(Parameter1, Parameter2, Parameter3)
End If
ViewBag.ShowView = True
Return View("Index", result)
End Function
HTML View
#ModelType CustomModel
...
#Using (Html.BeginForm("ViewInfo", "Home", Nothing, FormMethod.Post))
...
#Html.LabelFor(...)
#Html.TextBoxFor(...)
...
<button type=button>View</button>
<button type=submit>Update</button>
End Using
Output:
#If ViewBag.Show Then
// Multiple WebGrid calls ???
End If
I found a solution to get me on my way finally.
I did not realise the default layout page behind the scenes does not include the validation javascript file so I had to add to my specific view file:
#Section scripts
#Scripts.Render("~/bundles/jqueryval")
End Section
Is there something you have to do so that this file is automatically brought in or is what I have done is correct you have to manually include it in either the layout or the specific view where you need it?

Populating a ViewModel from a database model

I have a ViewModel that inherits a database model. I can't work out how to populate the ViewModel from the database.
"ViewModel"
Public Class CustomerView
Inherits Customer
Private lookUp As New LookUpData
ReadOnly Property OwnerList As List(Of SelectListItem)
Get
Return lookUp.OwnersList(True)
End Get
End Property
End Class
"Controller"
Function Edit(Optional Id As Integer = Nothing) As ActionResult
Dim model As New CustomerView
model = (From c In db.Customers Where c.Id = Id AndAlso c.OrganisationId = s.OrganisationId).FirstOrDefault
If model Is Nothing Then
Return RedirectToAction("List")
Else
Return View(model)
End If
End Function
I get an invalid cast exception.
Any help would be greatly appreciated.
Your Linq query is returning (I assume) as list of Customer object which you need to project into your view model object. Change your Linq by adding a select:
model = (From c In db.Customers _
Where c.Id = Id AndAlso _
c.OrganisationId = s.OrganisationId _
Select New CustomerView With { _
.CompanyName = c.CompanyName, _
.Address = c.Address, _
.City = cust.City, _
.Country = cust.Country}).FirstOrDefault
Been a while since I did VB, but that should either work or require only a tweak.

Limiting Mvc 3 Action links or Functions based On Date

I have a mvc 3 vb.net razor App that I need to redirect the view to a different action if the link is not clicked on between a preset range of Month/Day/Year ranges... This is needed to limit registration for a school semester to only be able to be performed between and Open Registration Date and a End Registration Date... I am thinking I can simply put this in the controller function in the form of either a If Statement or a select case and then use redirect based on the condition of the date...... Is there some simple short code to use to perform this test... My variables for the dates are OpenDate and EndDate. I think its probably something close to
Dim OpenDate as date = mm/dd/yy
Dim CloseDate as date = mm/dd/yy
If system.datetime.now.toshortdatestring < OpenDate Then
Return RedirecttoAction ("Too Soon")
ElseIf system.datetime.now.toshortdatestring > CloseDate Then
Return RedirecttoAction ("Too Late")
Else
Return View()
End If
Does this look good or is there a easier way???
I would write a custom authorization attribute:
Public Class MyAuthorizeAttribute
Inherits AuthorizeAttribute
Private ReadOnly _openDate As DateTime
Private ReadOnly _closeDate As DateTime
Public Sub New(openDate As String, closeDate As String)
_openDate = DateTime.ParseExact(openDate, "dd/MM/yyyy", CultureInfo.InvariantCulture)
_closeDate = DateTime.ParseExact(closeDate, "dd/MM/yyyy", CultureInfo.InvariantCulture)
End Sub
Protected Overrides Function AuthorizeCore(httpContext As HttpContextBase) As Boolean
Dim now = DateTime.Now
If now < _openDate Then
httpContext.Items("actionToRedirectTo") = "toosoon"
Return False
End If
If now > _closeDate Then
httpContext.Items("actionToRedirectTo") = "toolate"
Return False
End If
Return True
End Function
Protected Overrides Sub HandleUnauthorizedRequest(filterContext As AuthorizationContext)
Dim values = New RouteValueDictionary(New With { _
Key .action = filterContext.HttpContext.Items("actionToRedirectTo"), _
Key .controller = "somecontroller" _
})
filterContext.Result = New RedirectToRouteResult(values)
End Sub
End Class
and then simply decorate controller actions that need this kind of logic with it:
<AuthorizeRegistration("01/11/2011", "01/12/2011")>
Function Register() As ActionResult
Return View()
End Function
This way you can reuse it on different actions that need this kind of protection.
You want to limit registration for a school semester by date, so the given date for registration should be valid - this is a validation concern so you should implement go for validation with an error message and not redirecting special views.
You can use the Range (System.ComponentModel.DataAnnotations) attribute on your model for your purposes. If you would need more dynamic values for date range validation you should write your own custom validation attribute.

Combining extension methods

I'm trying to write 2 extension methods to handle Enum types. One to use the description attribute to give some better explanation to the enum options and a second method to list the enum options and their description to use in a selectlist or some kind of collection.
You can read my code up to now here:
<Extension()> _
Public Function ToDescriptionString(ByVal en As System.Enum) As String
Dim type As Type = en.GetType
Dim entries() As String = en.ToString().Split(","c)
Dim description(entries.Length) As String
For i = 0 To entries.Length - 1
Dim fieldInfo = type.GetField(entries(i).Trim())
Dim attributes() = DirectCast(fieldInfo.GetCustomAttributes(GetType(DescriptionAttribute), False), DescriptionAttribute())
description(i) = If(attributes.Length > 0, attributes(0).Description, entries(i).Trim())
Next
Return String.Join(", ", description)
End Function
<Extension()> _
Public Function ToListFirstTry(ByVal en As System.Enum) As IEnumerable
Dim type As Type = en.GetType
Dim items = From item In System.Enum.GetValues(type) _
Select New With {.Value = item, .Text = item.ToDescriptionString}
Return items
End Function
<Extension()> _
Public Function ToListSecondTry(ByVal en As System.Enum) As IEnumerable
Dim list As New Dictionary(Of Integer, String)
Dim enumValues As Array = System.Enum.GetValues(en.GetType)
For Each value In enumValues
list.Add(value, value.ToDescriptionString)
Next
Return list
End Function
So my problem is both extension methods don't work that well together. The methods that converts the enum options to an ienumerable can't use the extension method to get the description.
I found all kind of examples to do one of both but never in combination with each other. What am I doing wrong? I still new to these new .NET 3.5 stuff.
The problem is that Enum.GetValues just returns a weakly typed Array.
Try this:
Public Function ToListFirstTry(ByVal en As System.Enum) As IEnumerable
Dim type As Type = en.GetType
Dim items = From item In System.Enum.GetValues(type).Cast(Of Enum)() _
Select New With {.Value = item, .Text = item.ToDescriptionString}
Return items
End Function
(It looks like explicitly typed range variables in VB queries don't mean the same thing as in C#.)

Resources