RadGrid client side binding - telerik

I've got a question regarding the Telerik RadGrid control client side binding. I want to populate grid with the cities on the Client Side. I've got a object City, which has a property Country:
[DataContract]
[KnownType(typeof(Country))]
public class City
{
[DataMember]
public virtual string CityCode { get; set; }
[DataMember]
public virtual string CityName { get; set; }
[DataMember]}
public virtual Country Country { get; set;
}
}
[DataContract]
public class Country
{
[DataMember]
public virtual string CountryCode { get; set; }
[DataMember]
public virtual string Iso2Code { get; set; }
[DataMember]
public virtual string CountryName { get; set; }
[DataMember]
public virtual char RDC { get; set; }
}
I retrieve this data as a JSON object to the client side using the JQuery Ajax and WCF.
and then I bind it to the grid:
rgCity.set_dataSource(dataItem);
rgCity.dataBind();
Here are the Columns definition for the grid:
<Columns>
<telerik:GridBoundColumn HeaderText="City Code" DataField="CityCode" MaxLength="3"> </telerik:GridBoundColumn>
<telerik:GridBoundColumn HeaderText="City Name" DataField="CityName"></telerik:GridBoundColumn>
<telerik:GridBoundColumn HeaderText="Country Code" DataField="CountryCode" MaxLength="2"></telerik:GridBoundColumn>
</Columns>
The problem is I'm not getting the Country Code column populated with the data. I think the problem is in data binding, but I'm not sure if is it possible to bind a complex objects.
I think should be something like that:
<telerik:GridBoundColumn HeaderText="Country Code" DataField="**City.CountryCode**" MaxLength="2"></telerik:GridBoundColumn>
I appreciate any help solving that issue!

You can just overwrite the
Telerik.Web.UI.GridTableView.dataBind function by replacing
this snippet from the original minified telerik script:
var J = r[v].get_uniqueName();
var n = this.getCellByColumnUniqueName(H, J);
if (!n) {
continue;
}var D = r[v]._data.DataField;
if (typeof (D) == "undefined") {
D = J;
} var h = this._dataSource[u][D];
if (h == null) {
h = "";
with something, for example, like this:
var J = r[v].get_uniqueName();
var n = this.getCellByColumnUniqueName(H, J);
if (!n) {
continue;
}var D = r[v]._data.DataField;
if (typeof (D) == "undefined") {
D = J;
}
//change here to eval the dataField
var h = AvoidEvalDueToPerformance(this._dataSource[u], D);
if (h == null) {
h = "";
AvoidEvalDueToPerformance function is defined as follows:
function AvoidEvalDueToPerformance(object, propertyString) {
var k = propertyString.split(".");
for (var i = 0; i < k.length; i++)
if (object[k[i]]) object = object[k[i]];
else return object;
return object;
}
Hope this helps someone as this was the first result i stumbled upon searching for the answer of question "How to bind RadGrid to complex object clientside"
P.S. to overwrite a function you could write
Telerik.Web.UI.GridTableView.dataBind.prototype = function(){
//copy-paste the Telerik.Web.UI.GridTableView.dataBind.prototype contents
//from your favorite javascript debugger output
//(or grep output or w/e you prefer) here :)<br />
}

I don't think you can do complex databinding like that. Instead, I'd make a new property that returned the Country Code directly, then bind to that. Example:
[DataContract]
[KnownType(typeof(Country))]
public class City
{
...
[DataMember]}
public virtual CountryCode{ get Country.CountryCode; }
}
Declarative databinding for the grid then is what you had:
<Columns>
...
<telerik:GridBoundColumn HeaderText="Country Code" DataField="CountryCode" MaxLength="2"></telerik:GridBoundColumn>

Related

Telerik Blazor Grid: GridAutoGeneratedColumns set the width of a column in the model using Data annotation

I am using Telerik Blazor Grid and GridAutoGeneratedColumns feature to generate a grid and its columns regarding the properties of the model.
here's the question:
I remember I saw something like adding a Data annotation to the model's property which defines the ColumnWidth for instance to indicate a specific width for a column. But I cannot find it anymore.
So, generally, is there any way to define a specific column width for a property in the model so the auto generated columns can render it automatically and dynamically?
Have a look on the code so it'll be more clear:
#page "/test"
#using System.ComponentModel.DataAnnotations;
<TelerikGrid Data=#GridData
AutoGenerateColumns="true"
Pageable="true"
Sortable="true"
Groupable="true"
OnUpdate="#UpdateItem"
OnDelete="#DeleteItem"
OnCreate="#CreateItem">
<GridToolBar>
<GridCommandButton Command="Add" Icon="add">Add Employee</GridCommandButton>
</GridToolBar>
<GridColumns>
<GridColumn Field="#nameof(Employee.EmployeeId)" Title="Employee Id" Width="120px" Editable="false" />
<GridAutoGeneratedColumns />
<GridCommandColumn>
<GridCommandButton Command="Edit" Icon="edit">Edit</GridCommandButton>
<GridCommandButton Command="Delete" Icon="delete">Delete</GridCommandButton>
<GridCommandButton Command="Save" Icon="save" ShowInEdit="true">Update</GridCommandButton>
<GridCommandButton Command="Cancel" Icon="cancel" ShowInEdit="true">Cancel</GridCommandButton>
</GridCommandColumn>
</GridColumns>
</TelerikGrid>
#code {
public class Employee
{
public Employee()
{
HireDate = MeetingDate = DateTime.Now;
}
[Display(AutoGenerateField = false, Name = "Employee #")]
public int? EmployeeId { get; set; }
[Editable(false)]
[Display(Name = "Employee Name")]
public string Name { get; set; }
[Display(Name = "Age In Years")]
public int? AgeInYears { get; set; }
[Display(Name = "Graduate Grade")]
public decimal? GraduateGrade { get; set; }
[Display(Name = "HireDate")]
public DateTime HireDate { get; set; }
[Display(AutoGenerateField = false, Name = "Meeting Date")]
public DateTime MeetingDate { get; set; }
}
public List<Employee> GridData { get; set; }
protected override void OnInitialized()
{
GridData = new List<Employee>();
var rand = new Random();
for (int i = 0; i < 100; i++)
{
GridData.Add(new Employee()
{
EmployeeId = i,
Name = "Employee " + i.ToString(),
AgeInYears = rand.Next(10, 80),
HireDate = DateTime.Now.Date.AddDays(rand.Next(-20, 20)),
MeetingDate = DateTime.Now.Date.AddDays(rand.Next(20, 40)),
GraduateGrade = i % 4 + 3
});
}
}
private void CreateItem(GridCommandEventArgs args)
{
var argsItem = args.Item as Employee;
argsItem.EmployeeId = GridData.Count + 1;
argsItem.Name = "Employee " + argsItem.EmployeeId;
GridData.Insert(0, argsItem);
}
private void DeleteItem(GridCommandEventArgs args)
{
var argsItem = args.Item as Employee;
GridData.Remove(argsItem);
}
private void UpdateItem(GridCommandEventArgs args)
{
var argsItem = args.Item as Employee;
var index = GridData.FindIndex(i => i.EmployeeId == argsItem.EmployeeId);
if (index != -1)
{
GridData[index] = argsItem;
}
}
}
And the result is something like:
And what I expect is setting a specific column width for instance, something like this:
[Editable(false)]
[Display(Name = "Employee Name")]
[ColumnWidth="200px"]
public string Name { get; set; }
Thank you in advance and stay healty and productive.
The column width feature of the autogenerated columns is the ColumnWidth parameter that lets you set the same width for all of them. You can read more about it in the AutoGenerated Columns - Customization section
There is no "column width" attribute out-of-the-box in C# and so it would be strange for the grid to use it. What I can suggest you consider is that you create a custom attribute (see an example here) and use a loop to create the columns in a fashion similar to this example (you can use reflection to get the fields from the model if you don't use an expando object), and then read their attributes and set the Width parameter - essentially, make your own column generation.

Model binding issues with Kendo complex object

My problem is very similar to this Model binding issues with Kendo objects with complex child properties . The only difference is that i have another level in the object.
My model is:
Public Person
{
public int Id {get;set;}
public string Name {get;set;}
public IEnumerable<Course> Courses {get;set;}
}
public Course
{
public int Id {get;set;}
public string Description {get;set;}
public IEnumerable<Schedule> Schedules {get;set;}
}
Public Schedule
{
public DateTime Init {get;set;}
public DateTime End {get;set;}
}
This model is bound to a KendoGrid. Everything works well, except that Init and End properties are always null when I posted the model.
In the Ajax Datasource :
.Update(update => update.Action("Update", "Controller").Data("serialize"))
.Create(create => create.Action("Create", "Controller").Data("serialize"))
<script>
function serialize(data) {
for (var property in data) {
if ($.isArray(data[property])) {
serializeArray(property, data[property], data);
}
}
};
function serializeArray(prefix, array, result) {
for (var i = 0; i < array.length; i++) {
if ($.isPlainObject(array[i])) {
for (var property in array[i]) {
result[prefix + "[" + i + "]." + property] = array[i][property];
}
}
else {
result[prefix + "[" + i + "]"] = array[i];
}
}
}
</script>
What I have to do to send the properties of the lists schedules?
I had also looked at their serializeArray solution, but it didn't work for me in case of 3 level objects I had. I could have fixed that but then I didn't want to write recursive code. The solution I used is pretty straight-forward and aligned to the problem I had. Its very readable.
I absolutely wish Kendo should do this out of the box for their grid, but they told this when I raised a support question.
"You will need to send the values as additional data in this case because the built-in filtering does not support collection values. To format the data so that it will be bound by the model binder, you should follow the guidelines from my previous reply(dot notation for objects and indexer for arrays)"
Here is my C# ViewModels
//relates to one control value (for e.g. one entry in multi-select)
public class FormUnitFilter
{
public string Operator { get; set; }
public string Field { get; set; }
public string Value { get; set; }
public List<string> ValueList { get; set; }
}
//relates to a set of filters in a combined set (for e.g. the whole multi-select or a radiobutton or date control which appears in a single panel)
public class FormSetFilter
{
public List<FormUnitFilter> Filters { get; set; }
public string LogicalOperator { get; set; }
}
//relates to the whole set of filters present on the screen (for e.g. the filters across different panels)
public class FormWholeFilter
{
public List<FormSetFilter> Filters { get; set; }
public string LogicalOperator { get; set; }
}
here is my js function which converts this json model to a type recognized by MVC controller action parameter.
function buildFilterCriteria() {
var data = {};
if (modelObj) {
//reset the filters
modelObj.FormWholeFilter.Filters.length = 0;
//Assign FormWholeFilter data (outermost object)
data["FormWholeFilter.LogicalOperator"] = modelObj.FormWholeFilter.LogicalOperator;
//now iterate the filters inside FormWholeFilter (1st inner object)
for (var setIndex = 0; setIndex < modelObj.FormWholeFilter.Filters.length; setIndex++) {
var setFilter = modelObj.FormWholeFilter.Filters[setIndex];
data["FormWholeFilter.Filters[" + setIndex + "].LogicalOperator"] = setFilter.LogicalOperator;
//now iterate the filters inside FormSetFilter (2nd inner object)
for (var unitIndex = 0; unitIndex < setFilter.Filters.length; unitIndex++) {
var unitFilter = setFilter.Filters[unitIndex];
data["FormWholeFilter.Filters[" + setIndex + "].Filters[" + unitIndex + "].Operator"] = unitFilter.Operator;
data["FormWholeFilter.Filters[" + setIndex + "].Filters[" + unitIndex + "].Field"] = unitFilter.Field;
data["FormWholeFilter.Filters[" + setIndex + "].Filters[" + unitIndex + "].Value"] = unitFilter.Value;
if (unitFilter.ValueList)
for (var valIndex = 0; valIndex < unitFilter.ValueList.length; valIndex++) {
data["FormWholeFilter.Filters[" + setIndex + "].Filters[" + unitIndex + "].ValueList[" + valIndex + "]"] = unitFilter.ValueList[valIndex];
}
}
}
}
return modelObj && data;
}
Here is my controller action method which takes the Kendo grid datasourcerequest and the FormWholeFilter I pass from JavaScript.
public JsonResult ProcessFilters([DataSourceRequest] DataSourceRequest request, FormWholeFilter formWholeFilter)
{
//Method body
}
Also, when I load the page for the first time, I had assigned the modelObj to the FormWholeFilter blank json like this and thats why I could use this variable in the buildFilterCriteria method:
var modelObj;
$(document).ready(function () {
modelObj = $.parseJSON('#Html.Raw(Json.Encode(#Model))');
});

MVC 3 Post of Viewmodel with Completex IEnumerable

I have a complex class that is part of a property of a viewmodel. My viewmodel has a wine class property and a wine class has a ICollection property called CaseProductions. The CaseProduction class has several properties as well.
On the create GET event, the NewWineViewModel is instantiated, then it runs a GetCaseProductionDefaults with create a list of CaseProduction classes that have some default values, but are mostly empty.
Now, I originally used razor to do a foreach statement and just pop out my table the way I wanted it. But I've see around that doesn't work to bind this type of IEnumerable back to the viewmodel on POST. I've tried to use the below, but no dice.
EditorFor(m => m.Wine.CaseProductions)
I'm really looking for advise on what the best way to handle this is. Each wine will have a collection of caseproductions, and I want that to bind back to the wine within the viewmodel. Is their some way I can edit the ids of those elements in razor to make sure they bind? What's the best way to handle this one?
viewmodel:
public class NewWineViewModel
{
public Wine Wine { get; set; }
public VOAVIRequest VOAVIRequest { get; set; }
public bool IsRequest { get; set; }
public Dictionary<int, int> BottlesPerCase { get; set; }
public SelectList VarTypes { get; set; }
public SelectList Origins { get; set; }
public SelectList Apps { get; set; }
public SelectList Vintages { get; set; }
public SelectList Importers { get; set; }
}
case production class:
public class CaseProduction
{
public int CaseProductionID { get; set; }
public int WineID { get; set; }
public int CaseProductionSizeID { get; set; }
public int CaseCount { get; set; }
public int CountPerCase { get; set; }
public virtual CaseProductionSize CaseProductionSize { get; set; }
public virtual Wine Wine { get; set; }
}
getting default case productions:
public List<CaseProduction> GetCaseProductionDefaults(vfContext db)
{
//creates blank list of all formats
List<CaseProduction> cp = new List<CaseProduction>();
foreach (CaseProductionSize size in db.CaseProductionSizes)
{
int defaultBottlesPerCase = 1;
switch ((CaseProductionSizeEnum)size.CaseProductionSizeID)
{
case CaseProductionSizeEnum.s187ml:
defaultBottlesPerCase= 24;
break;
case CaseProductionSizeEnum.s375ml:
defaultBottlesPerCase = 12;
break;
case CaseProductionSizeEnum.s500ml:
defaultBottlesPerCase = 12;
break;
case CaseProductionSizeEnum.s750ml:
defaultBottlesPerCase = 12;
break;
default:
defaultBottlesPerCase = 1;
break;
}
cp.Add(new CaseProduction { CaseProductionSizeID = size.CaseProductionSizeID, CountPerCase = defaultBottlesPerCase, CaseProductionSize = size, WineID = this.Wine.WineID });
}
return cp;
}
my razor code for the case production table:
#foreach (vf2.Models.CaseProduction cp in Model.Wine.CaseProductions)
{
<tr>
<td>#cp.CaseProductionSize.Name
</td>
<td>#Html.Raw(#Html.TextBoxFor(m => m.Wine.CaseProductions.Where(c => c.CaseProductionSizeID == cp.CaseProductionSizeID).First().CaseCount, new { #class = "caseCount", id = "txt" + cp.CaseProductionSize.Name }).ToString().Replace("CaseCount","txt" + cp.CaseProductionSize.Name))
</td>
<td>
#Html.DropDownListFor(m => m.Wine.CaseProductions.Where(c => c.CaseProductionSizeID == cp.CaseProductionSizeID).First().CountPerCase, new SelectList(Model.BottlesPerCase, "Key", "Value", cp.CountPerCase), new { #class = "countPerCase", id = "ddl" + cp.CaseProductionSize.Name, name = "ddl" + cp.CaseProductionSize.Name})
</td>
<td class="totalBottleCalc">
</td>
</tr>
}
instantiation of my caseproduction collection:
public ActionResult Create(int ID = 0, int VintUpID = 0)
{
NewWineViewModel nw = new NewWineViewModel();
nw.Wine.CaseProductions = nw.GetCaseProductionDefaults(db);
nw.BottlesPerCase = nw.GetBottlesPerCase(db);
I believe the model binder isn't picking up on your CaseProduction objects because they don't look like a CaseProduction objects.
You have renamed CaseCount, your CaseProductionSize has no Id (nor does you CaseProduction, and it's missing several properties. In your loop you have to include all properties, and keep the names consistent with the names of your POCOs. Otherwise the model binder won't know what they are. You can put all the properties in hidden fields if you want.
You must instantiate your nested Lists and complex models in your parent models constructor. The default model binder will not instantiate child classes.
If you do that, then you can use the EditorFor(m => m.Wine.CaseProductions) should work, and you don't need the complex view code you are using.
If you want to customize how the CaseProduction is rendered, then you can create a CaseProduction.cshtml file in ~/Shared/EditorTemplates and it will use this definition to render each item in the collection (it will automatically iterate over the collection for you).
Also, you shouldn't do linq queries in your view. Your problem there is that it looks like you're passing your data entity directly to the view. This is bad design. You need to instead create a ViewModel that contains only the information needed to render the view. Then, you filter your data before you assign it to the View model.

Use knockout.js for 4 cascading dropdowns based on a hierarchy of objects

I am trying to get four cascading dropdowns using knockout.js:
Search Criteria
Sub Criteria
Value
State
I was able to get the first cascade going (but not the others due to databinding issues) by using code from the following link:
http://blogs.msdn.com/b/thebeebs/archive/2011/12/01/price-calculator.aspx
The data for these dropdowns is being returned to my razor viewpage as an IEnumrable of SearchCriterion from an MVC view using ViewBag.CriteriaData variable. The code for my classes is as follows:
public class SearchCriterion
{
public string Text { get; set; }
public string Value { get; set; }
public List<SubCriterion> SubCriteria { get; set; }
}
public class SubCriterion
{
public string SearchCriterionValue { get; set; }
public string Text { get; set; }
public string Value { get; set; }
public List<ColumnValue> ColumnValues { get; set; }
}
public class ColumnValue
{
public string SearchCriterionValue { get; set; }
public string SubCriterionValue { get; set; }
public string Text { get; set; }
public string Value { get; set; }
public IEnumerable<StateValue> StateValues { get; set; }
}
public class StateValue
{
public string SearchCriterionValue { get; set; }
public string SubCriterionValue { get; set; }
public string ColumnValue { get; set; }
public IEnumerable<int> InputStateIds { get; set; }
public IEnumerable<int> OutputStateIds { get; set; }
public int SelectedInputStateId { get; set; }
public int SelectedOutputStateId { get; set; }
public string Text { get; set; }
public string Value { get; set; }
}
The issues I am facing are in the following portions of the .cshtml code:
What do I specify in this template for the other two dropdowns. e.g. the third dropdown needs to be bound to ColumnValue.Value (ColumnValue is part of SubCriterion)
<script id='criteriaRowTemplate' type='text/html'>
<tr>
<td><select data-bind='options: criteriaData, optionsText: "Text", optionsCaption: "Search Criterion", value: SearchCriterion' /></td>
<td><select data-bind='visible: SearchCriterion, options: SearchCriterion() ? SearchCriterion().SubCriteria : null, optionsText: "Text", optionsCaption: "Sub Criterion", value: SubCriterion' /></td>
<td><select data-bind='visible: SubCriterion, options: SubCriterion() ? SubCriterion().ColumnValues : null, optionsText: "Text", optionsCaption: "Column Value", value: ColumnValue'/></td>
<td><select data-bind='visible: ColumnValue, options: ColumnValue() ? ColumnValue().StateValues : null, optionsText: "Text", optionsCaption: "State", value: StateValue'/></td>
<td><button data-bind='click: function() { viewModel.removeLine($data) }'>Remove</button></td>
</tr>
</script>
Is this correct?
var CriteriaLine = function() {
this.SearchCriterion = ko.observable();
this.SubCriterion = ko.observable();
this.ColumnValue = ko.observable();
this.StateValue = ko.observable();
// Whenever the Search Criteria changes, reset the Sub Criteria selection
this.SearchCriterion.subscribe(function() { this.SubCriterion(undefined); }.bind(this));
this.SubCriterion.subscribe(function() { this.ColumnValue(undefined); }.bind(this));
this.ColumnValue.subscribe(function() { this.StateValue(undefined); }.bind(this));
};
How do I map the complete C# object with the Javascript object? It works if we just have the first two dropdowns:
// Create a Javascript object object with the same property names as the C# object
var dataToSearch = $.map(this.lines(), function (line) { return line.StateValue() ? line.StateValue() : undefined; });
var SearchObject = new function () {
this.StateValues = dataToSearch;
};
// Convert the object to JSON
var searchCriteria = JSON.stringify(SearchObject);
Does anything need to change here for the binding?
// Apply the data from the server to the variable
var criteriaData = #Html.Raw(#Json.Encode(ViewBag.CriteriaData));
var viewModel = new Criteria();
ko.applyBindings(viewModel, document.getElementById("criteriaDiv"));
EDIT:
I am now able to populate the cascading dropdowns (updated code above). Now I have 4 columns, each column having one of the dropdowns. I also have 1...n number of rows being added dynamically by using Knockoutjs. So, the user can now select values from these dropdowns and add more rows of dropdowns if he wants. The only thing remaining is to return the values that the user selects for the dropdowns to the controller(point 3 above). I am not sure how I can do that. Any help would be appreciated.
EDIT 2:
Added working code for Item # 3 and modified the ColumnValue and StateValue classes.
I'm not sure I fully understand your question, but I'm going to take a whack at it anyway :). I think you're looking for a way to "validate" if it is in fact time to allow the next drop down to be active?
If so, you could approach it from a standpoint of Computed Observables. Basically, you would bind each of your dropdowns to a computed value which is derived from the previous dependencies.
Let me write fiddle and I'll show you :)
OK, give this a shot...sorry for the delay...http://jsfiddle.net/farina/ZNBcM/3/
I update the answer, Hope, it will help new Comers.
Methods for Binding Hierarchical Dropdowns using Knockout JS in MVC
Here you can find the good example .

How to validate one field related to another's value in ASP .NET MVC 3

I had two fields some thing like phone number and mobile number. Some thing like..
[Required]
public string Phone { get; set; }
[Required]
public string Mobile{ get; set; }
But user can enter data in either one of it. One is mandatory. How to handle them i.e how to disable the required field validator for one field when user enter data in another field and viceversa. In which event i have to handle it in javascript and what are the scripts i need to add for this. Can anyone please help to find the solution...
One possibility is to write a custom validation attribute:
public class RequiredIfOtherFieldIsNullAttribute : ValidationAttribute, IClientValidatable
{
private readonly string _otherProperty;
public RequiredIfOtherFieldIsNullAttribute(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,
"Unknown property {0}",
new[] { _otherProperty }
));
}
var otherPropertyValue = property.GetValue(validationContext.ObjectInstance, null);
if (otherPropertyValue == null || otherPropertyValue as string == string.Empty)
{
if (value == null || value as string == string.Empty)
{
return new ValidationResult(string.Format(
CultureInfo.CurrentCulture,
FormatErrorMessage(validationContext.DisplayName),
new[] { _otherProperty }
));
}
}
return null;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var rule = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.GetDisplayName()),
ValidationType = "requiredif",
};
rule.ValidationParameters.Add("other", _otherProperty);
yield return rule;
}
}
which you would apply to one of the properties of your view model:
public class MyViewModel
{
[RequiredIfOtherFieldIsNull("Mobile")]
public string Phone { get; set; }
public string Mobile { get; set; }
}
then you could have a controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new MyViewModel());
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
return View(model);
}
}
and finally a view in which you will register an adapter to wire the client side validation for this custom rule:
#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(
'requiredif', ['other'], function (options) {
var getModelPrefix = function (fieldName) {
return fieldName.substr(0, fieldName.lastIndexOf('.') + 1);
}
var appendModelPrefix = function (value, prefix) {
if (value.indexOf('*.') === 0) {
value = value.replace('*.', prefix);
}
return value;
}
var prefix = getModelPrefix(options.element.name),
other = options.params.other,
fullOtherName = appendModelPrefix(other, prefix),
element = $(options.form).find(':input[name="' + fullOtherName + '"]')[0];
options.rules['requiredif'] = element;
if (options.message) {
options.messages['requiredif'] = options.message;
}
}
);
jQuery.validator.addMethod('requiredif', function (value, element, params) {
var otherValue = $(params).val();
if (otherValue != null && otherValue != '') {
return true;
}
return value != null && value != '';
}, '');
</script>
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.Phone)
#Html.EditorFor(x => x.Phone)
#Html.ValidationMessageFor(x => x.Phone)
</div>
<div>
#Html.LabelFor(x => x.Mobile)
#Html.EditorFor(x => x.Mobile)
#Html.ValidationMessageFor(x => x.Mobile)
</div>
<button type="submit">OK</button>
}
Pretty sick stuff for something so extremely easy as validation rule that we encounter in our everyday lives. I don't know what the designers of ASP.NET MVC have been thinking when they decided to pick a declarative approach for validation instead of imperative.
Anyway, that's why I use FluentValidation.NET instead of data annotations to perform validations on my models. Implementing such simple validation scenarios is implemented in a way that it should be - simple.
I know this question is not so hot, because it was asked relatively long time ago, nevertheless I'm going to share with a slightly different idea of solving such an issue. I decided to implement mechanism which provides conditional attributes to calculate validation results based on other properties values and relations between them, which are defined in logical expressions.
Your problem can be defined and automatically solved by the usage of following annotations:
[RequiredIf("Mobile == null",
ErrorMessage = "At least email or phone should be provided.")]
public string Phone{ get; set; }
[RequiredIf("Phone == null",
ErrorMessage = "At least email or phone should be provided.")]
public string Mobile { get; set; }
If you feel it would be useful for your purposes, more information about ExpressiveAnnotations library can be found here. Client side validation is also supported out of the box.
Since nobody else suggested it, I'm going to tell you a different way to do this that we use.
If you create a notmapped field of a custom data type (in my example, a pair of gps points), you can put the validator on that and you don't even need to use reflection to get all the values.
[NotMapped]
[DCGps]
public GPS EntryPoint
{
get
{
return new GPS(EntryPointLat, EntryPointLon);
}
}
and the class, a standard getter/setter
public class GPS
{
public decimal? lat { get; set; }
public decimal? lon { get; set; }
public GPS(decimal? lat, decimal? lon)
{
this.lat = lat;
this.lon = lon;
}
}
and now the validator:
public class DCGps : DCValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
if (!(value is GPS)) {
return new ValidationResult("DCGps: This annotation only works with fields with the data type GPS.");
}
//value stored in the field.
//these come through as zero or emptry string. Normalize to ""
string lonValue = ((GPS)value).lonstring == "0" ? "" : ((GPS)value).lonstring;
string latValue = ((GPS)value).latstring == "0" ? "" : ((GPS)value).latstring;
//place validation code here. You have access to both values.
//If you have a ton of values to validate, you can do them all at once this way.
}
}

Resources