Custom dataannotations and clientside validation MVC2 - c#

I have a property on my view model that is a custom class with a value property.
e.g.
class mycustomobj
{
public int? Value {get; set; }
}
public class myviewmodel
{
[DefaultablePercentRange]
public property mycustomobj { get; set; }
}
I have a custom range attribute DefaultablePercentRange that I decorate this property with so that I can check apprpiate inputs. Associated with this is the relevant javascript validator for clientside.
The javascript is:
Sys.Mvc.ValidatorRegistry.validators["defaultablePercentRange"] = function (rule) {
var _minimum = rule.ValidationParameters["minimum"];
var _maximum = rule.ValidationParameters["maximum"];
return function (value, context) {
if (!value || !value.length) {
return true; // return true as null values allowed
}
var n = Number.parseLocale(value);
return (!isNaN(n) && _minimum <= n && n <= _maximum);
};
}
I am also using Html.EditorFor on my view with templates so that I can output the property as mycustomobj.Value rather than just mycustomobj . So the view property in html ends up being rendered something like:
<input class="defaultable tiny" default="0" defaultwhen="0" id="mycustomobj_Value" name="mycustomobj.Value" type="text" value="" placeholder="0" style="">
Now my problem is the javascript validation is passing null into my clientside validators function. After a fair amount of investigation I have identified this being because the JSON created for my custom DataAnnotationsModelValidator is not using the full id of the property. For example the JSON created is:
{"FieldName":"mycustomobj","ReplaceValidationMessageContents":true,"ValidationMessageId":"mycustomobj_validationMessage","ValidationRules":[{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"},{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"}]}
Where I need:
{"FieldName":"mycustomobj.value","ReplaceValidationMessageContents":true,"ValidationMessageId":"mycustomobj_value_validationMessage","ValidationRules":[{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"},{"ErrorMessage":"This value must be in the range 0 - 100","ValidationParameters":{"minimum":0,"maximum":100},"ValidationType":"defaultablePercentRange"}]}
My question is. How can I get the right property name serialized out for the clientside validation so that my clientside validation will work. My serverside works just fine.
Please let me know if anyone needs more info.

I ended up getting around this by using a combination of factors.
Created a model binder specifically for mycustomobj that knows how to set the value on mycustomobj
changed template so that .Value was output but the control name was still just property name
This meant that when binding back to the viewmodel on post I can ensure the correct property on mycustomobj was set. And in the javascript the javascript client validation code was being called appropiately as the correct input id was being set.

Related

ModelState is invalid for a nullable property

I have a model where the property CompanyID allows a null value
public partial class ReportTemplateItem
{
[Key]
public int ReportTemplateItemID { get; set; }
[Required]
public int ReportTemplateID { get; set; }
public int? CompanyID { get; set; }
}
In the DBContext OnModelCreating, there is no property declared for CompanyID
But when posting the ModelState is Invalid. The form works as intended if I remove ModelState validation
ModelState Invalid
Accepting a null value appears to be the default behavior for Model Validation, what am i missing?
Razor Pages with EF Core 3.0, Nullable Reference Types is disabled
many thanks
edit - the invalid object at time of validation
In your code if you have input or select if you try to set the value with zero
that could result in constrain problems in the database
the solution is to set the value=""
and why that work and just not setting the value at all result in validation error is due to that the validation check is running against the raw value which in our case will be string "null" not real null
so just do
<select asp-for="CustomerId" asp-items="#ViewBag.CustomersList" >
<option value="">please select...</option>
</select>
that will solve the problem of wrong binding
hope MS team take care of that next .net core version
If this is any help, you may try just not to send a null value at all (exclude it from data being sent).
For example, instead of sending the folowing json data:
var data = {
reportTemplateItemID: 1,
reportTemplateID: 2,
companyID: null
};
send only:
var data = {
reportTemplateItemID: 1,
reportTemplateID: 2
};
If you have a complex object, you may easily strip all nulls before making an ajax call:
// let's remove null and undefined values
// an easy way is to serialize using a replacer function and deserialize back
const str = JSON.stringify(data, function(key, value) { return value === null ? undefined : value; });
const newData = JSON.parse(str);
See how it works:
var data = { "aaa" : undefined, "bbb": null, ccc : ""}
// newData = "{"ccc":""}"
ModelState validation won't fail in such case (as long as the value type is nullable), at least in ASP.NET Core 3.1 (I didn't check other versions).
I had similar problem when my "CustomerId" was be selected from a select element.
the problem was solved with setting value for the default option of the select:
<select asp-for="CustomerId" asp-items="#ViewBag.CustomersList" >
<option value="0">please select...</option>
</select>
before setting value="0" for the default option in my action method the ModelState.IsValid always was false although the CustomerId property in the model was nullable.

Convert List of Enums to a SelectList and maintain display?

We have a view that is driven by a List of options. Depending on the initial conditions, a DropDownList is created with choices for the user's next step. This list is populated with choices from a larger list of options encased within a custom Enumeration.
So we have an Enum like so:
public enum ChangeMode
{
[Display(Name ="Please Select")] InitialState,
[Display(Name = "Change A Thing")] ChangeThing,
//...
[Display(Name = "Do a Dance")] DoADance,
FinishedSuccess,
FinishedFailure
}
And in the controller for the view, we build a List that contains all the valid options for the user, which is NEVER the full list.
if( /*Irrelevant determining characteristic*/ )
{//Valid choices for this option
model.ValidModes = new List<ChangeMode> { ChangeMode.InitialState, ChangeMode.ChangeThing};
}
We then create a DropDownList in the view, based on the model's list of ChangeModes
#Html.DropDownListFor(m => Model.AlterMethod, new SelectList(Model.ValidModes))
All of this is working just fine, except that the generated DDL does not include the user-friendly Name that each mode is assigned, instead showing the Enum's developer-readable value ('InitialState', for example). How do I change this to have the View correctly render the dropdownlist such that it uses the Display(Name) as the Text for the DDL?
You'll need to get the values from your Enum attributes. To do this, you need a helper method that will return the attribute.
here is a helper method to access Enum attributes:
public T GetAttribute<T>(Enum _enum) where T : Attribute
{
return
(T)_enum.GetType()
.GetField(Enum.GetName(_enum.GetType(), _enum))
.GetCustomAttribute(typeof(T));
}
Usage :
var name = GetAttribute<DisplayAttribute>(ChangeMode.InitialState).Name;
ASP.NET Core ships with a helper for generating a SelectListItem for
any enum.
This means that if you have an enum of the TEnum type, you can generate the available options in your
View using asp-items="Html.GetEnumSelectList<TEnum>()".
Change:
#Html.DropDownListFor(m => Model.AlterMethod, new SelectList(Model.ValidModes))
To:
<select asp-for="AlterMethod" asp-items="Html.GetEnumSelectList<Enums.ChangeMode>()"></select>
Or if you only want the Values from ValidModes:
<select asp-for="AlterMethod" asp-items="Html.GetEnumSelectList(typeof(Model.ValidModes))"></select>

Testing a x=> value in a HiddenFor line in a C# .NET view?

I have a form in a view in a C# ASP.NET MVC project that due to a bug in an earlier js cropping module occasionally ends off having a minus 1 (-1) in the value of the 'CropY' field.
Rather than trying to debug the cropper I thought I could just check for the -1 and make it a zero in the view, here:
#model UI.Models.PVModel
...
#Html.HiddenFor(x => x.CropY)
However, I don't seem to be able to modify the HiddenFor to set a value or 0 depending on if the value is >-1 or not, say with
#Html.HiddenFor(x => (x.CropY < 0 ? 0 : x.CropY))
As this (and all other combos I tried) gives me an error ( 'Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions.').
I tried altering the model value itself before the form on the view with
model.CropY = (model.CropY < 0 ? 0 : model.CropY)
But that doesn't alter the value in the HiddenFor form field (I'm a beginner to C# and .NET so forgive me if this is a fundamental error)
Next I tried altering the model itself to test for this
public int CropY
{
set { CropY = value; }
get { return (CropY < 0 ? 0 : CropY); }
}
But I can't get this to work (System.StackOverflowException: 'Exception of type 'System.StackOverflowException' was thrown.'), even if it is a viable method and I confess I don't know the correct format here!
Any idea please? Thanks.
--- edit after discussions below, this is more of what has been tried ---
Thanks. Okay, So very similarly, I have a basic get;set; in my model:
public class ProductViewModel
{
public int CropY { get; set; }
Then in my controller, it's a post as it's x in y number of forms/pages that go one after another:
[HttpPost]
[Route("Edit")]
[ValidateAntiForgeryToken]
public ActionResult Edit(ProductViewModel model, string nav)
{
model.CropY = (model.CropY < 0 ? 0 : model.CropY)
...
return View(view, model);
}
Then in my view I have this:
#model edie.UI.Models.ProductViewModel
#{
ViewBag.Title = "Step 2/";
}
<form action="#Url.Action(null, "MyPages")" method="post" class="form" novalidate="novalidate">
#Html.AntiForgeryToken()
#Html.HiddenFor(x => x.CropY)
And the value, when I view the page, is still coming through as -1. If I set breakpoints and step the code and check I can see the line in the controller is being triggered and setting CropY to 0, and the view is returned. On the view itself I can see the Model.CropY is 0. But the HiddenFor(x => x.CropY) inserts -1!
I suppose I'm missing something...
I would personally avoid implementation of setters and getters in Model/ViewModel objects, of course, it can helps in special cases but let's keep it simple. So, first of all, i would create simple view model object like this:
public class IndexViewModel
{
public int CropY { get; set; }
}
I would move the business logic from the view to the controller. In perfect world it should be moved to some service (SOC). So my get method in the home controller would look like this:
public class HomeController : Controller
{
// GET: Home
public ActionResult Index()
{
var value = -2;
var viewModel = new IndexViewModel();
if(value < 0)
{
viewModel.CropY = 0;
}
else
{
viewModel.CropY = value;
}
return View(viewModel);
}
}
And the Index.cshtml:
#model WebApplication1.Models.Home.IndexViewModel
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
#Html.HiddenFor(x => x.CropY)
value = -2; it's a variable which stores value that you receives (I suppose) from some data source like repository.
After some help from #stephen-muecke I found an answer. The issue is with with the modelstate. See TextBoxFor displaying initial value, not the value updated from code and TextBoxFor displaying initial value, not the value updated from code
So two of the methods I tried above did actually work (in a way) - altering the get; set; to handle the negative value and adding a check and update of the value in the controller before I return the view.
In the view itself the Model.CropY contained the correct 0 value then but the #Html.HiddenFor(x => x.CropY) still had the bad -1 value, and so added it to the form.
So the helper HiddenFor helper gets its value from the ModelState value, and not the model I was trying to alter and edit on the view.
Confused? I still am, but adding this in my POST controller fixed it:
ModelState.Clear();
You can also do a ModelState.Remove("SomeText"), so I'm advised this might be a better option that clearing the entire modelstate.

MVC EditorFor Remove Indexed Name

I have model that is a list of another model such that ModelList : ModelSingle
In my razor view I am using
#model somenamespace.ModelList
#Html.EditorForModel()
This iterates though each ModelSingle and returns an EditorTemplate that is strongly typed to ModelSingle.
#model somenamespace.ModelSingle
#using(Html.BeginForm("Action", "Controller", FormMethod.Post, new { id = "formname" + Model.ID}))
{
#Html.AntiForgeryToken()
#Html.EditorFor(p => p.SomeField)
#Html.EditorFor(p => p.AnotherField)
}
Each of these templates contains a form that can be used to edit the single model. These are posted individually with my controllers method expecting
public ActionResult(ModelSingle model)
The problem I'm having is that the model is not binding correctly. With a Model as such
public class ModelSingle()
{
public string SomeField { get; set; }
public string AnotherField { get; set; }
}
the EditorTemplate is being told that it was part of a list so I get
<Form>
<input name="[0].SomeField"/>
<input name="[0].AnotherField"/>
<input type="submit" value="Update"/>
</Form>
I can't simply bind to the ModelList as it's not naming ModelList[0].SomeField and even if it was I don't think that would work for anything but the first item.
Is there anyway to make the EditorTemplate ignore the fact that it's model was part of a list or force a DropDownListFor, EditorFor etc.... to just use the field name without prepending the [i].
I know I can force a Name="SomeField" change but I'd rather have a solution that will reflect any changes made in the Model class itself.
EDIT - As Requested added a simplified example of the View and EditorTemplate being used.
The problem is related to a mismatch between the input names generated by your page model (which is a list), and the model expected by your action, which is a single item from your list.
When rendering a list, the default behavior is to render the indexed names like you've shown to us (the [#] notation). Since you want to be able to post any arbitrary item from the list, you won't know ahead of time what index is used. When the model binder looks at the request for your single object, it does not attempt to use the index notation.
I don't know what your requirements are from the user perspective - e.g. whether or not a page refresh is desired, but one way to accomplish this is to provide a jQuery post for the specific item being posted:
// pass jquery form object in
var postItem = function($form) {
var postBody = {
SomeField: $form.find('input selector') // get your input value for this form
AnotherField: '' // another input select for this item
}
$.ajax({
url:'<your action url>',
type: 'POST',
contentType:"application/json; charset=utf-8",
data: JSON.stringify(postBody),
dataType: 'json',
success: function(response) {
// do something with returned markup/data
}
});
}
You are manually serializing a single instance of your model with a json object and posting that. What you return from the action is up to you: new markup to refresh that specific item, json data for a simple status, etc.
Alternately, you can consider manually looping over the items in your collection, and using Html.RenderPartial/Html.Partial to render each item using your View template. This will short-circuit the name generation for each item, and will generate the names as if it's a single instance of ModelSingle.
Finally, a quick (but kind of ugly) fix would be to have your action method take a list of ModelSingle objects. I don't suggest this.
Edit: I missed some important aspects of posting json to an mvc action
Edit2: After your comment about hardcoded names, something like this could help:
var inputs = $form.find('all input selector');
var jsonString = '{';
$.each(inputs, function(index, element) {
var parsedName = element.attr('name').chopOffTrailingFieldName();
jsonString += parsedName + ":'" + element.val() + "',";
});
jsonString += '}';

TextAreaFor doesn't show default value (MVC4)

I want to give my TextAreaFor a default value from my database (a certain comment which they can edit). I use the #Value and I can see it in the html code (inspect element), but in the textarea itself it isn't visible.
My code:
#Html.TextAreaFor(a => a.description, new { Value = ViewBag.Description }
ViewBag.Description = adver.description;
textarea works differently then other input fields in HTML and because of this the value attribute doesn't actually do anything that you'd expect.
<textarea value="you won't see this">you will see this</textarea>
Versus a text field:
<input type="text" value="you will see this" />
Because of this you need to assign the text that you want to be shown in the textarea to the variable before creating the textarea.
Edit
Here's a more complete example for you:
public ActionResult MyAction()
{
var myDefaultDescription = ""; //Replace with whatever you initialize ViewBag.Description with
return View(new AdverModel{ description = myDefaultDescription });
}
This will cause the model to be initialized in the controller. The #Html.TextAreaFor() only uses fields and values from the model object that's passed into the view. The Model variable is read only in the view so you must initialize it in the controller and pass it to the view during the return from the controller.
This will cause that field to auto populate with the default value.

Categories