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.
Related
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(',');
}
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
I have a model (called plan) which one of its properties is a list (ICollection) of Exercises (which is another model):
public int Id { get; set; }
public virtual ICollection<Exercise> Exercises { get; set; }
So i tried to create a view that creates a plan and another view to add Exercises to the plan from exercises in the database.
So i did a loop that ranges all the exercises from the DB and for each one i added a check box with and id same as the id of the exercise and i thought i could do something with it but i tried so many things and ways and i couldn't even sent the input of the check boxes to the controller.
I must say that i'm kinda new with programming with mvc and i tried to look all over the internet and i didn't really know people who knows to program so this is really my last chance to solve it. Sorry if it was a long post or too easy for you to even comment but i really need this.
This is very specific for my project and what i stacked on was to sent the input to the controller but i'm open to different solutions cause i'm desperate.
public async Task<ActionResult> AddExercises(string id,int[] selectedExercises)
{
List<Exercise> list = new List<Exercise>();
foreach (int i in selectedExercises)
{
list.Add(db.Exercises.Find(i));
}
db.Plans.Find(id).Exercises = list;
await db.SaveChangesAsync();
return RedirectToAction("index");
}
I am sure its wrong and the view i tried is:
#using (Html.BeginForm("index", "Plans"))
{
#Html.AntiForgeryToken()
foreach (var i in Model.Exercises)
{
<table class="table">
<tr>
<td>#Html.DisplayFor(modelItem => i.Level)</td>
<td>#Html.DisplayFor(modelItem => i.Description)</td>
<td>#Html.DisplayFor(modelItem => i.MoreDescription)</td>
<td>
#Html.ActionLink("Edit", "Edit", new { id = i.Id }) |
#Html.ActionLink("Details", "Details", new { id = i.Id }) |
<input type="checkbox" class="selectedObjects" id="i.id" /> |
</td>
</tr>
</table>
}
#Html.ActionLink("Finish", "AddExercises")
}
I use viewModel PlanExercise in this view
Your checkboxes do not have either a name attribute or a value attribute so there is nothing to submit. You html needs to be
<input type="checkbox" name="selectedExercises" value="#i.id" />
and then in order to submit the form, you need a submit button (remove #Html.ActionLink("Finish", "AddExercises"))
<input type="submit" value="Save" />
and the form needs to be (assuming the controller is FinishController)
#using (Html.BeginForm("AddExercises", "Finish"))
and the method needs to be marked with the [HttpPost] attribute.
I am making an application which has student class.
it is as follows. this is sample pseudo description. (don`t bother about syntax)
public class student{
[required]
string name;
int id;
List<course> courses;
}
public class course{
[required]
string name;
}
student class has list of courses. now when i bind this as model to view. view has a text field for name and the grid for courses which has course name. if i submit the form with out entering the name of student.the validation fires automatically. but for the courses it is not firing up. though on the controller side model state is considered invalid. but client side validation is not firing up.
again i am posting sort of pesudocode can`t post actual code.
#using (Html.BeginForm("create", "student", FormMethod.Post, new { id = "createStudent" }))
{
#Html.TextboxFor(m=>m.name)
#Html.ValidationMessageFor(m=>m.Name)
<table><tr>
<td>
#Html.TextBox("courses[0].name")<br/>
#Html.ValidationMessage("course[0].name")
</td>
</table>
<input type="submit"></input>
}
now clicking on submit for doesnot fire validation for course name . but model.state is invalid.
if i enter something in course name the model is validated.
but i want to display validation on the page as it is firing for student name.
Please suggest me something
note:- i also tried (for course name) this.
#Html.TextBox("courses[0].Name", "", new { #class = "input-xlarge", id = "txtPropName" })
#Html.ValidationMessageFor(m=>m.courses[0].Name)
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)