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.
Related
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.
Here is my logic in a code snippet.
I am trying to login, if data comes from web-page and if it matches with the database to proceed allowing to login
[HttpPost]//post method
public ActionResult Index(FormCollection collection)//using formcollection
{
var logindata = amcs.Logins.Where(a => a.Admin_Email_Id == collection["Admin_Email_Id"] && a.Admin_Password == collection["Admin_Password"]).SingleOrDefault();//compare string
if (logindata == null)//match data
{
return Redirect("/Contract/Login/index");//redirect if not match
}
else
{
Session["Emailids"] = collection["EmailId"];//add session
Session["Passwords"] = collection["AdminPassword"];
return Redirect("/Contract/Homepage/index");
}
}
If you are getting NULL as a result, have you looked further into this yourself?
For example, what values are
collection["Admin_Email_Id"]
collection["Admin_Password"]
Does the data in amcs.Logins contain objects whose properties match those values? You can hover the mouse of the data and look at it in the debugger.
EDIT
In response to a comment:
In the HTML does the
<input type="text" id="Admin_Email_Id"> also have an attribute name="Admin_Email_Id"? If not then add it manually e.g.
Html.TextBoxFor(m => m.Admin_Email_Id, new { name = "Admin_Email_Id", PlaceHolder = "EmailId"})
I'd be surprised that you need to do that though, but it's worth checking the HTML to check that name is there. Without name, when posting to the controller, the FormColleciton won't have a value for that missing 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 += '}';
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.
With a view model containing the field:
public bool? IsDefault { get; set; }
I get an error when trying to map in the view:
<%= Html.CheckBoxFor(model => model.IsDefault) %>
Cannot implicitly convert type 'bool?' to 'bool'. An explicit conversion exists (are you missing a cast?)
I've tried casting, and using .Value and neither worked.
Note the behaviour I would like is that submitting the form should set IsDefault in the model to true or false. A value of null simply means that the model has not been populated.
The issue is you really have three possible values; true, false and null, so the the CheckBoxFor cannot handle the three states (only two states).
Brad Wilson discusses on his blog here. He uses a DropDownList for nullable booleans.
This StackOverflow question does a much better job of describing the situation than I did above. The downside to the solution is sometimes nullable does not imply false, it should be nullable. An example of this would be filter criteria where you don't want true or false applied.
If you don't care about the null value, and just want the checkbox to be unchecked when its null, you can do the following:
Create another property of type bool in your Model like this:
public bool NotNullableBool
{
get
{
return NullableBool == true;
}
set
{
NullableBool = value;
}
}
Then just use that for binding...
To me, this is a lot better:
<%= Html.CheckBox("IsDefault", Model.IsDefault.HasValue? Model.IsDefault : false) %>
Here's how to map a bullable boolean bool? property in a DropDownListFor:
#model SomeModel
<!-- ...some HTML... -->
#Html.DropDownListFor(m => m.NullableBooleanProperty, new SelectList(
new[] {
new { Value = "", Text = "-- Choose YES or NO --" },
new { Value = "true", Text = "YES" },
new { Value = "false", Text = "NO" },
},
"Value",
"Text"
))
And here's how to map it to a CheckBoxFor by using a non-nullable proxy property as a workaround:
In the ViewModel:
public bool NullableBooleanPropertyProxy
{
get
{
return NullableBooleanProperty == true;
}
set
{
NullableBooleanProperty = value;
}
}
In the View:
#model SomeModel
<!-- ...some HTML... -->
#Html.CheckBoxFor(m => m.NullableBooleanPropertyProxy)
The only downside of this workaround is that the null value will be treated as false: if you can't accept that, it's better to use a control that can support three states, such as the aforementioned DropDownListFor .
For further info, read this post on my blog.
To add to vapcguy answer, another more "cleaner" way to do it is like follows
#Html.CheckBox("IsDefault", Model?.IsDefault)
Or
<%= Html.CheckBox("IsDefault", Model?.IsDefault) %>
You can create an editor template to render a checkbox for nullable Booleans. Name the template Boolean.cshtml to use it as the default for all of the Boolean properties within the site. Ensure that the file is in the folder ~/Views/Shared/EditorTemplates
#model bool?
#Html.CheckBox(String.Empty, Model??false)