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
Related
Prefix: I don't like this approach, I think that the Bind attribute is clunky. That said, I have a complex model. It is in an ASP.NET MVC razor view that uses a partial view.
Ideally I would like to just have the partial view accept a DTO that has only the fields that I would like to bind. For reasons I can't control I don't have that option.
However I have the ability to modify the controller. So, I am trying to use the Bind attribute to only allow the properties of the child that I would like to bind.
Here are the parent and child model classes:
public class Parent
{
public Menu FullMenu { get; set; }
public KidDTO SubDTO { get; set; }
}
public class KidDTO
{
public string Name { get; set;}
public string Date { get; set;}
public string Addr { get; set;}
}
Razor parent view (again I would prefer to only pass the smaller DTO that will be used... but I don't have that option):
#model Core.ViewModels.Parent
//Other stuff here
#{ Html.RenderPartial("_CreateEdit", Model);}
Here's the child view:
#model Core.ViewModels.Parent
#using (Ajax.BeginForm("CreateEdit", "Child", new AjaxOptions { HttpMethod = "POST"}))
{
<div class="form-horizontal">
<hr />
<div class="form-group row">
<div class="col-md-10">
#Html.EditorFor(model => model.SubDTO.Name)
</div>
</div>
<input type="button" value="Submit" />
</div>
}
Controller:
[HttpPost]
public async Task<ActionResult> CreateEdit([Bind(Include="SubDTO.Name")] Parent model)
{
// check if its Null or Not
var IsItNull = model.SubDTO.Name; //<--model.SubDTO.Name is always Null. ?
return RedirectToAction($"{ControllerEntity}Manager");
}
So, in short If I Bind to the SubDTO by iteslf it has values, but that then allows for binding to the other attributes of the SubModel, which is what I'm trying to avoid?
I was Not able to figure how to get the Include to work with Child Models properties. I could only bind the entire child or nothing.. However I was get it to work using the [Bind(Prefix="")] in the controller like so.
Controller.
[HttpPost]
public async Task<ActionResult> CreateEdit([Bind(Prefix="SubDTO.Name")] string childName)
{
// check if its Null or Not
var IsItNull = childName //<--UGLY but Appears to work, This is not Ideal.
return RedirectToAction($"{ControllerEntity}Manager");
}
Like the comments above alludes, I do not consider this to be the best solution for this common situation, but based on my restrictions it currently the only option that I have available. This can get very very ugly with a lot of properties.
Try changing your model to explicitly set what you can bind to at the class level.
public class Parent
{
public Menu FullMenu { get; set; }
public KidDTO SubDTO { get; set; }
}
[Bind(Include = "Name")]
public class KidDTO
{
public string Name { get; set;}
public string Date { get; set;}
public string Addr { get; set;}
}
Controller
[HttpPost]
public async Task<ActionResult> CreateEdit(Parent model)
{
// check if its Null or Not
var IsItNull = model.SubDTO.Name;
return RedirectToAction($"{ControllerEntity}Manager");
}
Form Collection Post
[HttpPost]
public async Task<ActionResult> CreateEdit(FormCollection collection)
{
var subDTO = new KidDTO()
{
Name = collection["SubDTO.Name"]
};
// check if its Null or Not
var IsItNull = subDTO.Name;
return RedirectToAction($"{ControllerEntity}Manager");
}
cshtml change type button to submit
<input type="submit" value="Submit" />
To keep things simple and clean, try creating a separate model just with the Name property. The drawback using explicit [Bind] is that you can now only bind to the Name using the default Model binder so why not just create a new ViewModel to begin with?
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.
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.
In my Mvc project I have this model:
namespace CameraWebApp.Models
public class Images
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ImageId { get; set; }
[Required(ErrorMessage="Please enter your first name")]
public string SubmitterFirstName { get; set; }
[Required(ErrorMessage = "Please enter your surname name")]
public string SubmitterLastName { get; set; }
[ExistingFileName]
public string NameOfImage { get; set; }
[StringLength(140, ErrorMessage="Please reduce the length of your description to below 140 characters")]
[DataType(DataType.MultilineText)]
public string DescriptionOfImage { get; set; }
public string ImagePath { get; set; }
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public DateTime DateAdded { get; set; }
}
As you can see the NameOfImage property has the attribute [ExistingFileName] which is a custom validator, the code for this validator is below:
//Overiding isValid to make a custom Validator
protected override System.ComponentModel.DataAnnotations.ValidationResult IsValid(object value, System.ComponentModel.DataAnnotations.ValidationContext validationContext)
{
if (value!=null)
{
string fileName = value.ToString();
if (FileExists(fileName))
{
//If the file exists use default error message or the one passed in if there is one
return new ValidationResult(ExistingImageErrorMessage ?? defaultExistingImage);
}
else
{
return ValidationResult.Success;
}
}
else
{
//If theres no value passed in then use error message or default if none is passed
return new ValidationResult(ErrorMessage ?? DefaultErrorMessage);
}
}
bool FileExists(string fileName)
{
bool exists = false;
//A list is passed all the images
List<Images> images = cameraRepo.getAllImages().ToList();
//Loops through every image checking if the name already exists
foreach (var image in images)
{
if (image.NameOfImage==fileName)
{
exists = true;
break;
}
}
return exists;
}
Each of the previous properties are being validated Client Side in the code below:
#using (Html.BeginForm())
{
<div id="errorMessages">
#Html.ValidationSummary(false)
</div>
<label>base64 image:</label>
<input id="imageToForm" type="text" name="imgEncoded"/>
<label>First Name</label>
#Html.EditorFor(model => model.SubmitterFirstName)
<label>Last Name</label>
#Html.EditorFor(model => model.SubmitterLastName)
<label>Name of Image</label>
#Html.EditorFor(model => model.NameOfImage)
<label>Image Description</label>
#Html.EditorFor(model => model.DescriptionOfImage)
<input type=button id="button"value=" Camera button"/>
<input type="submit" value="Click this when your happy with your photo"/>
}
</div>
#Html.ActionLink("gfd!!", "DisplayLatest")
<script src="#Url.Content("~/Scripts/LiveVideoCapture.js")" type="text/javascript"></script>
All validation works Client side except my Custom validation [ExisitingFileName] and I have no idea why? Does anyone know why this might be?
Thanks in advance!
Since it is a custom validation, c# mvc cannot generate a client-side validation: you'll have to implement your own custom client-side validation for this field (using Javascript). In it you may want to use AJAX to call a server method to check if filename already exists.
You can also try to use remote validation, which seems to be simpler:
http://msdn.microsoft.com/en-us/library/ff398048(VS.100).aspx
When validating on the client side, you need to implement IClientValidateable. This requires you to write client side validation code (javascript) and server side validation code (C#)
http://forums.asp.net/t/1850838.aspx/1
This post is also helpful
http://odetocode.com/Blogs/scott/archive/2011/02/22/custom-data-annotation-validator-part-ii-client-code.aspx
I have been scouring the internet trying to find a way to accomodate dashes from my form elements into the default model binding behavior of ASP.NET's Controllers in MVC 2, 3, or even 4.
As a front-end developer, I prefer dashes in my CSS over camelCase or underscores. In my markup, what I want to be able to do to is something like this:
<input type="text" name="first-name" class="required" />
<input type="text" name="last-name" class="required" />
In the controller, I would be passing in a C# object that would look like this:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
//etc...
}
Is there some way to extend the Controller class to accommodate this through some regex or other behavior? I hate the fact that I have to do something like this:
<input type="text" name="person.firstname" class="required" />
or even this:
<input type="text" name="isPersonAttending" class="required" />
Thoughts?
You could always create your own model binder.
Here's an example that implements a binder that supports adding Aliases to model properties:
http://ole.michelsen.dk/blog/bind-a-model-property-to-a-different-named-query-string-field/
And with it do something like:
[ModelBinder(typeof(AliasModelBinder))]
public class Person
{
[BindAlias("first-name")]
public string FirstName { get; set; }
[BindAlias("last-name")]
public string LastName { get; set; }
//etc...
}
EDIT:
This implementation, as the blogger says, is based on Andras' answer on the following SO question:
Asp.Net MVC 2 - Bind a model's property to a different named value
By creating a custom form value provider you could solve this problem easily. The other advantage is you can avoid polluting all the model properties by decorating custom attributes.
Custom Form Value Provider
public class DashFormValueProvider : NameValueCollectionValueProvider
{
public DashFormValueProvider(ControllerContext controllerContext)
: base(controllerContext.HttpContext.Request.Form,
controllerContext.HttpContext.Request.Unvalidated().Form,
CultureInfo.CurrentCulture)
{
}
public override bool ContainsPrefix(string prefix)
{
return base.ContainsPrefix(GetModifiedPrefix(prefix));
}
public override ValueProviderResult GetValue(string key)
{
return base.GetValue(GetModifiedPrefix(key));
}
public override ValueProviderResult GetValue(string key, bool skipValidation)
{
return base.GetValue(GetModifiedPrefix(key), skipValidation);
}
// this will convert the key "FirstName" to "first-name".
private string GetModifiedPrefix(string prefix)
{
return Regex.Replace(prefix, "([a-z](?=[A-Z])|[A-Z](?=[A-Z][a-z]))", "$1-").ToLower();
}
}
Value Provider Factory
public class DashFormValueProviderFactory : ValueProviderFactory
{
public override IValueProvider GetValueProvider(ControllerContext controllerContext)
{
if (controllerContext == null)
{
throw new ArgumentNullException("controllerContext");
}
return new DashFormValueProvider(controllerContext);
}
}
Global.asax.cs
ValueProviderFactories.Factories.Add(new DashFormValueProviderFactory());
For readers looking for an ASP.NET Core solution, try one of the [From...] attributes. For example:
public class Person
{
[FromForm(Name = "first-name")]
public string FirstName { get; set; }
[FromForm(Name = "last-name")]
public string LastName { get; set; }
//etc...
}
The [From...] attributes are briefly described at https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1#sources.