mvc 5 custom validation for variables inside of model object - asp.net-mvc-5

I have searched for this question in multiple places and was unable to find exactly what I am looking for. Let's say I have this MVC Model Structure:
public class Person {
[Required]
public string Name { get; set; }
public int Age { get; set; }
}
public class Workers {
[AgeRequired]
public Person Pilots { get; set; }
public Person Chefs { get; set; }
}
and here would be my cshtml code:
#Model Workers
<div>
<label asp-for="Pilots.Name"></label>
<input asp-for="Pilots.Name"></input>
<span asp-validation-for="Pilots.Name"></span>
</div>
<div>
<label asp-for="Pilots.Age"></label>
<input asp-for="Pilots.Age"></input>
<span asp-validation-for="Pilots.Age"></span>
</div>
<div>
<label asp-for="Chefs.Name"></label>
<input asp-for="Chefs.Name"></input>
<span asp-validation-for="Chefs.Name"></span>
</div>
<div>
<label asp-for="Chefs.Age"></label>
<input asp-for="Chefs.Age"></input>
<span asp-validation-for="Chefs.Age"></span>
</div>
Person is a generic Model class that holds information about Pilots or Chefs. What I want is for my AgeRequired Custom Validation Attribute to make Age required only when referring to Pilots, not Chefs. Would that be possible?
I have it working on the backend side, after the form has been submitted, however I would like this to be on the front end as well. Here is my code for my Attribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public class AgeRequiredAttribute: ValidationAttribute, IClientModelValidator
{
public override bool IsValid(object value)
{
Workers workers = value as Workers;
return workers.Age > 0;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ClientModelValidationContext context)
{
yield return new ModelClientValidationRule("agerequired", "{0} is a required field.");
}
}
}
and here is my javascript code for the front end validation:
/// <reference path="jquery.validate.js" />
/// <reference path="jquery.validate.unobtrusive.js" />
$.validator.addMethod("agerequired",
function (value, element, parameters) {
return value > 0;
});
$.validator.unobtrusive.adapters.add("agerequired", [], function (options) {
options.rules.agerequired= {};
options.messages["agerequired"] = options.message;
});
ClientValidationEnabled and UnobstrusiveJavaScriptEnabled are both set to true.
This custom attribute will work when I have it on the Age field itself, but that makes it required for both Pilots and Chefs. I only want it required for Pilots.
Thank you in advance for your help!

I was actually able to create a work around for those who are interested.
If you put in the data-val-[attribute] that would get generated by MVC directly into your input tag or select tag, followed by the error message you want to throw, it will do the front end validation, and still do the back end validation as MVC will notice that the complex object has information in it. It's not ideal, but probably what I will have to do.
For example:
#Model Workers
<div>
<label asp-for="Pilots.Name"></label>
<input asp-for="Pilots.Name"></input>
<span asp-validation-for="Pilots.Name"></span>
</div>
<div>
<label asp-for="Pilots.Age"></label>
<input asp-for="Pilots.Age" data-val-agerequired="Age is Required for Pilots."></input>
<span asp-validation-for="Pilots.Age"></span>
</div>
<div>
<label asp-for="Chefs.Name"></label>
<input asp-for="Chefs.Name"></input>
<span asp-validation-for="Chefs.Name"></span>
</div>
<div>
<label asp-for="Chefs.Age"></label>
<input asp-for="Chefs.Age"></input>
<span asp-validation-for="Chefs.Age"></span>
</div>
Will work. It's not ideal, but it allows us to keep the MVC Back End validation in the same spot.

Another option is to have person age as optional in a base class.
public int? Age { get; set; }
Inherit Chef and Pilot from Person. In pilot, make the age value non-optional - you'll probably want to make sure they have a minimal age too using Data Annotations
public new int Age { get; set; }
The model is now a list of Persons. The client should be able to work out which is optional and which is not

Related

How to localize error messages in .NET Core 6

I am new to .NET Core 6 and I have realized that several things should be done manually.
One of them is localization. For example, when using a simple action to change the user password, the message "Incorrect password." is returned in the errors collection when the old password mismatch.
I have spent a lot of time trying to localize that simple message so that it is shown in Spanish. I have read a lot of pages telling about this but none works. I think it is because this message is not a DataAnnotation message.
When I used .NET Framework, all of these were made automatically since the resource DLL is always installed by default. It seems that in .NET Core 6 those DLL's are missing, or at least, they are vey hidden.
As an attempt, I added this to Program.cs file:
builder.Services.AddMvc()
.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, options => options.ResourcesPath = "Resources")
.AddDataAnnotationsLocalization();
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.Configure<RequestLocalizationOptions>(options =>
{
var supportedCultures = new[] { new CultureInfo("en"), new CultureInfo("es") };
options.DefaultRequestCulture = new RequestCulture("es");
options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures;
});
app.UseRequestLocalization();
And also added a file "Resouces\ErrorMessages.es.resx" with an entry whose key is PasswordMismatch with the message in Spanish, but no avail.
Any help, please?
Alought I couldn't check the details of your resourcefile,I think there's something wrong with your resourcefile for the name you've shown
Resouces\ErrorMessages.es.resx
I tried in my case as below:
public class TestModel
{
[Required(ErrorMessage = "The Email field is required.")]
[EmailAddress(ErrorMessage = "The Email field is not a valid email address.")]
[Display(Name = "Email")]
public string Email { get; set; }
[Required(ErrorMessage = "The Password field is required.")]
[StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; }
[DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
in Razor View:
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Password" class="control-label"></label>
<input asp-for="Password" class="form-control" />
<span asp-validation-for="Password" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ConfirmPassword" class="control-label"></label>
<input asp-for="ConfirmPassword" class="form-control" />
<span asp-validation-for="ConfirmPassword" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
It works well:
Update:
Since You are not interested in developing a multilingual site,I think the most simlpe solution is Custom an Attribute as below:
public class CusRequiredAttribute: RequiredAttribute
{
public CusRequiredAttribute()
{
//You could wrote the errormessage in your language as well
ErrorMessage = "{0}Required";
}
}
add [CusRequired]attrbute on the target property
for developing a multilingual site
Create an empty class named SharedResources and a resource file as below:
set as below in startup:
services.AddControllersWithViews()
.AddDataAnnotationsLocalization(
options =>
{
options.DataAnnotationLocalizerProvider = (type, factory) =>
factory.Create(typeof(SharedResources));
});
Result:

Posting an array of string

I am trying to post a string array to the post action in an Razor Pages project. For this, I thought about using a hidden <select> tag. The user would enter text into a text box, press a button and I would then add a new option to the <select> then post the whole thing with a submit button. However, after everything is posted, the array property of my model is empty.
Does anyone know if there is a better way of doing this or what I am doing wrong?
Razor:
<form method="post">
<input id="string-value" />
<input type="button" id="add-item" value="Add item" />
<select asp-items="#Model.Model.ArrayOfStrings" id="hidden-select"></select>
<table id="table-items">
</table>
<input type="submit" value="Submit" />
</form>
public class ArrayModel
{
public List<SelectListItem> ArrayOfStrings { get; set; } = new List<SelectListItem>();
}
public class IndexModel : PageModel
{
[BindProperty]
public ArrayModel Model { get; set; }
public void OnGet()
{
Model = new ArrayModel();
}
public void OnPost()
{
System.Diagnostics.Debugger.Break();
}
}
JS:
$('#add-item').on('click', function () {
debugger;
var value = $('#string-value').val();
$('#hidden-select').append(new Option(value, value));
$('#table-item tr:last').after('<tr><td>' + value + '</td></tr>')
});
Repository can be found here.
The options of the select will not be posted so this will not work.
The easiest way to do this is append the results to a hidden input with a separator char, then do a string split on the server side.
Another, maybee more elegant way, would be to add hidden inputs with the same name. Each input with it's own value. You should then be able to get this as a List or Array on the server.
Razor:
<input value="#String.Join(",", Model.Model.ArrayOfStrings)" id="tags"></select>
JS
$('#tags').val($('#tags').val() + ',' + value);
Controller
public void OnPost(string tags)
{
var tagsArray = tags.split(',');
}

ASP.NET core persisting values between Get and Post error validation

I'm new to web development so I don't know a good way on how to persist data between requests.
This is my site so far:
The elephant title is being fetched from an API on the GET request, according to the titleId query parameter. When I press login, model validations are being run, for example that email and password must have been entered. However, when error page is returned, elephant text is empty since that value was not persisted. What are the best approaches to persist that value so that is still visible when POST error is returned? Does it has to be included in the POST data? I don't want to request the API again.
Code behind:
public class IndexModel : PageModel
{
private string apiTitle;
public string ApiTitle { get { return apiTitle; } set { apiTitle = value; } }
// Bind form values
[BindProperty]
public User user { get; set; }
public Task<IActionResult> OnGetAsync(string titleId)
{
if (!string.IsNullOrEmpty(titleId))
{
ApiTitle = await GetTitleFromApiAsync(titleId);
}
return Page();
}
public async Task<IActionResult> OnPostLoginAsync()
{
if (!IsLoginFormValid())
{
// When this is returned, for example if no password was entered,
// elephant title goes missing since apiTitle is null?
return Page();
}
var user = await LoginEmailAsync();
return RedirectToPage("/Profile");
}
}
Html:
#page
#model IndexModel
#{
ViewData["Title"] = "Home page";
}
<link rel="stylesheet" href="css/index.css">
<script src='http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js'></script>
<div class="login-page">
<span id="label_api_title">#Model.ApiTitle</span>
<div class="form">
<form class="login-form" method="post" asp-page-handler="Login">
<input type="text" placeholder="Email" asp-for="User.Email"/>
<span asp-validation-for="User.Email" class="text-danger"></span>
<input type="password" placeholder="Password" asp-for="User.Password1" />
<span asp-validation-for="User.Password1" class="text-danger"></span>
<button>login</button>
<p class="message">Not registered? Create an account</p>
</form>
</div>
</div>
<script src="js/index.js"></script>
Yes. What you see is the expected behavior. Remember, Http is stateless. You are making 2 separate http calls, one for the GET and one for POST. The second call has no idea what the first call did ( or even there was first call at all!)
If you want to have a way to read the ApiTitle property value in the Post call and return that to the view, you need to persist it somewhere so that it is available between http calls. But in your case, all you need is to include that in the form post and have the framework bind it for you.
In your case, you can simply use a public property (Which is settable and gettable) for this. No need to keep a private variable. Decorate your property with BindProperty attribute so the model binder will bind the data on this property.
public class CreateModel : PageModel
{
[BindProperty]
public string ApiTitle { get; set; }
//Your existing code goes here
}
Now inside your form tag, have an input hidden element for the ApiTitle. This way, when the form is submitted, the value of ApiTitle property will be send in the request data.
<form class="login-form" method="post" asp-page-handler="Login">
<input type="hidden" asp-for="ApiTitle"/>
<input type="text" placeholder="Email" asp-for="User.Username"/>
<!--Your other existing form elements -->
<button>login</button>
</form>
Now in your OnPostLoginAsync method, you can read the ApiTitle value if needed. When you return the Page (when validation fails), the UI will display the ApiTitle property value in your span element.
public async Task<IActionResult> OnPostLoginAsync()
{
var title = this.ApiTitle; // If you want to read this value
if (!ModelState.IsValid)
{
return Page();
}
return RedirectToPage("/Profile");
}
It could be that your not sending it inside your form in the razor and its only in the get request and not in the post request:
public Task<IActionResult> OnGetAsync(string titleId)//<----its here
{
if (!string.IsNullOrEmpty(titleId))
{
ApiTitle = await GetTitleFromApiAsync(titleId);
}
return Page();
}
//-->need to pass title id in post
public async Task<IActionResult> OnPostLoginAsync(string titleId)
{
if (!string.IsNullOrEmpty(titleId))
{
ApiTitle = await GetTitleFromApiAsync(titleId);
}
if (!IsLoginFormValid())
{
// When this is returned, for example if no password was entered,
// elephant title goes missing since apiTitle is null?
return Page();
}
var user = await LoginEmailAsync();
return RedirectToPage("/Profile");
}
and in your razor add the span in the form:
<span id="label_api_title">#Model.ApiTitle</span>
<div class="form">
<form class="login-form" method="post" asp-page-handler="Login">
<span id="label_api_title">#Model.ApiTitle</span>/*<--------here*/
<input type="text" placeholder="Email" asp-for="User.Email"/>
<span asp-validation-for="User.Email" class="text-danger"></span>
<input type="password" placeholder="Password" asp-for="User.Password1"/>
<span asp-validation-for="User.Password1" class="text-danger"></span>
<button>login</button>
<p class="message">Not registered? Create an account</p>
</form>

requiredif on an element in a list of custom inputs

I have a modelview that contains a list of ICustomInput values
public class DemoViewModel {
[Required]
public string FirstName {get; set;}
[Required]
public string LastName {get; set;}
[RequiredIf("DayPhoneRequired", true)]
public string DayPhone {get; set;}
public bool DayPhoneRequired {get; set;} = false;
public List<ICustomInput> CustomInputFields { get; set; } = new List<ICustomInput>();
}
an example of an ICustomInput
public class CustomTextInput : ICustomInput
{
public CustomField Field { get; }
public string DisplayName { get; set; }
[RequiredIf("DataValueRequired", true, ErrorMessage = "This is a required field")]
public virtual string DataValue { get; set; }
public bool DataValueRequired { get; set; } = false;
public virtual string ClassName => "CustomTextInput";
public string AssemblyName => "Application.Models";
}
The purpose of this is so that i can pull information from the DB about the custom input fields that the logged in client has requested on the form. One client may want a couple text fields, another client may want a drop down. These custom fields may or may not require input as well. (The CustomField object is an older object returned by the dataLayer and used heavily, I don't want to rebuild it, but assume it's just full of strings)
I have an editor template for the concrete implementations of ICustomInputs as well as custom binders that allow me to get the data on post. But the issue I'm having is that the RequiredIf attribute is setting the unobtrusive data values for client side validation the same for all ICustomInputs. It makes sense since they all have the same name for their dependent property, but it doesn't solve the issue I have.
My view displays the list of ICustomInput by simply:
#Html.EditorFor(model => model.CustomInputFields)
Then each concrete type that implements ICustomInput has it's own editorTemplate similar to:
<div class="columnPositioner">
<div class="inputContainer">
#Html.TextBoxFor(model => model.DataValue, new
{
#class = "inputFields input-lg form-control",
placeholder = Model.Field.Display
})
<span class="inputLabel">
#Html.LabelFor(model => model.Field.Display, Model.Field.Display)
</span>
#Html.ValidationMessageFor(model => model.DataValue, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.DataValueRequired)
</div>
</div>
The resulting HTML looks like:
<select name="CustomInputFields[0].DataValue" class="inputFields input-lg form-control" id="CustomInputFields_0__DataValue" data-val="true" data-val-requiredif-operator="EqualTo" data-val-requiredif-dependentvalue="True" data-val-requiredif-dependentproperty="DataValueRequired" data-val-requiredif="This is a required field"><option value="">TEST01</option>
<option value="01">01</option>
<option value="02">02</option>
<option value="03">03</option>
</select>
<input name="CustomInputFields[0].DataValueRequired" class="hasContent" id="CustomInputFields_0__DataValueRequired" type="hidden" value="True" data-val-required="The DataValueRequired field is required." data-val="true">
<input name="CustomInputFields[1].DataValue" class="inputFields input-lg form-control" id="CustomInputFields_1__DataValue" type="text" placeholder="TEST02" value="" data-val="true" data-val-requiredif-operator="EqualTo" data-val-requiredif-dependentvalue="True" data-val-requiredif-dependentproperty="DataValueRequired" data-val-requiredif="This is a required field">
<input name="CustomInputFields[1].DataValueRequired" id="CustomInputFields_1__DataValueRequired" type="hidden" value="False" data-val-required="The DataValueRequired field is required." data-val="true">
The hidden field is named properly, but how can I get the attribute to set the data-val-requiredif-dependentproperty to the actual id/name on the hidden field?
I do not currently have a custom editor template for the List. I did have one, but couldn't get it to bind the data back correctly. Dropping the editor template on the List and building unique editor templates for the concrete implementations of ICustomInput gave me all the UI layout control I needed and bound the data correctly, but now I can't get the client side validation to work properly. If it's just a editor template, what might that look like?
Update
This is A fix, but I don't like it. I have a javascript that's already doing an .each through inputs to apply styles so I added this to the .each:
function requiredIfHack($input) {
var depPropVal = $input.data("val-requiredif-dependentproperty");
//return if the value exists
if ($("#" + depPropVal).length) return;
//it doesn't. it's missing the parent object name
var parentName = $input.attr("name").split(".")[0].replace("[", "_").replace("]", "_");
$input.data("val-requiredif-dependentproperty", parentName + "_" + depPropVal);
}
It solves the problem, but I don't think it should be a problem that is the js responsibility to solve. And since it's a pretty sneaky fix, it could trip up others trying to work on this code in the future. I still want to find a better way to do it.

Model object passed to HttpPost action is having null values

I have a model with properties declared, Controller actions. and View with Viewmodel specified. I fill data in the form and submit, but model has only null values for all properties. If i try with view model i get same null values in HttpPost action.
My Model:
public class Supplier
{
public string SupplierSequenceNumber { get; set; }
public string SupplierName { get; set; }
public string SupplierActive { get; set; }
}
My Controller:
[HttpGet]
public ActionResult Add()
{
SupplierVM objSupplierVM = new SupplierVM();
return View(objSupplierVM);
}
[HttpPost]
public ActionResult Add(Supplier objSupplier)
{
return View();
}
My View:
#model AIEComm.ViewModel.SupplierVM
#using (Html.BeginForm("Add", "Supplier", FormMethod.Post, new { id = "formAddSupplier" }))
{
<div class="control-group">
#Html.LabelFor(m=>m.objSupplier.SupplierName, new{#class = "control-label"})
<div class="controls">
#Html.TextBoxFor(m => m.objSupplier.SupplierName, new { placeholder = "Swatch Style" })
#Html.ValidationMessageFor(m=>m.objSupplier.SupplierName)
</div>
</div>
<div class="control-group">
#Html.LabelFor(m=>m.objSupplier.SupplierActive, new{#class = "control-label"})
<div class="controls">
#Html.DropDownListFor(m=>m.objSupplier.SupplierActive,new SelectList(AIEComm.Models.Utilities.YesNoSelectList,"Value","Text"),new{#class=""})
#Html.ValidationMessageFor(m=>m.objSupplier.SupplierName)
</div>
</div>
<div class="control-group">
<div class="controls">
<input type="submit" class="btn btn-primary" id="btnSubmit" value="Add"/>
</div>
</div>
}
The reason for this is the following code:
m => m.objSupplier.SupplierName
You're generating HTML elements with a model that is inside a ViewModel. This is a good approach, and your problem can be solved quite easily.
It's a good approach because you're keeping things organised, but it's not working because the above code is saying:
Ok, using the ViewModel object (m), take the objSupplier object and then use the SupplierName property.
This is fine, but then when you're submitting data, you're saying (to the action):
Hi, I have a SupplierName property. Please put this into the objSupplier object which you can find inside the ViewModel object.
To which the action says "Well, I am expecting an objSupplier object, but what's this ViewModel you speak of?"
A simple solution is to create a new partial view to generate your form. It's model type should be:
_SupplierForm.cshtml
#model Supplier
#* // The Form *#
In your View, continue to use the same ViewModel, but pass in the correct supplier model:
#model AIEComm.ViewModel.SupplierVM
#Html.Partial("_SupplierForm", Model.objSupplier)

Resources