I create requridif attribute as described in http://blogs.msdn.com/b/simonince/archive/2011/02/04/conditional-validation-in-asp-net-mvc-3.aspx
I have classes like this:
public class MainClass
{
public string Title { get; set; }
public SubClass Additional { get; set; }
}
public class SubClass
{
[RequiredIf("Field1NotExists", false, ErrorMessage = "field 1 is required")]
public string Field1 { get; set; }
public bool Field1NotExists { get; set; }
}
When in view I use EditorFor:
#Html.EditorFor(m => m.Additional.Field1)
#Html.EditorFor(m => m.Additional.Field1NotExists)
unobtrusive client side validation work properly
But when I use TextBoxFor and CheckBoxFor
#Html.TextBoxFor(m => m.Additional.Field1)
#Html.CheckBoxFor(m => m.Additional.Field1NotExists)
unobtrusive client side validation doesn't work
Part of requiredif attribute:
private string BuildDependentPropertyId(ModelMetadata metadata, ViewContext viewContext)
{
string depProp = viewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty);
var thisField = metadata.PropertyName + "_";
if (depProp.StartsWith(thisField))
depProp = depProp.Substring(thisField.Length);
else
{
var thisFieldInMiddle = "_" + metadata.PropertyName + "_";
if (!this.DependentProperty.Contains(thisFieldInMiddle))
depProp = depProp.Replace(thisFieldInMiddle, "_");
}
return depProp;
}
When I use EditorFor ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty) return full id (Additional_Field1_Field1NotExists)
And when I use TExtBoxFor ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(this.DependentProperty) return only las field name (Field1NotExists instead of Additional_Field1NotExists)
Why ViewContext is different in this two ways? And how can I get full field id when I use TextBoxFor?
Sorry for bad English
TextBoxFor and other input extensions don't persist prefixes, only editor and display extensions do. If you check the TemplateInfo.HtmlFieldPrefix you'll see that it's empty.
You'll also find that if you're using TextBoxFor with complex ViewModels, data won't be bound correctly when you submit for this very reason. Another reason to use Editor templates instead.
Related
I have a model of question list which of type BOOL, STRING and OBJECT. whenever i select any response it has to bind the value to the model and while submitting the form it has to post the value.
Model.cs
public enum QuestionType
{
bool,
list,
YesorNo
}
public class Question
{
public string Id{ get; set; }
public QuestionType Type{ get; set; }
public object ResponseValue{ get; set; }
public List<Option> Option { get; set; }
}
The List option will have the list the options when the question type is list. Below is the Option model
public class Option
{
public int Id{ get; set; }
public string name{ get; set; }
public bool selected{ get; set; }
}
so whenever iam giving the response for the question which is of
BOOL type then the value should be binded as True or False to the ResponseValue
LIST type then it has to get all the selected options along with the Id and name and bind it to ResponseValue as object
YesorNo then the value should be binded as Yes, NO or NA to the ResponseValue
Iam using c# razor view for model binding
#Html.CheckBoxFor(m => Model.ResponseValue, new { #class = "form-control" })
The above code throws error because of its datatype object but the checkboxfor accepts only bool data type.
Please suggest how make the ResponseValue to accept all the data type(bool, string and object)
It would probably be better if you could come up with a design that does not make use of object, but if there is no other way, you might get it to work using something like the following:
Cast the variable to the corresponding type:
#{
bool? responseValueBool = Model.ResponseValue as bool?;
}
Use the new variable in the call of the HTML Helper:
#if (responseValueBool.HasValue)
{
var ResponseValue = responseValueBool.Value;
#Html.CheckBoxFor(m => ResponseValue, new {#class = "form-control"});
}
Edit: To include the index in the element's name, you could put the ResponseValues into a list, so that you are able to pass the element as expected in the form of m => ResponseValue[index], as shown in the docs, e.g.:
var ResponseValue = new List<bool> {responseValueBool.Value};
#Html.CheckBoxFor(m => ResponseValue[0], new {#class = "form-control", name="ResponseValue[1]"});
You would need to take care of using continuous indexes though.
Note that you are not forced to use the HTML Helper and could also create the HTML elements yourself to avoid that kind of workarounds.
I noticed a strange behavior in which there is inconsistent requirement of Model prefix for asp.net core tag helpers below. asp-for cannot accept Model prefix but asp-items must have Model prefix. My head explodes.
#model ProblemVM
<select
asp-for="Problem.TagId"
asp-items="Model.Tags.ToSelectListItem(Model.Problem.TagId)"
/>
public class ProblemVM
{
public IEnumerable<Tag> Tags { get; set; }
public Problem Problem{ get; set; }
}
Related classes.
public abstract class ISelectListItemable
{
[Key]
public int Id { get; set; }
[Required]
public virtual string Name { get; set; }
}
public class Tag: ISelectListItemable
{
[Display(Name = "Tag Name")]
public override string Name { get; set; }
}
public class Problem : ISelectListItemable
{
[Display(Name = "Problem Name")]
public override string Name { get; set; }
public int TagId { get; set; }
[ForeignKey(nameof(TagId))]
public virtual Tag Tag { get; set; }
}
public static IEnumerable<SelectListItem> ToSelectListItem<T>(this IEnumerable<T> items, int selectedValue)
where T : ISelectListItemable
{
return from item in items
select new SelectListItem
{
Text = item.Name,
Value = item.Id.ToString(),
Selected = item.Id.Equals(selectedValue)
};
}
Question
What is the rule of using Model prefix for tag helpers?
It's about the actual type of the property the attribute of the tag helper corresponds to. The asp-for attribute maps to a For property on the built-in tag helpers which is typed as ModelExpression. So, it literally is looking for an expression relative to the model of the view.
The asp-items attribute, on the other hand is typed as IEnumerable<SelectListItem>, and thus literally needs a concrete value, not just an expression. The fact that it just so happens to be coming from a prop on your model is inconsequential. The value could come from ViewData, or be satisfied directly inline.
There are certain situations, though, when you still need to include Model for an attribute like asp-for. This is generally when your model itself is a list, dictionary, etc. and you need to index. You can't just add something like [i].Foo as an expression, so in that case you would do #Model[i].Foo.
The documentation does say that:
The asp-for attribute value is a special case and doesn't require a Model prefix, the other Tag Helper attributes do (such as asp-items)
The reason I see is that asp-for will always be from your model. But asp-items can be any collection. It doesn't have to be from your model. So if you do want it to be from your model, you need to tell it that.
Is it correct a model?
public class NewForm
{
public string[] Field { get; set; }
public bool[] Check { get; set; }
}
for such a VIEW:
#Html.TextAreaFor(model => model.Field)
#Html.TextAreaFor(model => model.Field)
#Html.TextAreaFor(model => model.Field)
#Html.CheckBoxFor(model => model.Check)
#Html.CheckBoxFor(model => model.Check)
Or is there a better way to create fields of the same name?
In Controller displays only the first value. But i need all
[HttpPost]
public ActionResult Edit(NewForm model)
{
Response.Write(model.Field);
Response.Write(model.Check);
}
Fields may be an indefinite number due to the fact that by clicking on the button JavaScript adds a new field of the same name with the same name
It sounds like you want to submit multiple instances of your model back to the controller.
You could do something like this. My example will submit 10 instances of Field back to your controller.
View:
#using (Html.BeginForm())
{
<div>
#for(int i = 0; i<10; i++)
{
<div>#Html.TextBox("items[" + i + "].Field", "", new { id = "items[" + i + "].Field", placeholder = "Enter Text..." })</div>
#Html.Hidden("items.Index", i)
}
</div>
<input type="submit" value="Submit" />
}
Class:
public class MyClass
{
public string Field {get;set;}
}
Controller Method:
[HttpPost]
public ActionResult ActionName(List<MyClass> items)
{
//...do stuff
}
Obviously you could also add your checkbox into the model and form too in order to submit many of those.
Why you want to be the same name for the fields, each field has proper name
public class NewForm
{
public string FirstField { get; set; }
public string Field { get; set; }
public bool Check { get; set; }
}
VIEW
#Html.TextAreaFor(model => model.FirstField)
#Html.TextAreaFor(model => model.Field)
#Html.CheckBoxFor(model => model.Check)
ee
[HttpPost]
public ActionResult Edit(NewForm model)
{
Response.Write(model.FirstField);
Response.Write(model.Field);
Response.Write(model.Check);
}
No, it isn't. In the model you defined, you have created two different arrays: The first property Field is an array of strings and the second property Check an array of bools. putting the [] after a type indicates an array.
If you have an unknown number of what I'll call "mini forms" and the number of these is decided by the user via the UI, then you should create a view model to represent this mini form, and a container view model to house it and any other properties your view will need.
For example:
public class MiniFormViewModel
{
public string MyInput { get; set; }
public bool MyCheck { get; set; }
}
then in your container view model:
public class ContainerViewModel
{
public IEnumerable<MiniFormViewModel> MiniForms { get; set; }
//Any other properties you need on the view that will occur a single time
}
Now, in the JS you'll need to add some manipulation in order to do this:
function getViewModel() {
//You'll have to decide how you want to get the values of the mini form's fields. Perhaps you might even have a function to supply these values. Up to you.
return {
MiniForms: [{
MyInput: '', //value from the first "mini form' string field
Mycheck: false //value from the first "mini-form" bool field
},
{
MyInput: '', //value from the second"mini form' string field
Mycheck: false //value from the second"mini-form" bool field
}
]
}
}
Then you'll need to post this back to the server. I'll demonstrate how to do this via the built in JS Fetch function:
fetch(yourUrlForTheControllerAction,
{
method: 'post',
body: JSON.stringify(getViewModel()),
headers: {
'content-type': 'application/json; charset=UTF-8'
}
})
And then blammo, you should be good to go. I excluded the part of dynamically adding the mini form fields because it sounds like you have a solution for that already.
In my MVC-project I have different custom validation-attributes. One of them is to check the value of a property against the value of another property.
As stated in many articles, I add something like
result.ValidationParameters.Add("otherproperty", _otherPropertyHtml);
result.ValidationParameters.Add("comparetype", _compareType);
result.ValidationParameters.Add("equalitytype", _equalityType);
to the returning ModelClientValidationRule object.
My problem now is, that - if my property to check - is encapsulated in another object, validation will not work.
If I create something like
#Html.TextBoxFor(m => m.ValueOne)
#Html.TextBoxFor(m => m.ValueTwo)
validation will work fine as it renders
data-val-otherproperty="ValueTwo"
My problem is for the following
#Html.TextBoxFor(m => m.IntermediateObject.ValueOne)
#Html.TextBoxFor(m => m.IntermediateObject.ValueTwo)
This will render two textboxes with names IntermediateObject_ValueOne and IntermediateObject.ValueTwo. But still data-val-otherproperty="ValueOne" for the first textbox.
How can it be achieved, that data-val-otherproperty has always the correct name of the other property?
My thoughts are something like HtmlHelper<>.NameFor(m => ...) or something that uses reflection?
Update 1 - Added code as requested by comments
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class, AllowMultiple = false)]
public class CustomCompareToOther : ValidationAttribute, IClientValidatable
{
// private backing-field
private readonly string _otherPropertyName;
// constructor
public OemCompareToOther(string otherPropertyName)
{
_otherPropertyName = otherPropertyName;
}
// implementation of IClientValidatable
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
var result = new ModelClientValidationRule
{
ErrorMessage = FormatErrorMessage(metadata.DisplayName),
ValidationType = "customcomparetoother"
};
// add the property-name so it is known when rendered for client-side validation
result.ValidationParameters.Add("otherproperty", _otherPropertyHtml); // here I would need IntermediateObject.ValueTwo instead of only ValueTwo
yield return result;
}
}
Usage at model-level would be
public class MyModel
{
[CustomCompareToOther("ValueOTwo", CompareType.NotEqual, PropertyType.String)]
public string ValueOne { get; set; }
[CustomCompareToOther("ValueTwo", CompareType.NotEqual, PropertyType.String)]
public string ValueTwo { get; set; }
}
And what I will put into my View would be something like
public class ViewModel
{
public MyModel IntermediateObject { get; set; }
}
used e.g. return View(new ViewModel()).
So, in the rendered HTML I would have an input
<input type="text" name="IntermediateObject_ValueOne" id="IntermediateObject.ValueOne" data-val-customcomparetoother-otherpropertyname="ValueTwo" />
<input type="text" name="IntermediateObject_ValueTwo" id="IntermediateObject.ValueTwo" data-val-customcomparetoother-otherpropertyname="ValueOne" />
but I need
<input type="text" name="IntermediateObject_ValueOne" id="IntermediateObject.ValueOne" data-val-customcomparetoother-otherpropertyname="IntermediateObject.ValueTwo" />
<input type="text" name="IntermediateObject_ValueTwo" id="IntermediateObject.ValueTwo" data-val-customcomparetoother-otherpropertyname="IntermediateObject.ValueOne" />
in the html so javascript-validation can fetch the other property correctly.
You can use the [Compare("PropertyName")] Data Annotation.
Example in your View Model:
[Display(Name = "New Password")]
[DataType(DataType.Password)]
public string NewPassword { get; set; }
[Display(Name = "Confirm Password")]
[DataType(DataType.Password)]
[Compare("NewPassword")]
public string PasswordConfirmation { get; set; }
Just remember to add the System.ComponentModel.DataAnnotations namespace to your using statements
I am relying heavily on EditorTemplates in my application, but I've run into a problem which I can not seem to solve, without not moving away from EditorTemplates for drop down lists.
Consider this (View)Model:
public class CreateStudentViewModel
{
public DropDownList StudentTypes { get; set; }
public CreateStudent Command { get; set; }
}
public class DropDownList {
public string SelectedValue { get; set; }
public IList<SelectListItem> Items { get; set; }
}
public class CreateStudent {
public string Name { get; set; }
public int StudentTypeId { get; set; }
}
I use this to provide a way for the frontend user to set the student type, this is done with the following EditorTemplate:
#model DropDownList
<div class="form-group#(Html.ValidationErrorFor(m => m.SelectedValue, " has-error"))">
#Html.LabelFor(m => m)
#Html.DropDownListFor(m => m.SelectedValue, Model.Items)
#Html.ValidationMessageFor(m => m.SelectedValue, null)
</div>
And used within my view:
#Html.EditorFor(m => m.StudentTypes)
Now this EditorTemplate is binding to the StudentTypes.SelectedValue on DropDownList, which is good in some cases - but I need to bind this to my Model.Command.StudentTypeId here.
I know I can move all this code directly to the view and directly bind it, instead of having it inside a EditorTemplate, but I will try my best to avoid this.
Ideally I am thinking of extending the EditorFor to provide a way like:
#Html.EditorFor(m => m.StudentTypes, new { selectedValue = Model.Command.StudentTypeId });
But I can not seem to translate this to something like:
#Html.DropDownList(#ViewBag.selectedValue.ToString(), Model.Items);
As this just places the value (int) as the field name. Any suggestions is welcome! :-)
Your chief problem here is encapsulating your drop down list in a class in order to rely on the C# type editor template convention. Instead, just use your model directly and use UIHint to tell Razor to use a particular template. Here's a simplified version of what I use:
View Model
[UIHint("Choice")]
public int SelectedFoo { get; set; }
public IEnumerable<SelectListItem> FooChoices { get; set; }
Views\Shared\EditorTemplates\Choice.cshtml
#{
var choices = ViewData["choices"] as IEnumerable<SelectListItem> ?? new List<SelectListItem>();
if (typeof(System.Collections.IEnumerable).IsAssignableFrom(ViewData.ModelMetadata.ModelType) && ViewData.ModelMetadata.ModelType != typeof(string))
{
#Html.ListBox("", choices)
}
else
{
#Html.DropDownList("", choices)
}
}
View
#Html.EditorFor(m => m.SelectedFoo, new { choices = Model.FooChoices })
In case it's not obvious, the conditional in the editor template determines if the property is a value or list type, and either uses a drop down list control or listbox control, respectively.