ExpressiveAnnotations RequiredIf Client Side Validation - asp.net-mvc

Installed EA for trying to have a parameter be required if there is nothing in the database, IE. this is the first time someone is creating something.
However, the RequiredIf never fires for client side validation, even though when the model gets into the partial view, the BindingExists bool is set to false and the Xml value is still null.
Model:
public class AddTestStepXmlParameterModel
{
public ParameterTypeEnum ParameterType { get; set; }
public string ParameterName { get; set; }
public string Description { get; set; }
[RequiredIf("BindingExists == false", ErrorMessage = "An XML File is required: Please Try again")]
[FileExtensions(Extensions = "xml", ErrorMessage = "Specify an XML file.")]
public HttpPostedFileBase XmlValue { get; set; }
public bool BindingExists { get; set; }
}
Global.asax:
protected void Application_Start()
{
ModelValidatorProviders.Providers.Remove(ModelValidatorProviders.Providers.FirstOrDefault(x => x is DataAnnotationsModelValidatorProvider));
ModelValidatorProviders.Providers.Add(new ExpressiveAnnotationsModelValidatorProvider());
}
Scripts in View:
<script src="~/Scripts/jquery-3.1.0.js"></script>
<script src="~/Scripts/jquery.validate.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.js"></script>
<script src="~/Scripts/expressive.annotations.validate.js"></script>
Partial View Call:
#Html.Partial("AddParameters", Model.AddTestStepModel.AddTestStepParametersModel)
Partial View:
#Html.HiddenFor(m => m.AddTestStepXmlParameterModels[k].BindingExists, new {#Value = Model.AddTestStepXmlParameterModels[k].BindingExists})
#Html.TextBoxFor(m => m.AddTestStepXmlParameterModels[k].XmlValue, new {type = "file", #class = "btn btn-default btn-file", style = "color:transparent", onchange = "this.style.color = 'black'"})
#Html.ValidationMessageFor(m => m.AddTestStepXmlParameterModels[k].XmlValue)
When using just a normal "Required", the Xml Value client side fires off fine, however using the RequiredIf fails to do any validation. I've followed the isntallation steps with the Global.asax

Your HTML is not being generated as expected. Due to that, serialized form isn't properly understood by the model binder and cannot be correctly deserialized.
Instead of partial view use the editor template:
move the AddParameters.cshtml template under Views...\EditorTemplates\ directory,
change #Html.Partial(... invocation into #Html.EditorFor(model => model.AddTestStepModel.AddTestStepParametersModel, "AddParameters").
When you compare the output HTML for these two invocations you'll see
short input fields names for the partial view: AddTestStepXmlParameterModels[0].XmlValue,
in contrast to editor template rendering full names, used by the binder to map respective fields: AddTestStepModel.AddTestStepParametersModel.AddTestStepXmlParameterModels[0].XmlValue.

Related

How to persist an object in view model on post back [duplicate]

I have a ViewModel that has a complex object as one of its members. The complex object has 4 properties (all strings). I'm trying to create a re-usable partial view where I can pass in the complex object and have it generate the html with html helpers for its properties. That's all working great. However, when I submit the form, the model binder isn't mapping the values back to the ViewModel's member so I don't get anything back on the server side. How can I read the values a user types into the html helpers for the complex object.
ViewModel
public class MyViewModel
{
public string SomeProperty { get; set; }
public MyComplexModel ComplexModel { get; set; }
}
MyComplexModel
public class MyComplexModel
{
public int id { get; set; }
public string Name { get; set; }
public string Address { get; set; }
....
}
Controller
public class MyController : Controller
{
public ActionResult Index()
{
MyViewModel model = new MyViewModel();
model.ComplexModel = new MyComplexModel();
model.ComplexModel.id = 15;
return View(model);
}
[HttpPost]
public ActionResult Index(MyViewModel model)
{
// model here never has my nested model populated in the partial view
return View(model);
}
}
View
#using(Html.BeginForm("Index", "MyController", FormMethod.Post))
{
....
#Html.Partial("MyPartialView", Model.ComplexModel)
}
Partial View
#model my.path.to.namespace.MyComplexModel
#Html.TextBoxFor(m => m.Name)
...
how can I bind this data on form submission so that the parent model contains the data entered on the web form from the partial view?
thanks
EDIT: I've figured out that I need to prepend "ComplexModel." to all of my control's names in the partial view (textboxes) so that it maps to the nested object, but I can't pass the ViewModel type to the partial view to get that extra layer because it needs to be generic to accept several ViewModel types. I could just rewrite the name attribute with javascript, but that seems overly ghetto to me. How else can I do this?
EDIT 2: I can statically set the name attribute with new { Name="ComplexModel.Name" } so I think I'm in business unless someone has a better method?
You can pass the prefix to the partial using
#Html.Partial("MyPartialView", Model.ComplexModel,
new ViewDataDictionary { TemplateInfo = new TemplateInfo { HtmlFieldPrefix = "ComplexModel" }})
which will perpend the prefix to you controls name attribute so that <input name="Name" ../> will become <input name="ComplexModel.Name" ../> and correctly bind to typeof MyViewModel on post back
Edit
To make it a little easier, you can encapsulate this in a html helper
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
string name = ExpressionHelper.GetExpressionText(expression);
object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo
{
HtmlFieldPrefix = string.IsNullOrEmpty(helper.ViewData.TemplateInfo.HtmlFieldPrefix) ?
name : $"{helper.ViewData.TemplateInfo.HtmlFieldPrefix}.{name}"
}
};
return helper.Partial(partialViewName, model, viewData);
}
and use it as
#Html.PartialFor(m => m.ComplexModel, "MyPartialView")
If you use tag helpers, the partial tag helper accepts a for attribute, which does what you expect.
<partial name="MyPartialView" for="ComplexModel" />
Using the for attribute, rather than the typical model attribute, will cause all of the form fields within the partial to be named with the ComplexModel. prefix.
You can try passing the ViewModel to the partial.
#model my.path.to.namespace.MyViewModel
#Html.TextBoxFor(m => m.ComplexModel.Name)
Edit
You can create a base model and push the complex model in there and pass the based model to the partial.
public class MyViewModel :BaseModel
{
public string SomeProperty { get; set; }
}
public class MyViewModel2 :BaseModel
{
public string SomeProperty2 { get; set; }
}
public class BaseModel
{
public MyComplexModel ComplexModel { get; set; }
}
public class MyComplexModel
{
public int id { get; set; }
public string Name { get; set; }
...
}
Then your partial will be like below :
#model my.path.to.namespace.BaseModel
#Html.TextBoxFor(m => m.ComplexModel.Name)
If this is not an acceptable solution, you may have to think in terms of overriding the model binder. You can read about that here.
I came across the same situation and with the help of such informative posts changed my partial code to have prefix on generated in input elements generated by partial view
I have used Html.partial helper giving partialview name and object of ModelType and an instance of ViewDataDictionary object with Html Field Prefix to constructor of Html.partial.
This results in GET request of "xyz url" of "Main view" and rendering partial view inside it with input elements generated with prefix e.g. earlier Name="Title" now becomes Name="MySubType.Title" in respective HTML element and same for rest of the form input elements.
The problem occurred when POST request is made to "xyz url", expecting the Form which is filled in gets saved in to my database. But the MVC Modelbinder didn't bind my POSTed model data with form values filled in and also ModelState is also lost. The model in viewdata was also coming to null.
Finally I tried to update model data in Posted form using TryUppdateModel method which takes model instance and html prefix which was passed earlier to partial view,and can see now model is bound with values and model state is also present.
Please let me know if this approach is fine or bit diversified!

ASP.Net Filter Dropdown list items from multiple tables

Summary:
I'm trying to use two DropDownList controls to filter the data that is currently being sorted and displayed in a view.
What we are going to learn
Creating the ViewController for One to Many and Many-to-Many relationships that could Filter the data using DropDownList
Possible Causes
If my DropdownList code is not terrible wrong, The ViewModel I'm using to display the data has no proper support for the DropDownList items.
In other words, the RazorView and my ViewModels are not compatible for what I'm trying to achieve. If I try to change my ViewModel or RazorView, I get an eldless loop of errors for my existing code.
OR The Linq Query needs an expert attention
Here is FilterViewModel.cs
public IEnumerable <App> Apps { get; set; }
public IEnumerable <Language> Languages { get; set; }
public IEnumerable <Platform> Platforms { get; set; }
public IEnumerable <AgeGroup> AgeGroups { get; set; }
public IEnumerable <ProductCode> ProductCodes { get; set; }
Here is AppsController.cs
public ActionResult FilterApps(App app)
{
var apps = _context.Apps.ToList();
var languages = _context.Languages.ToList();
var productCodes = _context.ProductCodes.ToList();
var platforms = _context.Platforms.ToList();
var ageGroups = _context.AgeGroups.ToList();
var viewModel = new FilterViewModel
{
AgeGroups = ageGroups,
Languages = languages,
Platforms = platforms,
ProductCodes = productCodes,
Apps = apps
.Where(a => a.LanguageId == app.LanguageId && a.PlatformId == app.PlatformId)
// I also tried all possible combinations :(a.Lanage.Id etc)
};
return View("FilterApps", viewModel);
}
Here is the FilterApps.cshtml
#model Marketing.ViewModels.FilterViewModel
<h2>FilterApps</h2>
#using (Html.BeginForm("FilterApps", "Apps", FormMethod.Post, new { enctype = "multipart/form-data" }))
{
<div class="form-group">
#Html.DropDownListFor( m => m.Languages,
new SelectList(Model.Languages, "Id", "Name"),"Select Language",
new { #class = "form-control", #id = "dropDown" })
#Html.DropDownListFor(m => m.Platforms,
new SelectList(Model.Platforms, "Id", "Name"), "Select Platform",
new { #onchange = "this.form.submit();",
#class = "form-control", #id = "dropDown" })
</div>
}
//The existing code below is working fine so far.
#foreach (var group in Model.AgeGroups)
{
<h4>#group.Name</h4>
#foreach (var app in Model.Apps.OrderBy(a => a.AppOrder))
{
if (app.AgeGroupId == group.Id)
{
#app.ProductCode.Name
#app.Name
#app.Platform.Name
}
}
}
Probably unnecessary but I hope the additional information will help.
Additional Information
The App.cs is referencing all other tables e.g.
public Language Language { get; set; }
public int LanguageId { get; set; }
public Platform Platform { get; set; }
public int PlatformId { get; set; }
and so on...
What I have already tried
Several breakpoints and Logs to track the data, I also tried to use the following but it ruins my existing sorting and grouping.
public App App { get; set; } //Instead of the IEnumerable<App>
There are multiple issues with your code.
First you cannot bind a <select> element to a collection of complex objects. A <select> posts back the value of its selected option (which will be an int assuming the Id property of Language is int).
Next the view in the model is FilterViewModel (and your generating form controls with name attributes based on those properties), but your posting back to a different model (App) which does not contain those properties so nothing would bind anyway.
Your adding a null label option ("Select Language") and if that were selected, it would post a null value which would cause your query to fail.
There are also some bad practices which I have noted below.
Your view model should be
public class AppsFilterVM
{
public int? Language { get; set; }
public int? Platform { get; set; }
public IEnumerable<SelectListItem> LanguageOptions { get; set; }
public IEnumerable<SelectListItem> PlatformOptions { get; set; }
...
public IEnumerable <App> Apps { get; set; }
}
Its not clear what AgeGroups and ProductCodes are for so I have omitted them in the code above, and from your comments, I have assumed that the user can filter by either Language or Platform or both
The controller code would be
public ActionResult FilterApps(AppsFilterVM model)
{
var apps = _context.Apps;
if (model.Language.HasValue)
{
apps = apps.Where(x => x.LanguageId == model.Language.Value);
}
if (model.Platform.HasValue)
{
apps = apps.Where(x => x.PlatformId == model.Platform.Value);
}
model.Apps = apps;
ConfigureViewModel(model);
return View(model);
}
private void ConfigureViewModel(AppsFilterVM model)
{
// populate the selectlists
var languages = _context.Languages;
var platforms = _context.Platforms
model.LanguageOptions = new SelectList(languages, "Id", "Name");
model.PlatformOptions = new SelectList(platforms , "Id", "Name");
}
Then in the view (note its making a GET, not a POST)
#model.AppsFilterVM
....
#using (Html.BeginForm("FilterApps", "Apps", FormMethod.Get))
{
#Html.LabelFor(m => m.Language)
#Html.DropdownListFor(m => m.Language, Model.LanguageOptions, "No filter")
#Html.ValidationMessageFor(m => m.Language)
#Html.LabelFor(m => m.Platform)
#Html.DropdownListFor(m => m.Platform, Model.PlatformOptions, "No filter")
#Html.ValidationMessageFor(m => m.Platform)
<input type="submit" value="Filter" />
}
#foreach (var group in Model.AgeGroups)
{
....
There a a few other thing you should not be doing. Your giving both <select> elements the same id attribute which is invalid html (the DropDownListFor() method already generates a unique id based on the property name).
You should not submit a form based on the change event of a <select> Not only is it unexpected behavior, if a user uses the keyboard to navigate through options (e.g. using the arrow keys, or typing a character to go to the first option starting with that letter, then the form will be immediately submitted. In addition, the user might select an option from the 2nd dropdownlist first, which would immediately post before they have a chance to select the option in the first one. Allow the user to make their selections, check them, and then submit the form when they choose to.
Your view should not contain linq queries, and your grouping and ordering should be done in the controller before you pass the model to the view. Your Apps property should in fact be a view model containing a property for the group name, and a collection property for the Apps, (similar to the view models in your previous question) so that the view is simply
#foreach(var group in Model.AgeGroups)
{
#group.Name
foreach (var app in group.Apps)
{
#app.ProductCode
#app.Name
#app.Platform
}
}
You should also consider using ajax to submit your form, which would call separate server method that returns a partial view of just the Apps, and update the DOM in the success callback, which would improve performance. For an example, refer Rendering partial view on button click in ASP.NET MVC.

Checkbox adds extra "false" value per selected one

I have this piece of code in my View which belongs to a form
<div class="col-md-10">
#foreach (var l in leads)
{
#: #Html.CheckBox("cbLead", false, new { #value = #l.Id }) #Html.TextBox("worth", "") - #Html.Label(l.Name)
}
</div>
And this is the form with I handle the post:
[HttpPost]
public ActionResult Update(string[] cbLead, double[] worth)
{
// code
}
I have 24 checkboxes, but for each checkbox selected I receive 2 values in the Update method. So for example if I select 3 out of that 24 checkboxes, I receive 27 values in the string[] cblead.
Example with 24 checkboxes:
And this is what I get in the method:
So I receive the value checked and an added false after. Any tips?
That's because the Html.CheckBox helper generates an additional hidden field with the same name and the value false. The reason for that is because if the checkbox is not checked, then no value will be sent to the server and thus the model binder will fail to properly bind to a boolean property on your model. Also notice that the Html.CheckBox helper expects that you are working with boolean values on your models. Your syntax here is incorrect:
#Html.CheckBox("cbLead", false, new { #value = #l.Id })
You seem to be trying to manually set the value attribute of the checkbox (which should not be done when using helpers) to the Id property of your model which I suppose is not boolean but rather a Guid as can be seen from the screenshot.
This is by design and is expected behavior. If you do not want this behavior that you could write your own custom helper or use plain HTML instead.
I suspect that what you need to receive on the server is the list of IDs along with a boolean value corresponding to whether the element was checked or not. For this purpose I suggest you writing the following view model:
public class MyViewModel
{
public IList<LeadViewModel> Leads { get; set; }
}
public class LeadViewModel
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Worth { get; set; }
public bool IsChecked { get; set; }
}
and then:
#for (var i = 0; i < Model.Leads.Count; i++)
{
Html.CheckBoxFor(x => x.Leads[i].IsChecked)
Html.HiddenFor(x => x.Leads[i].Id)
Html.TextBoxFor(x => x.Leads[i].Worth) -
Html.LabelFor(x => x.Leads[i].Name)
}

Fluent Validation in MVC: specify RuleSet for Client-Side validation

In my ASP.NET MVC 4 project I have validator for one of my view models, that contain rules definition for RuleSets. Edit ruleset used in Post action, when all client validation passed. Url and Email rule sets rules used in Edit ruleset (you can see it below) and in special ajax actions that validate only Email and only Url accordingly.
My problem is that view doesn't know that it should use Edit rule set for client html attributes generation, and use default rule set, which is empty. How can I tell view to use Edit rule set for input attributes generation?
Model:
public class ShopInfoViewModel
{
public long ShopId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string Description { get; set; }
public string Email { get; set; }
}
Validator:
public class ShopInfoViewModelValidator : AbstractValidator<ShopInfoViewModel>
{
public ShopInfoViewModelValidator()
{
var shopManagementService = ServiceLocator.Instance.GetService<IShopService>();
RuleSet("Edit", () =>
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Enter name.")
.Length(0, 255).WithMessage("Name length should not exceed 255 chars.");
RuleFor(x => x.Description)
.NotEmpty().WithMessage("Enter name.")
.Length(0, 10000).WithMessage("Name length should not exceed 10000 chars.");
ApplyUrlRule(shopManagementService);
ApplyEmailRule(shopManagementService);
});
RuleSet("Url", () => ApplyUrlRule(shopManagementService));
RuleSet("Email", () => ApplyEmailRule(shopManagementService));
}
private void ApplyUrlRule(IShopService shopService)
{
RuleFor(x => x.Url)
.NotEmpty().WithMessage("Enter url.")
.Length(4, 30).WithMessage("Length between 4 and 30 chars.")
.Matches(#"[a-z\-\d]").WithMessage("Incorrect format.")
.Must((model, url) => shopService.Available(url, model.ShopId)).WithMessage("Shop with this url already exists.");
}
private void ApplyEmailRule(IShopService shopService)
{
// similar to url rule: not empty, length, regex and must check for unique
}
}
Validation action example:
public ActionResult ValidateShopInfoUrl([CustomizeValidator(RuleSet = "Url")]
ShopInfoViewModel infoViewModel)
{
return Validation(ModelState);
}
Get and Post actions for ShopInfoViewModel:
[HttpGet]
public ActionResult ShopInfo()
{
var viewModel = OwnedShop.ToViewModel();
return PartialView("_ShopInfo", viewModel);
}
[HttpPost]
public ActionResult ShopInfo(CustomizeValidator(RuleSet = "Edit")]ShopInfoViewModel infoViewModel)
{
var success = false;
if (ModelState.IsValid)
{
// save logic goes here
}
}
View contains next code:
#{
Html.EnableClientValidation(true);
Html.EnableUnobtrusiveJavaScript(true);
}
<form class="master-form" action="#Url.RouteUrl(ManagementRoutes.ShopInfo)" method="POST" id="masterforminfo">
#Html.TextBoxFor(x => x.Name)
#Html.TextBoxFor(x => x.Url, new { validationUrl = Url.RouteUrl(ManagementRoutes.ValidateShopInfoUrl) })
#Html.TextAreaFor(x => x.Description)
#Html.TextBoxFor(x => x.Email, new { validationUrl = Url.RouteUrl(ManagementRoutes.ValidateShopInfoEmail) })
<input type="submit" name="asdfasfd" value="Сохранить" style="display: none">
</form>
Result html input (without any client validation attributes):
<input name="Name" type="text" value="Super Shop"/>
After digging in FluentValidation sources I found solution. To tell view that you want to use specific ruleset, decorate your action, that returns view, with RuleSetForClientSideMessagesAttribute:
[HttpGet]
[RuleSetForClientSideMessages("Edit")]
public ActionResult ShopInfo()
{
var viewModel = OwnedShop.ToViewModel();
return PartialView("_ShopInfo", viewModel);
}
If you need to specify more than one ruleset — use another constructor overload and separate rulesets with commas:
[RuleSetForClientSideMessages("Edit", "Email", "Url")]
public ActionResult ShopInfo()
{
var viewModel = OwnedShop.ToViewModel();
return PartialView("_ShopInfo", viewModel);
}
If you need to decide about which ruleset would be used directly in action — you can hack FluentValidation by putting array in HttpContext next way (RuleSetForClientSideMessagesAttribute currently is not designed to be overriden):
public ActionResult ShopInfo(validateOnlyEmail)
{
var emailRuleSet = new[]{"Email"};
var allRuleSet = new[]{"Edit", "Url", "Email"};
var actualRuleSet = validateOnlyEmail ? emailRuleSet : allRuleSet;
HttpContext.Items["_FV_ClientSideRuleSet"] = actualRuleSet;
return PartialView("_ShopInfo", viewModel);
}
Unfortunately, there are no info about this attribute in official documentation.
UPDATE
In newest version we have special extension method for dynamic ruleset setting, that you should use inside your action method or inside OnActionExecuting/OnActionExecuted/OnResultExecuting override methods of controller:
ControllerContext.SetRulesetForClientsideMessages("Edit", "Email");
Or inside custom ActionFilter/ResultFilter:
public class MyFilter: ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
((Controller)context.Controller).ControllerContext.SetRulesetForClientsideMessages("Edit", "Email");
//same syntax for OnActionExecuted/OnResultExecuting
}
}
Adding to this as the library has been updated to account for this situation...
As of 7.4.0, it's possible to dynamically select one or multiple rule sets based on your specific conditions;
ControllerContext.SetRulesetForClientsideMessages("ruleset1", "ruleset2" /*...etc*);
Documentation on this can be found in the latest FluentValidation site:
https://fluentvalidation.net/aspnet#asp-net-mvc-5
Adding the CustomizeValidator attribute to the action will apply the ruleset within the pipeline when the validator is being initialized and the model is being automatically validated.
public ActionResult Save([CustomizeValidator(RuleSet="MyRuleset")] Customer cust) {
// ...
}

ASP.NET MVC - Stop Html.TextBoxFor adding a dot to the Name attribute

I'm fairly new to ASP.NET MVC and I've got a problem with Html.TextBoxFor() - it's giving the rendered input's name attribute a dot.
<%= Html.TextBoxFor(m => m.Box.Name, new { id= "txtName" }) %>
renders the following HTML:
<input type="text" value="" name="Box.Name" id="txtName">
I'm using the jQuery Validation plugin for client-side validation. It can't validate this input because of the dot in Box.Name (causes a javascript error).
My Model looks like this:
public class Box {
[Required(ErrorMessage = "Please enter a name")]
public string Name { get; set; }
public int BoxMaterialID { get; set; }
}
My ViewModel looks like this:
public class BoxViewModel
{
public Box Box { get; set; }
public List<BoxMaterial> BoxMaterial { get; set;}
}
My Controller looks like this:
public ActionResult New(FormCollection postData)
{
Box box = new Box();
try
{
UpdateModel(box, "Box");
boxService.SaveBox(box);
}
catch
{
return View(new BoxViewModel(box));
}
return RedirectToAction("Index", "Boxes");
}
Server-side validation is working like a charm using DataAnnotations on the Model. The only problem I seem to be having is with the client-side validation because of the "." in the name attribute.
Thanks for your help!
The dot added to the name is used by the default model binder when the form is submitted in order to correctly populate the model. As far as validation is concerned you could set it like this:
$("#myForm").validate({
rules: {
'Box.Name': {
required: true
}
},
messages: {
'Box.Name': {
required: 'some error message'
}
}
});
Try specifying the name attribute as well:
<%= Html.TextBoxFor(m => m.Box.Name, new { id= "txtName", name = "txtName" }) %>

Resources