I'm using knockout to bind my view model to my view. Multiple properties in my view model are nullable, such as DateTime?s. Here's an example:
public class ViewModel
{
public int ID { get; set; }
public string Name { get; set; }
public DateTime? CreationDate { get; set;}
}
As you can see, the property CreationDate is a nullable DateTime.
I'm binding the property with a custom datepicker binder:
ko.bindingHandlers.datepicker = {
init: function (element, valueAccessor, allBindingsAccessor, viewModel) {
try {
var jsonDate = ko.utils.unwrapObservable(valueAccessor());
var value = parseJsonDateString(jsonDate);
var strDate = value.getMonth() + 1 + "/"
+ value.getDate() + "/"
+ value.getFullYear();
element.setAttribute('value', strDate);
}
catch (exc) {
}
$(element).change(function () {
var value = valueAccessor();
value(element.getAttribute('value'));
});
},
update: function (element, valueAccessor, allBindingsAccessor, viewModel) {
var val = valueAccessor();
val(element.getAttribute('value'));
}
};
var jsonDateRE = /^\/Date\((-?\d+)(\+|-)?(\d+)?\)\/$/;
var parseJsonDateString = function (value) {
var arr = value && jsonDateRE.exec(value);
if (arr) {
return new Date(parseInt(arr[1]));
}
return value;
};
This enables me to bind my property in the view like so:
<input type="text" data-bind="datepicker: CreationDate" />
Problem
Here's the problem. Sometimes this property is already null when it enters the view. A JSON example could look like this:
{
"Id": 2004,
"Name": "Test",
"CreationDate": null
}
If this is the case, and I change this value to some random value from the datepicker, and send an ajax POST to my controller, I can see that models CreationDate still is equal to null.
So if the DateTime is null as the model enters the view, how do I populate the models property?
Found the solution on my own
I managed to solve my issue by simply changing my binding to the following:
ko.bindingHandlers.datepicker = {
init: function (element, valueAccessor, allBindingsAccessor) {
//initialize datepicker with some optional options
var options = allBindingsAccessor().datepickerOptions || {};
$(element).datepicker(options);
//handle the field changing
ko.utils.registerEventHandler(element, "change", function () {
var observable = valueAccessor();
var value = $(element).val();
// if the input field is empty, the value is falsy and therefore the observable should be = null
if(!value){
observable(null);
} else {
var date = new Date(value);
observable(date);
}
});
//handle disposal (if KO removes by the template binding)
ko.utils.domNodeDisposal.addDisposeCallback(element, function () {
$(element).datepicker("destroy");
});
},
//update the control when the view model changes
update: function (element, valueAccessor) {
var value = ko.unwrap(valueAccessor());
//if the value received is null, we should display nothing in the input field
if (value === null) {
$(element).val(null);
} else {
//we need to manipulate the data to show something user friendly to the user
var date = parseJsonDateString(value);
var strDate = date.getMonth() + 1 + "/"
+ date.getDate() + "/"
+ date.getFullYear();
$(element).val(strDate);
}
}
};
So basically if the value I'm getting in the update function, simply set $(element).val(null). This way, nullable properties are handled correctly.
Related
in this project i create cardGroup. in httpGet Method we get some needed info and pass to view to fill dropdown. when httpPost trigger if some field Date has Problem we must return error with addModelError but after return View, all ViewData Clear and Return Exception. how can handle this. just show error in view.
[HttpGet]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup()
{
var discounts =
UnitOfWork.DiscountPatternRepository.GetNotExpireDiscountPattern();
var discountDtos = discounts?.Select(c => new SelectListItem
{
Text = c.PatternTitle,
Value = c.Id.ToString()
}).ToList();
ViewData["DiscountPatterns"] = discountDtos;
var serials =
UnitOfWork.ChargeCardSerialRepository.GetNotAssignedSerials();
var serialDtos = serials?.Select(c => new SelectListItem
{
Text = c.SerialNumber.ToString(),
Value = c.Id.ToString()
}).ToList();
ViewData["ChargeSerials"] = serialDtos;
ViewData["CardSerialCount"] =
UnitOfWork.GiftCardSerialRepository.GetNotUsedGiftSerials();
return View();
}
[HttpPost]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup(CardGroupCreateDto dto)
{
if (!ModelState.IsValid)
return View(dto);
if(!UnitOfWork.DiscountPatternRepository
.IsCardGroupDateInRange(dto.DiscountPatternId,
dto.ActiveFromDate, dto.ActiveToDate))
{
ModelState.AddModelError("ActiveFromDate", #"Error In Date.");
return View(dto); <---Problem Here
}
var group = dto.LoadFrom();
var insertedId = UnitOfWork.CardGroupRepository.Add(group);
foreach (var rangeDto in group.CardGroupGiftSerialRanges)
{
for (var i = rangeDto.GiftCardSerialBegin; i <=
rangeDto.GiftCardSerialEnd; i++)
{
var serial =
UnitOfWork.GiftCardSerialRepository.GetBySerial(i);
if (serial != null)
{
serial.CardGroupGiftSerialRangeId = rangeDto.Id;
serial.DiscountPatternId = group.DiscountPatternId;
UnitOfWork.Complete();
}
}
}
return Redirect("/CardGroup");
}
From this article:
ViewData
ViewData is a property of ControllerBase class.
ViewData is used to pass data from controller to corresponding view
Its life lies only during the current request. If redirection occurs, then its value becomes null. It’s required typecasting for getting data and check for null values to avoid error.
So what's happening is once you've done your post back to the server, you're now in a different request, meaning, that you need to repopulate your ViewData items so that their values are populated again, or else they'll be null.
So I'd recommend refactoring your Dropdown population method into a private method on your controller and then call that method in your post when you find a validation error or are just returning by calling return View(dto).
If they're used in other controllers, you can add them to a LookupService or LookupRepository or even a general helpers class that contains your lookup logic (whatever fits into your UnitofWork pattern the best for you), to make them available to those other controllers, instead of having it as a private method as per my example.
So something like this for example:
[HttpGet]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup()
{
PopulateCreateCardGroupLookups();
return View();
}
[HttpPost]
[Route("CreateCardGroup")]
public ActionResult CreateCardGroup(CardGroupCreateDto dto)
{
if (!ModelState.IsValid)
{
PopulateCreateCardGroupLookups();
return View(dto);
}
if(!UnitOfWork.DiscountPatternRepository
.IsCardGroupDateInRange(dto.DiscountPatternId,
dto.ActiveFromDate, dto.ActiveToDate))
{
ModelState.AddModelError("ActiveFromDate", #"Error In Date.");
PopulateCreateCardGroupLookups();
return View(dto); <---Problem Here
}
var group = dto.LoadFrom();
var insertedId = UnitOfWork.CardGroupRepository.Add(group);
foreach (var rangeDto in group.CardGroupGiftSerialRanges)
{
for (var i = rangeDto.GiftCardSerialBegin; i <=
rangeDto.GiftCardSerialEnd; i++)
{
var serial =
UnitOfWork.GiftCardSerialRepository.GetBySerial(i);
if (serial != null)
{
serial.CardGroupGiftSerialRangeId = rangeDto.Id;
serial.DiscountPatternId = group.DiscountPatternId;
UnitOfWork.Complete();
}
}
}
return Redirect("/CardGroup");
}
private void PopulateCreateCardGroupLookups()
{
var discounts =
UnitOfWork.DiscountPatternRepository.GetNotExpireDiscountPattern();
var discountDtos = discounts?.Select(c => new SelectListItem
{
Text = c.PatternTitle,
Value = c.Id.ToString()
}).ToList();
ViewData["DiscountPatterns"] = discountDtos;
var serials =
UnitOfWork.ChargeCardSerialRepository.GetNotAssignedSerials();
var serialDtos = serials?.Select(c => new SelectListItem
{
Text = c.SerialNumber.ToString(),
Value = c.Id.ToString()
}).ToList();
ViewData["ChargeSerials"] = serialDtos;
ViewData["CardSerialCount"] =
UnitOfWork.GiftCardSerialRepository.GetNotUsedGiftSerials();
}
This is something that has always puzzled me as to the best way round, while keeping maintainable code. The below code sets up a list of months and years for a payment gateway form, before assigning these to a variable of type List<SelectListItem>.
Intial Action
PayNowViewModel paymentGateway = new PayNowViewModel();
List<SelectListItem> paymentGatewayMonthsList = new List<SelectListItem>();
List<SelectListItem> paymentGatewayYearsList = new List<SelectListItem>();
for (int i = 1; i <= 12; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayMonthsList.Add(selectListItem);
}
int year = DateTime.Now.Year;
for (int i = year; i <= year + 10; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayYearsList.Add(selectListItem);
}
paymentGateway.ExpiryMonth = paymentGatewayMonthsList;
paymentGateway.ExpiryYear = paymentGatewayYearsList;
return View(paymentGateway);
It's a fair bit of code, and I find myself repeating this code, in similar formats to re-setup the dropdown lists options should the ModelState.IsValid be false and I want to return back to the view for the user to correct there mistakes.
HttpPost Action - Code
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ConfirmPayment(PayNowViewModel paymentGatewayForm, FormCollection form)
{
if (ModelState.IsValid)
{
// Post processing actions...
return View();
}
else
{
for (int i = 1; i <= 12; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayMonthsList.Add(selectListItem);
}
int year = DateTime.Now.Year;
for (int i = year; i <= year + 10; i++)
{
SelectListItem selectListItem = new SelectListItem();
selectListItem.Value = i.ToString();
selectListItem.Text = i.ToString("00");
paymentGatewayYearsList.Add(selectListItem);
}
form.ExpiryMonth = paymentGatewayMonthsList;
form.ExpiryYear = paymentGatewayYearsList;
return View("MakePayment", form);
}
}
What's the best way to centralise this dropdown setup code so its only in one place? At present you'll see a large proportion (the for loops), is exactly repeated twice. A base controller with function? Or is it better to re-setup like the above?
Any advice appreciated!
Mike.
Add a private method to your controller (the following code assumes your ExpiryMonth and ExpiryYear properties are IEnumerable<SelectListItem> which is all that the DropDownListFor() method requires)
private void ConfigureViewModel(PayNowViewModel model)
{
model.ExpiryMonth = Enumerable.Range(1, 12).Select(m => new SelectListItem
{
Value = m.ToString(),
Text = m.ToString("00")
});
model.ExpiryYear = Enumerable.Range(DateTime.Today.Year, 10).Select(y => new SelectListItem
{
Value = y.ToString(),
Text = y.ToString("00")
});
}
and then in the GET method
public ActionResult ConfirmPayment()
{
PayNowViewModel model = new PayNowViewModel();
ConfigureViewModel(model);
return View(model);
}
and in the POST method
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ConfirmPayment(PayNowViewModel model)
{
if (!ModelState.IsValid)
{
ConfigureViewModel(model);
return View(model);
}
.... // save and redirect (should not be returning the view here)
}
If the set of your dropdown options is fixed (or recompilation is OK after the potential options change), you can use an enum to store your options.
public enum Month {
// if the dropdown is not required, add default value 0
Optional = 0,
[Display(Name = #"Month_January")]
January = 1,
[Display(Name = #"Month_February")]
February = 2,
// etc ..
}
To render this as a dropdown use an EditorTemplate Enum.cshtml:
#model Enum
#{
var enumType = ViewData.ModelMetadata.ModelType;
var allValues = Enum.GetValues(enumType).Cast<object>().ToSelectList(Model);
// read any attributes like [Required] from ViewData and ModelMetadata ...
var attributes = new Dictionary<string, object>();
}
#Html.DropDownListFor(m => m, allValues, attributes)
The ToSelectList extension method loops over all enum values and converts them to SelectListItems:
public static IList<SelectListItem> ToSelectList<T>(this IEnumerable<T> list) {
return ToSelectList<T>(list, list.FirstOrDefault());
}
public static IList<SelectListItem> ToSelectList<T>(this IEnumerable<T> list, T selectedItem) {
var items = new List<SelectListItem>();
var displayAttributeType = typeof(DisplayAttribute);
foreach (var item in list) {
string displayName;
// multi-language:
// assume item is an enum value
var field = item.GetType().GetField(item.ToString());
try {
// read [Display(Name = #"someKey")] attribute
var attrs = (DisplayAttribute)field.GetCustomAttributes(displayAttributeType, false).First();
// lookup translation for someKey in the Resource file
displayName = Resources.ResourceManager.GetString(attrs.Name);
} catch {
// no attribute -> display enum value name
displayName = item.ToString();
}
// keep selected value after postback:
// assume selectedItem is the Model passed from MVC
var isSelected = false;
if (selectedItem != null) {
isSelected = (selectedItem.ToString() == item.ToString());
}
items.Add(new SelectListItem {
Selected = isSelected,
Text = displayName,
Value = item.ToString()
});
}
return items;
}
To support multiple languages, add translations for the display name keys, e.g. "Month_January", to the Resource file.
Now that the setup code has been abstracted away using some reflection magic, creating a new viewmodel is a breeze :>
public class PayNowViewModel {
// SelectListItems are only generated if this gets rendered
public Month ExpiryMonth { get; set; }
}
// Intial Action
var paymentGateway = new PayNowViewModel();
return View(paymentGateway);
// Razor View: call the EditorTemplate
#Html.EditorFor(m => m.ExpiryMonth)
Note that in the EditorTemplate, Model is passed as the selected item to ToSelectList. After postback, Model will hold the currently selected value. Therefore it stays selected, even if you just return the model after an error in the controller:
// HttpPost Action
if (!ModelState.IsValid) {
return View("MakePayment", paymentGatewayForm);
}
Took us some time to come up with this solution, credits go to the Saratiba team.
I have an asp.net-mvc site and I user jqgrid on the front end. I have a simple page using jqgrid and I filter down my jqgrid results (server side filter) using the top bar filter of the advanced filter.
I now want a way where I can share a URL with someone else and when they load the page, they get the same filter applied so somehow I need to take the filter criteria and append it to the query string.
The issue is that I can do this "manually" field by field like this by creating queryparams like
myurl?NameFilter=JoeBrown
and then doing something like this in my asp.net-mvc view
var myfilter = { groupOp: "AND", rules: [] };
<% if (!String.IsNullOrEmpty(Model.NameFilter)) { %>
myfilter.rules.push({ field: "Name", op: "eq", data: "<% = Model.NameFilter%>" });
<%}%>
but that doesn't really scale very well given I have many different pages with lots of columns so I am looking for a more generic way to persist the filter values into a URL and then apply them again so that I can then model bind on the server side back to my controller action.
Here is an example Server Side controller action that I am calling to load data from the server:
public ActionResult GridData(GridData args)
{
var data = GetData(args).Paginate(args.page ?? 1, args.rows ?? 10,
i =>
new
{
i.Id,
i.Name
}
}
so basically I need the query string to bind to my GridData class similar to what happens when I do a normal filter that gets posted on the ajax call when I do a regular filter.
My GridData class looks like this:
public class GridData
{
public int? page { get; set; }
public int? rows { get; set; }
public bool search { get; set; }
public string sidx { get; set; }
public string sord { get; set; }
public Filter Where { get; set; }
}
public class Filter
{
public string groupOp { get; set; }
public Rule[] rules { get; set; }
}
public class Rule
{
public string field { get; set; }
public string op { get; set; }
public string data { get; set; }
}
If you are looking for a way to construct a model that will bind directly from query string values, you can try the following. The methods buildParamsCustom() and serializeJson() are basically taken from jQuery source and modified for creating a query string which will be supported by the MVC default model binder.
http://api.jquery.com/jquery.param/ has the details about the default jQuery implementation.
I have tested for your scenario. I am able to view the serialized data.
var r20 = /%20/g, rbracket = /\[\]$/;
function buildParamsCustom(prefix, obj, traditional, add) {
var name;
if (jQuery.isArray(obj)) {
// Serialize array item.
jQuery.each(obj, function (i, v) {
if (traditional || rbracket.test(prefix)) {
// Treat each array item as a scalar.
add(prefix, v);
} else {
// Item is non-scalar (array or object), encode its numeric index.
buildParamsCustom(prefix + "[" + (typeof v === "object" ? i : "") + "]", v, traditional, add);
}
});
} else if (!traditional && jQuery.type(obj) === "object") {
// Serialize object item.
for (name in obj) {
buildParamsCustom(prefix + "." + name, obj[name], traditional, add);
}
} else {
// Serialize scalar item.
add(prefix, obj);
}
}
// Serialize an array of form elements or a set of
// key/values into a query string
var serializeJson = function (a, traditional) {
var prefix,
s = [],
add = function (key, value) {
// If value is a function, invoke it and return its value
value = jQuery.isFunction(value) ? value() : (value == null ? "" : value);
s[s.length] = encodeURIComponent(key) + "=" + encodeURIComponent(value);
};
// Set traditional to true for jQuery <= 1.3.2 behavior.
if (traditional === undefined) {
traditional = jQuery.ajaxSettings && jQuery.ajaxSettings.traditional;
}
// If an array was passed in, assume that it is an array of form elements.
if (jQuery.isArray(a) || (a.jquery && !jQuery.isPlainObject(a))) {
// Serialize the form elements
jQuery.each(a, function () {
add(this.name, this.value);
});
} else {
// If traditional, encode the "old" way (the way 1.3.2 or older
// did it), otherwise encode params recursively.
for (prefix in a) {
buildParamsCustom(prefix, a[prefix], traditional, add);
}
}
// Return the resulting serialization
return s.join("&").replace(r20, "+");
};
// Get this data from the grid
var data = { "page": 1, "rows": 10, "search": true, "Where": { "groupop": "AND", "rules": [{ "field": "Name", "op": "EQ", data: "John" }, { "field": "Title", "op": "EQ", data: "Mr" }] } };
var queryString = serializeJson(data);
var url = "someurl" + "?" + decodeURIComponent(queryString);
// Send your GET request here.
I'm using Steve Sandersons BeginCollectionItem extension to help with binding lists of items. This works fine for primitive types. The problem I'm having is that for a custom model binder that I've written I can't see how to generate the full name and index of the item that I'm binding to.
Currently my model binder looks like this:
public class MoneyModelBinder : DefaultModelBinder
{
protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
var valueResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Amount");
if (valueResult != null)
{
var value = valueResult.AttemptedValue;
var currencyCode = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Iso3LetterCode").AttemptedValue;
var money = (Money) bindingContext.Model;
Money parsedValue;
if (String.IsNullOrEmpty(value))
{
money.Amount = null;
return;
}
var currency = Currency.FromIso3LetterCode(currencyCode);
if (!Money.TryParse(value, currency, out parsedValue))
{
bindingContext.ModelState.AddModelError("Amount", string.Format("Unable to parse {0} as money", value));
}
else
{
money.Amount = parsedValue.Amount;
money.Currency = parsedValue.Currency;
}
}
else
{
base.OnModelUpdated(controllerContext, bindingContext);
}
}
}
My ViewModel Lokks like this (some propertis omitted for clarity):
public class EditFeeEarningCapacityViewModel
{
public List<FeeEarner> FeeEarners { get; set; }
public class FeeEarner
{
public Money AverageChargeOutRate { get; set; }
}
}
My Edit Template for the Money type looks like this:
#model Core.Money
#{
int decimalPlaces;
if(!int.TryParse(string.Format("{0}", ViewData["DecimalPlaces"]), out decimalPlaces))
{
decimalPlaces = 0;
}
}
<div class="input-prepend">
<span class="add-on">#Model.Currency.Symbol</span>#Html.TextBoxFor(m => m.Amount,
new
{
placeholder = string.Format("{0}", Model.Currency),
#class = "input-mini",
Value = String.Format("{0:n" + decimalPlaces + "}", Model.Amount)
})
</div>
#Html.HiddenFor(x => x.Iso3LetterCode)
For a form that has post values like this:
FeeEarners.index 3fa91d09-0617-4bea-ae3f-d84862be8c04
FeeEarners[3fa91d09-0617-4bea-ae3f-d84862be8c04].feeEarner.AverageChargeOutRate.Amount 500
FeeEarners[3fa91d09-0617-4bea-ae3f-d84862be8c04].feeEarner.AverageChargeOutRate.Iso3LetterCode GBP
I can't see how to detect the index of the item or the property name that I'm binding to. So essentially, how do I find the index of the item I'm trying to bind to and the name of the property that I'm trying to bind the data from?
I am not fimilar with that Helper but for collection i am doing a bit different trick.
define key
var key = "EditModel[{0}].{1}";
var index = 0;
then build form
foreach(var fee in Model.FeeEarners){
#Html.TextBox(string.Format(key, index, "PropertyNameFromYourFeeClass"));
//It will build text box and set value
}
On Controller side
create action with input parameter
public ActionResult Save(EditFeeEarningCapacityViewModel editModel){
...your code here
}
For a project at work I'm trying to create a process that lets a user dynamically create a form that other users could then fill out the values for. I'm having trouble figuring out how to go about getting this to play nice with the built in model binding and validation with ASP MVC 3, though.
Our view model is set up something like this. Please note that I've over simplified the example code:
public class Form
{
public FieldValue[] FieldValues { get; set; }
}
public class Field
{
public bool IsRequired { get; set; }
}
public class FieldValue
{
public Field Field { get; set; }
public string Value { get; set; }
}
And our view looks something like:
#model Form
#using (Html.BeginForm("Create", "Form", FormMethod.Post))
{
#for(var i = 0; i < Model.Fields.Count(); i++)
{
#Html.TextBoxFor(_ => #Model.Fields[i].Value)
}
<input type="submit" value="Save" name="Submit" />
}
I was hoping that we'd be able to create a custom ModelValidatorProvider or ModelMetadataProvider class that would be able to analyze a FieldValue instance, determine if its Field.IsRequired property is true, and then add a RequiredFieldValidator to that specific instance's validators. I'm having no luck with this, though. It seems that with ModelValidatorProvider(and ModelMetadataProvider) you can't access the parent container's value(ie: GetValidators() will be called for FieldValue.Value, but there's no way from there to get the FieldValue object).
Things I've tried:
In the ModelValidatorProvider, I've tried using
ControllerContext.Controller.ViewData.Model, but that doesn't work if
you have nested types. If I'm trying to figure out the validators
Form.FieldValues[3], I have no idea which FieldValue to use.
I tried using a custom ModelMetadata that tries to use the internal
modelAccessor's Target property to get the parent, but this also
doesn't work if you have a nested type. Somewhere internal to MVC, an
expression like the one in my example will result in the Target being
the Model's type(Form), not FieldValue. So I get the same problem as
above where I have no idea what instance of FieldValue to compare
against.
A class-level validation attribute that I could put on the FieldValue
class itself, but this only gets called during server validation. I
need client-side validation, too.
Is what I'm trying to do even possible in MVC? Or is there something I'm missing entirely?
One possibility is to use a custom validation attribute.
But before getting into the implementation I would like to point out a potential flaw in your scenario. The IsRequired property is part of your model. This means that when the form is submitted its value must be known so that we conditionally apply the required rule to the corresponding property. But for this value to be known when the form is submitted this means that it must be either part of the form (as a hidden or standard input field) or must be retrieved from somewhere (datastore, ...). The problem with the first approach is obvious => hidden field means that the user can set whatever value he likes, so it's no longer a real validation because it is the user that decides which field is required.
This warning being said, let's suppose that you trust your users and decide to take the hidden field approach for storing the IsRequired value. Let's see how a sample implementation:
Model:
public class Form
{
public FieldValue[] Fields { get; set; }
}
public class FieldValue
{
public Field Field { get; set; }
[ConditionalRequired("Field")]
public string Value { get; set; }
}
public class Field
{
public bool IsRequired { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new Form
{
Fields = new[]
{
new FieldValue { Field = new Field { IsRequired = true }, Value = "" },
new FieldValue { Field = new Field { IsRequired = true }, Value = "" },
new FieldValue { Field = new Field { IsRequired = false }, Value = "value 3" },
}
};
return View(model);
}
[HttpPost]
public ActionResult Index(Form model)
{
return View(model);
}
}
View:
#model Form
#using (Html.BeginForm())
{
#Html.EditorFor(x => x.Fields)
<input type="submit" value="Save" name="Submit" />
}
ConditionalRequiredAttribute:
public class ConditionalRequiredAttribute : ValidationAttribute, IClientValidatable
{
private RequiredAttribute _innerAttribute = new RequiredAttribute();
private readonly string _fieldProperty;
public ConditionalRequiredAttribute(string fieldProperty)
{
_fieldProperty = fieldProperty;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var containerType = validationContext.ObjectInstance.GetType();
var field = containerType.GetProperty(_fieldProperty);
if (field == null)
{
return new ValidationResult(string.Format("Unknown property {0}", _fieldProperty));
}
var fieldValue = (Field)field.GetValue(validationContext.ObjectInstance, null);
if (fieldValue == null)
{
return new ValidationResult(string.Format("The property {0} was null", _fieldProperty));
}
if (fieldValue.IsRequired && !_innerAttribute.IsValid(value))
{
return new ValidationResult(this.ErrorMessage, new[] { validationContext.MemberName });
}
return ValidationResult.Success;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule()
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "conditionalrequired",
};
rule.ValidationParameters.Add("iserquiredproperty", _fieldProperty + ".IsRequired");
yield return rule;
}
}
Associated unobtrusive adapter:
(function ($) {
$.validator.unobtrusive.adapters.add('conditionalrequired', ['iserquiredproperty'], function (options) {
options.rules['conditionalrequired'] = options.params;
if (options.message) {
options.messages['conditionalrequired'] = options.message;
}
});
$.validator.addMethod('conditionalrequired', function (value, element, parameters) {
var name = $(element).attr('name'),
prefix = name.substr(0, name.lastIndexOf('.') + 1),
isRequiredFiledName = prefix + parameters.iserquiredproperty,
requiredElement = $(':hidden[name="' + isRequiredFiledName + '"]'),
isRequired = requiredElement.val().toLowerCase() === 'true';
if (!isRequired) {
return true;
}
return value && value !== '';
});
})(jQuery);