My forms have inputs with default helper text that guides the user on what to enter (rather than using labels). This makes validation tricky because the input value is never null.
How can I extend unobtrusive validation to handle this? The form shouldn't be valid if the Name input is equal to "Please enter your name..."
I started reading Brad Wilson's blog post on validation adapters, but I'm not sure if this is the right way to go? I need to be able to validate against different default values depending on the field.
Thanks
Here's a sample illustrating how you could proceed to implement a custom validation attribute:
public class NotEqualAttribute : ValidationAttribute, IClientValidatable
{
public string OtherProperty { get; private set; }
public NotEqualAttribute(string otherProperty)
{
OtherProperty = otherProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var property = validationContext.ObjectType.GetProperty(OtherProperty);
if (property == null)
{
return new ValidationResult(
string.Format(
CultureInfo.CurrentCulture,
"{0} is unknown property",
OtherProperty
)
);
}
var otherValue = property.GetValue(validationContext.ObjectInstance, null);
if (object.Equals(value, otherValue))
{
return new ValidationResult(FormatErrorMessage(validationContext.DisplayName));
}
return null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = ErrorMessage,
ValidationType = "notequalto",
};
rule.ValidationParameters["other"] = OtherProperty;
yield return rule;
}
}
and then on the model:
public class MyViewModel
{
public string Prop1 { get; set; }
[NotEqual("Prop1", ErrorMessage = "should be different than Prop1")]
public string Prop2 { get; set; }
}
controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel
{
Prop1 = "foo",
Prop2 = "foo"
});
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
and view:
#model MyViewModel
<script src="#Url.Content("~/Scripts/jquery.validate.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.js")" type="text/javascript"></script>
<script type="text/javascript">
jQuery.validator.unobtrusive.adapters.add(
'notequalto', ['other'], function (options) {
options.rules['notEqualTo'] = '#' + options.params.other;
if (options.message) {
options.messages['notEqualTo'] = options.message;
}
});
jQuery.validator.addMethod('notEqualTo', function(value, element, param) {
return this.optional(element) || value != $(param).val();
}, '');
</script>
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.Prop1)
#Html.EditorFor(x => x.Prop1)
#Html.ValidationMessageFor(x => x.Prop1)
</div>
<div>
#Html.LabelFor(x => x.Prop2)
#Html.EditorFor(x => x.Prop2)
#Html.ValidationMessageFor(x => x.Prop2)
</div>
<input type="submit" value="OK" />
}
Yes thats the right way to go. You should implement your own atribute and implement IClientValidatable.
You could also have a required boolean value set initially to false as a hidden form field. When the user changes the textbox, set it to true.
You could make your ViewModel implement IValidatableObject and when implementing the Validate method (from IValidatableObject) add some logic to check the values of the properties e.g.
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) {
var results = new List<ValidationResult>();
if (Name == "Please enter your name...")
results.Add(new ValidationResult("You must enter a name");
...
Enter other validation here
...
return results;
}
Now, when Model.IsValid is called in your controller, this bit of logic will be ran and will return validation errors as normal.
It took a little while since your question was asked, but if you still like data annotations, this problem can be easily solved using this library:
[Required]
[AssertThat("FieldA != 'some text'")]
public string FieldA { get; set; }
Above, the field value is compared with some pre-defined text. Alternatively, you can compare fields values with each other:
[AssertThat("FieldA != FieldB")]
...and when the case of the strings being compared does not matter:
[AssertThat("CompareOrdinalIgnoreCase(FieldA, FieldB) != 0")]
To improve a little bit of #Darin Dimitrov answer, if you want to add messages from the resources using ErrorMessageResourceName and ErrorMessageResourceType, just add this to the to the Error message ErrorMessage = ErrorMessage ?? ErrorMessageString
The ErrorMessageString will look for the localized version of error message that you set in the model using those parameters (ErrorMessageResourceName and ErrorMessageResourceType)
The ideal solutions is a custom Attribute where you specify minimum and maximum lengths as well as MustNotContain="Please enter your name...".
Related
How can I run some Javascript functions in a Blazor app, without using the JS interop or jQuery? Just plain old Javascript functions that interact with the DOM, independently of Blazor.
I added my script right before the closing </body> tag:
<script src="app.js"></script>
And in app.js I have the following:
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
Of course the selector finds no element. I'm guessing they aren't yet present in the DOM at that point? How can I run that script without using the JS interop or jQuery?
If your okay with using jquery you can use an on handler, which will apply to dynamically added elements
$("body").on("click", ".some-element", function(){
alert("Hello");
});
You can do natively too, but seems sketchy IMHO:
document.querySelector('body').addEventListener('click', function(event) {
if (event.target.className.toLowerCase() === 'some-element') {
alert("Hello");
}
});
Note you have weird javascripty things to worry about now like z-index if the click event actually gets up to the body, but it should work for simple stuff.
Main question is WHY would you want to do this in Blazor - whole point is you can use C# events instead you crazy person!!
Create a page like this:
<div height="#($"{Height}px")" width="#($"{Width}px")">
Test
</div>
<button #onclick="DoubleSize">Double Size</button>
#code
{
public int Width { get; set; } = 50;
public int Height { get; set; } = 50;
private void DoubleSize()
{
Width = Width * 2;
Height = Height * 2;
}
}
On render the height and width are 50px:
Then you click the Button, and they change to 100px:
Similarly with CSS if that's what you were talking about:
<div style="width: #($"{Width}px"); height: #($"{Height}px")">
Test
</div>
This is interacting with the properties of the DOM elements without using Javascript... you can do this with any property. You don't need to use Javascript - you just bind them to your properties in C#...
You shouldn't need to 'retrieve the final properties of the rendered DOM elements' as you should be controlling them from C# and not worrying about the DOM
I'm pretty new in Blazor, so sorry if it's not the correct way to do it in Balzor. But I think that you need always use JS interop to use JavaScript function. You want to execute some script, but the question is: when do you want to execute the script? I imagine you want to execute an action after navigate to a page, after make a click in a button... and all this events happens in Blazor
If you ask about relationate an blazor element with the doom you need use #ref
A little example. You create a .js like
var auxiliarJs = auxiliarJs || {};
auxiliarJs.getBoundingClientRect = function (elementRef) {
let result = elementRef? elementRef.getBoundingClientRect():
{left: 0,top: 0,right: 0,bottom: 0,x: 0,y: 0,width: 0,height: 0 };
return result;
}
auxiliarJs.executeFunction = function (elementRef, funciones) {
let res = null;
try {
if (Array.isArray(funciones)) {
functiones.forEach(funcion => {
elementRef[funcion]()
});
}
else
res = elementRef[funciones]();
}
catch (e) { }
if (res)
return res;
}
auxiliarJs.setDocumentTitle = function (title) {
document.title = title;
};
And a service.cs with his interface
public interface IDocumentService
{
Task<ClientRect> getBoundingClientRect(ElementReference id);
Task setDocumentTitle(string title);
Task<JsonElement> executeFunction(ElementReference id, string funcion);
Task executeFunction(ElementReference id, string[] funciones);
}
public class DocumentService:IDocumentService
{
private IJSRuntime jsRuntime;
public DocumentService(IJSRuntime jsRuntime)
{
this.jsRuntime = jsRuntime;
}
public Dictionary<string, object> JSonElementToDictionary(JsonElement result)
{
Dictionary<string, object> obj = new Dictionary<string, object>();
JsonProperty[] enumerador = result.EnumerateObject().GetEnumerator().ToArray();
foreach (JsonProperty prop in enumerador)
{
obj.Add(prop.Name, prop.Value);
}
return obj;
}
public async Task<ClientRect> getBoundingClientRect(ElementReference id)
{
return await jsRuntime.InvokeAsync<ClientRect>("auxiliarJs.getBoundingClientRect", id);
}
public async Task setDocumentTitle(string title)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.setDocumentTitle", title);
}
public async Task<JsonElement> executeFunction(ElementReference id,string funcion)
{
var result= await jsRuntime.InvokeAsync<JsonElement>
("auxiliarJs.executeFunction", id, funcion);
return result;
}
public async Task executeFunction(ElementReference id, string[] funciones)
{
await jsRuntime.InvokeVoidAsync("auxiliarJs.executeFunction", id, funciones);
}
}
public class ClientRect
{
public float left{ get; set; }
public float top { get; set; }
public float right { get; set; }
public float bottom { get; set; }
public float x { get; set; }
public float y { get; set; }
public float width { get; set; }
public float height { get; set; }
}
Well, you inject the service as usual in program.cs
public static async Task Main(string[] args){
....
builder.Services.AddSingleton<IDocumentService, DocumentService>();
}
And in your component razor
#inject IDocumentService document
<div #ref="mydiv"></div>
<input #ref="myinput">
<button #onclick="click">click</button>
#code{
private ElementReference mydiv;
private ElementReference myinput;
click(){
ClientRect rect = await document.getBoundingClientRect(mydiv);
document.setDocumentTitle("New Title");
document.executeFunction(myinput 'focus')
}
}
Blazor "overwrites" all attached JS Events during render process
window.attachHandlers = () => {
var elements = document.querySelectorAll(".some-element");
elements.forEach(function (element) {
element.addEventListener("click", (e) => {
alert("Hello");
});
});
and in razor page
protected override void OnAfterRender(bool firstRender)
{
if(firstRender)
{
JSRuntime.InvokeVoidAsync("attachHandlers");
}
}
On a page in a Razor MVC app a bunch of lines are written from a model, like this:
#foreach (var item in Model)
{
// write lots of data
#Html.CheckBoxFor(item.Id) // incorrect syntax, I know
}
<input type="button" value="Update" onclick="Update();" />
The javascript that is run from the button is supposed to get the item.Id (which is a unique integer) from each checkbox that has been checked, put the ids in an array and pass the array to another function. This is what I'm working with so far:
function Update() {
var ids = document.getElementsByName("myCheckbox");
var idList = new Array(ids.length);
for (var i = 0; i < ids.length; i++) {
if (ids[i].checked {
idList.push(ids[i]);
}
}
$.ajax({
type: "POST",
url: "#Url.Action("Update", "Home")",
data: idList,
success: function (data) {
},
error: function (data) {
console.log(data);
}
});
}
How do I add the same name to all the checkboxes and have their ID be that of item.Id?
Edit
I tried C Murphy's suggestion by making a class for the checkboxes:
public class ToBeUpdated
{
public int ID { get; set; }
public bool IsSelected { get; set; }
}
And then adding a List to the class that's in the model:
public List<ToBeUpdated> ToBeUpdatedList { get; set; }
However, I can't figure out how to write the CheckBoxFor. His/her suggestion was:
#Html.CheckboxFor(modelItem => modelItem.SomeNameList[i].IsSelected)
But I'm not doing a for loop but a foreach:
foreach (var item in Model)
So I had to do this instead:
#Html.CheckBoxFor(modelItem => item.ToBeUpdatedList.Find(m => m.ID == item.Id).IsSelected)
Then I have my button that sends me to the function in my controller:
<input type="button" value="Update" onclick="location.href='#Url.Action("UpdateStatuses", "Home")'" />
public IActionResult UpdateStatuses(IEnumerable<FileData> model)
{
// iterate through the items in the model
}
The problem is that when I get to UpdateStatuses the model is empty even though I had a ton of checkboxes that I checked before clicking the button. The model in the View is
#model IEnumerable<MyData>
And MyData is a simple class:
public class MyData
{
public int Id { get; set; }
public List<ToBeUpdated> ToBeUpdatedList { get; set; }
}
Edit 2: Finally found a solution
I made my checkboxes like this:
<input type="checkbox" name="chkUpdateThis" id="#item.Id" />
And then I could, with the help of T.J's code, edit my javascript to do this:
function UpdateStatuses() {
const ids = [...document.querySelectorAll("[name=chkUpdateThis]")].filter(({checked}) => checked).map(({id}) => id)
var idList = new Array();
for (var i = 0; i < ids.length; i++) {
idList.push(parseInt(ids[i]));
}
$.ajax({
type: "POST",
url: "#Url.Action("UpdateStatuses", "Home")",
data: ({ ids: idList }),
success: function (data) {
},
error: function (data) {
console.log(data);
}
});
}
Then I can call the UpdateStatuses function in my Home controller:
public IActionResult UpdateStatuses(List<int> ids)
Since T.J didn't post a suggestion that I can give a checkbox to, I'll give it to C Murphy who was also helpful. I didn't use his suggestion, but I don't want to give myself a checkbox even though the solution I went for is right here.
#Html.CheckBoxFor(model => model.Id, new { Name = "myCheckbox" })
On a similar note to what Heretic Monkey commented, you should probably set up a class. Without knowing what your Update ActionResult is doing in your controller, I can't give you a complete answer, but you could follow this general structure:
Model:
public class SomeNameViewModel
{
public List<SomeName> SomeNameList { get; set; }
}
public class SomeName
{
public int Id { get; set; }
public bool IsSelected { get; set; }
}
View:
#model ProjectName.Models.SomeNameViewModel
#for (var i = 0; i < Model.Count(); i++)
{
<table>
<tr>
<td>#Html.HiddenFor(modelItem => modelItem.SomeNameList[i].Id)</td>
<td>#Html.CheckboxFor(modelItem => modelItem.SomeNameList[i].IsSelected)</td>
</tr>
</table>
}
Edit (due to information provided via comment):
(note I also adjusted the previous section to use a ViewModel instead of a list of the new class)
You would then adjust your ActionResult to instead accept the ViewModel class as an argument and perform the database update as follows:
Controller:
public ActionResult Update(SomeNameViewModel viewModel)
{
List<int> selectedIds = new List<int>();
foreach(var row in viewModel.SomeNameList)
{
if(row.IsSelected == true)
{
selectedIds.Add(row.Id);
}
else
{
// wasn't selected, so do nothing
}
}
// Now you have a list of Ids (selectedIds) that were selected
// and can then be passed to your other function
}
Second Edit:
I think there is still some confusion surrounding the business requirement around what you are trying to accomplish.
The reason for the error was I assumed you would be passing those IDs (in the form of the ToBeUpdated view model class) to the ActionResult, instead of passing in the IEnumerable<FileData>. That is why it is empty when it reaches your controller. The structure of MVC (Model View Controller) dictates that you pass information in the form of a model (class) to the controller from the view (and vice versa).
Please provide your full model list along with a more detailed explanation of what you are trying to achieve and why.
The reason why I utilized a for loop instead of a foreach loop is to ensure that the list is serialized with unique indexes for each item in the list. The only reason you need to use checkboxes is to identify which IDs are to be updated. The logic to determine which are going to be updated should be used on the controller side. Think of a checkbox as an input field for a boolean (true/false, 0/1) property. The goal of a unique ID is just to identify your unique data records. The boolean (checkbox field on the view side) is just to have your end user designate a specific record(s) as needing to be updated.Whatever your model may be, you should add a boolean property (like I gave as an example) to have the user determine which record(s) to update, and call the SQL update command from your controller ONLY on the records from your list/IEnumerable that have a value of true for your IsSelected property.
i am creating a form which can be accessed based on condition in MVC. I have first view with dropdownlist and submit button, i want when the submit button is clicked, the value in dropdownlist is passed and compared with condition set for that value, and if the condition is not ok, it shows alert rather than processing to the form.
Here is my code:
public ActionResult ChooseType()
{
var x = DataAccess.GetEmployee(#User.Identity.Name);
var lists = new SelectList(RuleAccess.GetAllRule(), "ID", "TypeDetail");
ViewBag.CategoryId = lists;
/*rule*/
ViewBag.comp1 = Logic.AnnualToogle(#User.Identity.Name);
if (x.EmpSex == "F" && x.EmpMaritalSt == "NIKAH")
{ ViewBag.comp2 = 1; }
else ViewBag.comp2 = 0;
return View();
}
[HttpGet]
public ActionResult Create(int lv_type)
{
var type = RuleAccess.GetTypeByID(lv_type);
ViewBag.type = type;
var model = new LeaveApplicationViewModels();
model.X = DataAccess.GetEmployee(#User.Identity.Name);
model.C = DataAccess.GetLeaveApp(#User.Identity.Name);
/*disable*/
ViewBag.dis = DataAccess.GetDisabledDate(#User.Identity.Name);
/*max*/
var max= RuleAccess.GetMaxByID(lv_type);
ViewBag.c = max;
if (lv_type == 1)
{
var used = RuleAccess.CountUsedAnnual(#User.Identity.Name);
var rem = max - used;
ViewBag.a = used;
ViewBag.b = rem;
}
else
{
ViewBag.b = max;
}
return View(model);
}
I used the Viewbag.comp 1 & 2 in my view:
<script type="text/javascript">
var x = #ViewBag.comp1;
var y = #ViewBag.comp2;
function validatecreate()
{
var value= document.getElementById("lv_type").value;
if (value==1)
{
if(x==1)
document.getElementById('validatecreate').submit();
else { alert('Action cant be done. You either have another annual leave application in pending status or you have reach the limit of annual leave'); }
}
else if(value==2)
{
if(y==1)
document.getElementById('validatecreate').submit();
else { alert('Action cant be done. You either are Male or Not Married Yet'); }
}
else if(value==3)
{
document.getElementById('validatecreate').submit();
}
else {
document.getElementById('validatecreate').submit();
//alert('Http Not Found');
}
}
#Html.DropDownList(
"lv_type", (SelectList) ViewBag.CategoryId,
"--Select One--",
new{ //anonymous type
#class = "form-control input-sm"
}
)
I feel like im doing it wrong especially because if someone manually put the url with ?lv_type=2, they not validate and can go to the form directly. But i need the value of lv_type bcs i use that in my view. Please Helpp :(
Validation must always be done on the server, and client side validation should only be considered a nice bonus that minimizes the need for to a call to the server. And presenting options in a dropdownlist to a user and then telling them thay can't select that option is an awful user experience. Instead, you should be presenting only those options which are applicable to the user (and delete all the scripts you have shown).
Create an additional method in your RuleAccess class, say GetEmployeeRules(Employee employee) which returns only the rules that are applicable to that employee, for example
public static List<Rule> GetEmployeeRules(Employee employee)
{
// Get the list of all rules
if (employee.EmpSex == "F" && employee.EmpMaritalSt == "NIKAH")
{
// Remove the appropriate Rule from the list
}
.....
// Return the filtered list
}
In addition, you should be using a view model in the view
public class LeaveTypeVM
{
[Required(ErrorMessage = "Please select a leave type")]
public int SelectedLeaveType { get; set; }
public IEnumerable<SelectListItem> LeaveTypeList { get; set; }
}
Then in the ChooseType() method
public ActionResult ChooseType()
{
var employee = DataAccess.GetEmployee(#User.Identity.Name);
var rules = RuleAccess.GetEmployeeRules(employee);
var model = new LeaveTypeVM()
{
LeaveTypeList = new SelectList(rules, "ID", "TypeDetail")
};
return View(model);
}
and in the view
#model LeaveTypeVM
#using (Html.BeginForm())
{
#Html.DropDownListFor(m => m.SelectedLeaveType, Model.LeaveTypeList, "--Select One--", new { #class = "form-control input-sm" }
#Html.ValidationMessageFor(m => m.SelectedLeaveType)
<input type="submit" value="Submit" />
}
and submit to a POST method which allows you to easily return the view if its invalid, or to redirect to the Create method.
[HttpPost]
public ActionResult ChooseType(LeaveTypeVM model)
{
if (!ModelState.IsValid)
{
model.LeaveTypeList = .... // as per GET method
}
return RedirectToAction("Create", new { leaveType = model.SelectedLeaveType });
and in the Create() method
public ActionResult Create(int leaveType)
{
var employee = DataAccess.GetEmployee(#User.Identity.Name);
var rule = RuleAccess.GetEmployeeRules(employee).Where(x => x.ID == leaveType).FirstOrDefault();
if (rule == null)
{
// Throw exception or redirect to an error page
}
var model = new LeaveApplicationViewModels();
....
return View(model);
}
Note your LeaveApplicationViewModels should contain additional properties so that you can avoid all those ViewBag properties and generate a strongly typed view.
In my experience I call simple web methods, like this:
[WebMethod]
public List<string> GetUserListByLetters(string strLetters){ ... }
And here is my OnComplete JS-function:
function OnComplete(args) {
...
if (args != "") {
for (var i = 0; i < args.length; i++) {
// Do what I need with string in args[i]
}
}
...
}
For now I would like to have such method:
[WebMethod]
public string GetUserListByCountry(int countryId, out List<User> users)
{
users=null;
if ( Validate(countryId)==false )
return "wrong country Id";
users = GetUsers(countryId); // returns list of User objects.
return "";
}
Question1: should "out" parameter work in WS? I saw few article (, for example) where said it is impossible.
Question2: if it doesn't work, how should I change method signature to get that workable?
Question3: if it works, how could I access data from 'out' parameter?
Thanks.
I will use this approach:
[WebMethod]
public object GetUserListByCountry(int countryId)
{
users=null;
if ( Validate(countryId)==false )
return "wrong country Id";
users = GetUsers(countryId); // returns list of User objects.
return new {Error="", Users=users};
}
I mean that I will return complex object, its one property will contain usual value for return and another - required data.
I have created a variable length list according to the many great posts by Steve Sanderson on how to do this in MVC 2. His blog has a lot of great tutorials.
I then created a custom "requiredif" conditional validator following this overview http://blogs.msdn.com/b/simonince/archive/2010/06/11/adding-client-side-script-to-an-mvc-conditional-validator.aspx
I used the JQuery validation handler from the MSDN blog entry which adds the following to a conditional-validators.js I include on my page's scripts:
(function ($) {
$.validator.addMethod('requiredif', function (value, element, parameters) {
var id = '#' + parameters['dependentProperty'];
// Get the target value (as a string, as that's what actual value will be)
var targetvalue = parameters['targetValue'];
targetvalue = (targetvalue == null ? '' : targetvalue).toString().toLowerCase();
// Get the actual value of the target control
var actualvalue = ($(id).val() == null ? '' : $(id).val()).toLowerCase();
// If the condition is true, reuse the existing required field validator functionality
if (targetvalue === actualvalue)
return $.validator.methods.required.call(this, value, element, parameters);
return true;
});
})(jQuery);
Alas, this does not cause a client-side validation to fire ... only the server-side validation fires. The inherent "required" validators DO fire client-side, meaning I have my script includes set-up correctly for basic validation. Has anyone accomplished custom validators in a variable length list in MVC 2 using JQuery as the client-side validation method?
NOTE that this same custom validator works client-side using the exact same set-up on a non-variable length list.
Turns out that it was a field ID naming issue with the way that collection IDs render in a variable length list. The validator was attempting to name the element ID of the dependent property with the expected statement of:
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty);
I analyzed the HTML viewsource (posted in my comment, above), and actually, [ and ] characters are not output in the HTML of the collection-index elements... they're replaced with _... so, when I changed my CustomValidator.cs to have the dependent property set to:
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty).Replace("[", "_").Replace("]", "_");
... then the client-side validator works since the name matches. I'll have to dig deeper to see WHY the ID is getting renamed in Sanderson's collection index method, below...
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Sendz.WebUI.Helpers
{
public static class HtmlPrefixScopeExtensions
{
private const string IdsToReuseKey = "__htmlPrefixScopeExtensions_IdsToReuse_";
public static IDisposable BeginCollectionItem(this HtmlHelper html, string collectionName)
{
var idsToReuse = GetIdsToReuse(html.ViewContext.HttpContext, collectionName);
var itemIndex = idsToReuse.Count > 0 ? idsToReuse.Dequeue() : Guid.NewGuid().ToString();
// autocomplete="off" is needed to work around a very annoying Chrome behaviour whereby it reuses old values after the user clicks "Back", which causes the xyz.index and xyz[...] values to get out of sync.
html.ViewContext.Writer.WriteLine(
string.Format("<input type=\"hidden\" name=\"{0}.index\" autocomplete=\"off\" value=\"{1}\" />",
collectionName, html.Encode(itemIndex)));
return BeginHtmlFieldPrefixScope(html, string.Format("{0}[{1}]", collectionName, itemIndex));
}
public static IDisposable BeginHtmlFieldPrefixScope(this HtmlHelper html, string htmlFieldPrefix)
{
return new HtmlFieldPrefixScope(html.ViewData.TemplateInfo, htmlFieldPrefix);
}
private static Queue<string> GetIdsToReuse(HttpContextBase httpContext, string collectionName)
{
// We need to use the same sequence of IDs following a server-side validation failure,
// otherwise the framework won't render the validation error messages next to each item.
var key = IdsToReuseKey + collectionName;
var queue = (Queue<string>)httpContext.Items[key];
if (queue == null)
{
httpContext.Items[key] = queue = new Queue<string>();
var previouslyUsedIds = httpContext.Request[collectionName + ".index"];
if (!string.IsNullOrEmpty(previouslyUsedIds))
foreach (var previouslyUsedId in previouslyUsedIds.Split(','))
queue.Enqueue(previouslyUsedId);
}
return queue;
}
#region Nested type: HtmlFieldPrefixScope
private class HtmlFieldPrefixScope : IDisposable
{
private readonly string _previousHtmlFieldPrefix;
private readonly TemplateInfo _templateInfo;
public HtmlFieldPrefixScope(TemplateInfo templateInfo, string htmlFieldPrefix)
{
_templateInfo = templateInfo;
_previousHtmlFieldPrefix = templateInfo.HtmlFieldPrefix;
templateInfo.HtmlFieldPrefix = htmlFieldPrefix;
}
#region IDisposable Members
public void Dispose()
{
_templateInfo.HtmlFieldPrefix = _previousHtmlFieldPrefix;
}
#endregion
}
#endregion
}
}
A complete validator / attribute reference...
public class RequiredIfAttribute : ValidationAttribute
{
private RequiredAttribute innerAttribute = new RequiredAttribute();
public string DependentProperty { get; set; }
public object TargetValue { get; set; }
public RequiredIfAttribute(string dependentProperty, object targetValue)
{
this.DependentProperty = dependentProperty;
this.TargetValue = targetValue;
}
public override bool IsValid(object value)
{
return innerAttribute.IsValid(value);
}
}
public RequiredIfValidator(ModelMetadata metadata, ControllerContext context, RequiredIfAttribute attribute)
: base(metadata, context, attribute) { }
public override IEnumerable<ModelValidationResult> Validate(object container)
{
// Get a reference to the property this validation depends upon
var field = Metadata.ContainerType.GetProperty(Attribute.DependentProperty);
if (field != null)
{
// Get the value of the dependent property
var value = field.GetValue(container, null);
// Compare the value against the target value
if ((value == null && Attribute.TargetValue == null) ||
(value != null && value.ToString().ToLowerInvariant().Equals(Attribute.TargetValue.ToString().ToLowerInvariant())))
{
// A match => means we should try validating this field
if (!Attribute.IsValid(Metadata.Model))
// Validation failed - return an error
yield return new ModelValidationResult { Message = ErrorMessage };
}
}
}
public override IEnumerable<ModelClientValidationRule> GetClientValidationRules()
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = ErrorMessage,
ValidationType = "requiredif"
};
var viewContext = (ControllerContext as ViewContext);
var depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(Attribute.DependentProperty).Replace("[", "_").Replace("]", "_");
rule.ValidationParameters.Add("dependentProperty", depProp);
rule.ValidationParameters.Add("targetValue", Attribute.TargetValue.ToString());
yield return rule;
}