I am working on my first MVC Webapplication (using Razor and C#) and I have run across a strange behaviour.
I am editing a "line" of data and using ajax calls to submit and redisplay data. Everything works fine as far as changing existing data and storing it goes. Also if I just redisplay the "line" that was submitted no problems.
However, I want to display a "new" line with some of the values from the old line retained and the rest blanked out.
However, when I submit the new line object to the Partial View, the "blanked" out values aren't being picked up by the #Html.... helpers. But if I display the property of the Model directly it has the correct (blank) value.
Here are the relevant sections of my code:
Controller Method:
[HttpPost]
public ActionResult EditLineForm(SkuRequestLine ln)
{
SkuRequestLine newline = null;
try
{
if (ln.Store(true))
{
ViewData["prodcatdesc"] = DataConnection.GetProductCategory(ln.Category).description;
newline = new SkuRequestLine();
newline.Copy(ln);
newline.Line = DataConnection.NextSkuRequestLineNumber(ln.Request);
newline.Comments = "";
newline.Description = "";
newline.Vendorsku = "";
return PartialView("EditLineForm", newline); // this line is being executed.
}
else
{
return PartialView("EditLineForm", ln);
}
}
catch (Exception ex)
{
List<string> msgs = new List<string>();
while (ex != null)
{
msgs.Add(ex.Message);
ex = ex.InnerException;
}
return PartialView("EditLineForm", ln);
}
}
Razor Code:
#model Sku_Management.Models.SkuRequestLine
#using (Ajax.BeginForm("EditLineForm", "SkuRequest", new AjaxOptions { OnSuccess = "UpdateLineList" }))
{
.
.
.
<tr>
<td>
<span class="editor-label">
#Html.LabelFor(model => model.Description)
</span>
</td>
<td colspan="5">
<span class="editor-field">
#Html.TextBoxFor(model => model.Description, new { #class = "fortywide" }) // Displays the Description from the edited Line passed in. Not what what Model.Description is.
#Html.ValidationMessageFor(model => model.Description)
</span>
<span>|#Model.Description|</span> // Displays "||" which is what it should be since Model.Description is blank.
</td>
</tr>
The only thing I can think of is that model => model.Description is using a cached version of the Model not the new Model passed into the PartialView call.
I have spent the day searching for anything even similar on the web but I can't find anything that even begins to describe this behavior.
Has anyone else encountered this and knows what I am dong wrong?
Thanks
This is because the HTMLHelpers look to the ModelState for values before using the Model.
You'll have to clear the ModelState entries to get this to work.
Related
I am taking an ASP.NET course an Udemy. Unfortunately, it's an old course, and I don't believe to get an answer there.
Now, what exactly is going on.
At this stage of the course, I need to work with Customers. The part that should show the list of customers, or the details of a specific customer, are working fine.
However, when I am trying to add a new customer to the database, the app crashes.
The full quote of the error:
The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method 'System.Web.Mvc.ActionResult CustomerDetails(Int32)' in 'VidlyExercise1.Controllers.CustomersController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Parameter name: parameters
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
I've trying to copy the exact code from lessons, but still something doesn't match.
The Customers Controller code:
public class CustomersController : Controller
{
private ApplicationDbContext _context;
public CustomersController()
{
_context = new ApplicationDbContext();
}
protected override void Dispose(bool Disposing)
{
_context.Dispose();
}
// GET: Customers
[Route("customers")]
public ActionResult CustomersList()
{
var customers = _context.Customers.Include(c => c.MembershipType).ToList();
var viewModel = new CustomersIndexViewModel
{
Customers = customers
};
return View(viewModel);
}
[Route("customers/{id}")]
public ActionResult CustomerDetails(int id)
{
var customer = _context.Customers.Include(m => m.MembershipType)
.SingleOrDefault(c => c.Id == id); //Eager loading
var viewModel = new CustomerDetailsViewModel
{
Name = customer.Name,
MembershipType = customer.MembershipType,
Birthdate = customer.Birthdate
};
return View(viewModel);
}
[Route("customers/new")]
public ActionResult New()
{
var membershipTypes = _context.MembershipTypes.ToList();
var viewModel = new NewCustomerViewModel()
{
MembershipTypes = membershipTypes,
Customer = new Customer()
};
return View("New", viewModel);
}
[HttpPost]
public ActionResult Create(Customer customer)
{
_context.Customers.Add(customer);
_context.SaveChanges();
return RedirectToAction("CustomersList", "Customers");
}
Now, when I click the button just to enter the View for adding a new Customer, it opens up fine.
But when I try to click the "Save" button, I get an error I posted above.
I even tried, changing the code in "Create" method, even just to post 404.
return HttpNotFound();
So, as I understand the Create method doesn't even get to the part of doing anything, it just crashes.
Here's the View code:
#model VidlyExercise1.ViewModels.NewCustomerViewModel
#{
ViewBag.Title = "New";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>New Customer</h2>
#using (Html.BeginForm("Create", "Customers", FormMethod.Get))
{
<div class="form-group">
#Html.LabelFor(m => m.Customer.Name)
#Html.TextBoxFor(m => m.Customer.Name, new { #class = "form-control" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.Birthdate)
#Html.TextBoxFor(m => m.Customer.Birthdate, new { #class = "form-control" })
</div>
<div class="form-group">
<label>
#Html.CheckBoxFor(m => m.Customer.IsSubscribedToNewsLetter) Subscribed to newsletter
</label>
</div>
<div class="form-group">
#Html.LabelFor(m => m.Customer.MembershipTypeId)
#Html.DropDownListFor(m => m.Customer.MembershipTypeId,
new SelectList(Model.MembershipTypes, "Id", "Name"),
"Select Membership Types", new { #class = "form-control" })
</div>
#Html.HiddenFor(m=>m.Customer.Id)
<button type="submit" class="btn btn-primary">Save</button>
}
I think the example code from mentor on GitHub is almost identical. The View html is different, because it also includes further lessons, but the Controller code seems to be correct.
I know the there is a need to post something I already tried. I did google for potential fixes, and did try some of them, but the problem is, I don't know what I am looking for.
Some examples of what I found in similar questions, but which didn't help:
#using (Html.BeginForm("Search", "Person",FormMethod.Get))
Don't use a variable "Id" in ActionResult CustomerList: I never used one
Maybe something else I don't remember now.
One more thing: When I click the button, the path shows: "localhost\Customers\Create"
There is no View suited for this, and I am not sure that it's what supposed to happen.
Can you please help to find what's wrong? Again, it's hard to find an error myself, since I only recently started learning ASP.NET (with the said course) and new to it.
And one more question: The "create" button should get a "Customer" from somewhere, but where in the View code I actually "send" it?
Thank you in advance,
Evgenie
I apologize for trouble, I seems I found what was the problem.
Upon clicking "Save" button, the route was trying to redirect to "customer/save".
And there was a line:
public ActionResult CustomerDetails(int)
So, the program was trying to open route "customer/save" as if the word "save" was an Id!
So, instead of even getting to the code in the ActionResult Save, it tried to find a customer with non-existing Id="Save".
To fix that, all I needed is to change the line declaring the ActionResult CustomerDetails, that it will only accept integers as an Id's.
Like this:
[Route("customers/{id:int}")]
public ActionResult CustomerDetails(int id)
I didn't have to do any changes to "Save" method in CustomersController, or to the relevant View.
Caution, before You read the rest
This question is not about POST method, redisplaying view with submited form or binding input values to controller method parameters. It's purely about rendering the View using html helper (HiddenFor or Hidden - both returns the same).
I created a simple hidden field using HiddenFor helper
#Html.HiddenFor(m => m.ProductCode)
and my problem is that value for this hidden field is rendered as null:
<input id="productCode" name="productCode" type="hidden" value/>
Even if I set it when instantiating a model and of course it's confirmed with debugging (it always has a value).
So instead it should look like this:
<input id="productCode" name="productCode" type="hidden" value="8888888"/>
Because I know there are some questions like this one (actually all of them refer to changing form values during form POST) I included list of things I tried already. My code is right below this section which I belive to be essential.
So far I tried:
ModelState.Clear() everywhere possible - cause as we know the value from ModelState is first place where it looks for the value. NO EFFECT which I expected, cause my ModelState is empty (my case is not about changing value during POST in controller as in many questions like that), so it should take value from my view model.
Not using HiddenFor helper, but pure html instead. WORKS, but its just workaround, not an answer to the problem.
Duplicating line with helper in view as follows:
#Html.HiddenFor(m => m.ProductCode)
#Html.HiddenFor(m => m.ProductCode)
PARTIALLY WORKS Produces first input as value/> and second as value="8888888"/> which indicates that there is probably something that hides initial property value. Anyway, I found nothing in ViewData at any point, nor in query string. Obviously I can't accept it this way, no explonation needed I guess.
Changing name of the property. Originally it was ProductCode. I changed it to productCode, ProdCode, ProductCodeasd, etc. and all of these WORKS. Again, it looks like there is something that hides/updates the value, but again - 100% sure there is no JS or anything else doing it. So still no answer found - just workaround again.
Explicitly setting the value for HiddenFor: #Html.HiddenFor(x => x.ProductCode, new {Value = #Model.ProductCode}). NO EFFECT, renders the same way.
Using #Html.Hidden instead of #Html.HiddenFor. NO EFFECT, with name set as ProductCode it renders the same way.
One more thing I found interesting. Reading html with Display page source in Chrome [ctrl+U] shows that value is valid value="8888888"/>, but in DevTools it's still value/> and of course submitting the form passes null to Controller method.
Model
public class Product
{
public string Description { get; set; }
public int Quantity { get; set; }
public string ProductCode { get; set; }
public string ImageUrl { get; set; }
public Product(string desc, string productCode, string imgUrl)
{
Description = desc;
ProductCode = productCode;
ImageUrl = imgUrl;
}
}
View
#model Product
#using (Html.BeginForm("UpdateCart", "Cart"))
{
<div class="row pad10">
<div class="col-sm-6 text-center">
<img src="#Model.ImageUrl" width="300" height="300" />
</div>
<div class="col-sm-6 text-justify">
<p>#Model.Description</p>
<div class="row padding-top-2">
<div class="col-sm-6">
<label>#CommonResources.Quantity: </label>
</div>
<div class="col-sm-4">
#Html.TextBoxFor(m => m.Quantity, new
{
#class = "form-control",
#data_val_required = CommonResources.FieldRequired,
#data_val_number = CommonResources.ValidationNumber
})
#Html.ValidationMessageFor(model => model.Quantity, "", new { #class = "text-danger" })
#Html.HiddenFor(m => m.ProductCode)
</div>
</div>
</div>
</div>
<div class="text-center col-xs-12 padTop20 padBottom20">
<input type="submit" value="Submit" class="whtBtn pad" />
</div>
}
Controller
The view is returned from controller with RedirectToAction as follows:
ValidateAndProceed -> ResolveNextStep (here redirection occurs) -> ShowProduct
public ActionResult ValidateAndProceed()
{
var order = Session.Current.Order;
var lang = LangService.GetSelectedLanguage();
var invoice = Session.Current.CurrentInvoice;
var localCurrency = Session.Current.LocalCurrencyInfo;
List<CheckoutValidationFieldError> errors = new List<CheckoutValidationFieldError>();
errors = ValidationService.ValidateAddress(order);
if (errors.Count > 0)
{
return RedirectToAction("InvalidAddress", "Address", new { serializedErrors = JsonConvert.SerializeObject(errors) });
}
return ResolveNextStep(order, invoice);
}
public ActionResult ResolveNextStep(IOrder order, IInvoice invoice)
{
if (OrderService.ShowProductView(order, invoice))
{
return RedirectToAction("ShowProduct");
}
return RedirectToAction("Summary");
}
public ActionResult ShowProduct()
{
Product model = ProductService.GetProduct(Session.Current.CurrentInvoice);
return View("~/Views/Product.cshtml", model );
}
Finally, what can cause such a weird behavior? I've already ran out of options. Maybe anyone had problem like mine before, would appreciate any clue on this case.
I debugged the whole process of rendering the view (got into .Net sources) checking every possible place that could make it fail and found nothing.
After #AndyMudrak and #Jeremy Lakeman comments I decided to try again to find JavaScript responsible for that behavior, but deeper than I did before. What I found was a really silly script where element Id is being concatenated from three strings what I didn't expect, cause it's really badly implemented. So finally - JavaScript is doing it and there is no bad behavior from framework etc.
Actually I am a bit disappointed (even if it's good to know this easy answer) cause it looked much more complicated than it really was and it took me hours to find out how simple it is :|
Thanks for comments, sorry for final simplicity.
Imagine a simple form that takes an email input like this:
#using (Html.BeginForm("save", "email", FormMethod.Post, new { #id = "my__form" }))
{
<div class="field">
#Html.LabelFor(model => model.Email)
#Html.ValidationMessageFor(model => model.Email)
#Html.TextBoxFor(model => model.Email, new { #placeholder = "Enter your email", #type = "email" })
</div>
<button type="submit" class="btn">Save email</button>
<div class='spinner'></div>
}
The spinner is not displayed initially (CSS):
.spinner {
display: none;
}
On form submit I show a spinner on the page:
$('.btn').on("click", function (event) {
event.preventDefault();
$('#my__form').submit();
$('.spinner').show();
});
My action is as follows:
[HttpPost]
[Route("email")]
public ActionResult Save(EmailViewModel model)
{
if (ModelState.IsValid)
{
//Do stuff with email
return RedirectToAction("action", "contoller");
}
return View(model);
}
Think of the above as pseudo-code that represents a more generic issue.
If a model state is invalid and the UI is updated in some way (in this case showing a spinner), what's the pattern or mechanism to reset the form?
For clarity, I am not talking here about standard data validation etc. I've tried having a hidden input on the form that I can populate using ModelState.AddModelError("spinner", false) and then acts as a 'switch' that javascript can read and then hide the spinner. This feels like a hack.
It feels like this is a common problem to which there is a common solution?
The hack you mentioned is really how it would be done using normal ASP.NET MVC.
There can be different implementations, such as storing the flag in a ViewBag instead. But the idea is the same.
You might be better off posting the form via AJAX, whose result might include a success flag and/or a list of validation errors. You can then manipulate the DOM in the submit handler via JavaScript based on this result.
I have a partialview _Psite which contains two dropdownlist and a text box, second one is based one first as Jsonresult (cascading dropdowns). So now suppose if customer select values in first dropdownlist, second one will load based on jquery and json.Then when he enter wrong values in text box validation fails(Session["username"] == null) it will display the same partial view after post in order to reenter .The problem now i am facing is the two dropdownlist is resetting in to default values.I have googled but couldn't find a solution
Following is view of _Psite
#using (Ajax.BeginForm("_firstGridAll", "mnis", new AjaxOptions { HttpMethod = "POST", UpdateTargetId = "PsitegridContent" }))
{
<div style="float: left">
#Html.DropDownList("REGION_CODE", (SelectList)ViewBag.Categories, "Select region code")
#Html.ValidationMessageFor(m => m.REGION_CODE)
</div>
<div class="tested">
<select id="GEO_ZONE_CODE" name="GEO_ZONE_CODE"></select>
</div>
<div class="tested">
#Html.TextBoxFor(m => m.PSITE_ID)
#Html.ValidationMessageFor(m => m.PSITE_ID)
</div>
<div class="Testedagain">
<input type="submit" value="Search" />
</div>
}
Controller is
public ActionResult _Psite()
{
if (TempData["values"].ToString() == "value persists")
{
ViewBag.change = true;
// ViewBag.Categories = TempData["EnterUniqueKeyHere"];
// return PartialView("_failValidation");
}
var categories = db1.MN_PSITE.Select(c => new
{
REGION_CODE = c.REGION_CODE,
CategoryName = c.REGION_CODE
}).Distinct().ToList();
ViewBag.Categories = new SelectList(categories, "REGION_CODE", "CategoryName");
ViewBag.error = false;
ViewBag.change = false;
return PartialView();
}
and the controller for validating data is following
[HttpPost]
public ActionResult _firstGridAll(string REGION_CODE, string GEO_ZONE_CODE, string PSITE_ID)
{
if (ModelState.IsValid == true)
{
Session["username"] = null;
var items = db1.MN_PSITE.Where(x => x.REGION_CODE == REGION_CODE).Where(y => y.GEO_ZONE_CODE == GEO_ZONE_CODE).Where(z => z.PSITE_ID == PSITE_ID);
//db1.MN_PSITE.Where(x => x.REGION_CODE == Region).Where(y => y.GEO_ZONE_CODE == GeoZONE).Where(z => z.PSITE_ID == Psiteid);
foreach (var it in items)
{
Session["username"] = it.PSITE_SLNO.ToString();
return PartialView(items.ToList());
}
if (Session["username"] == null) //validation fails
{
TempData["error"] = "value doesnot exisit,please renter the details";
return RedirectToAction("_Psite");
}
}
//count = 0;
return PartialView(db1.MN_PSITE.ToList());
}
UPDATE
i am using Entityframework generated classes as model no view viewmode ,do here 'db' is an instance of entity class
If you were posting a view model into your action instead of individual parameters, then you would be able to simply pass that model back out in your partial at the end of the action.
It will only be a small class with a few properties, so will take a minute to create. Do it. It will give you what you want.
I've got a page with some server side validation. It works, in that on a server validation failure it displays the same page, with a box with the validation errors above the form where the data was input.
My problem is that there is a whole bunch of other stuff at the top of the page, so the user isn't directed to the error box, in fact it could be off screen. The errors are in a div #server_errors. What I want is to tell the controller to return the view, but jump to the errors section, the same as appending #server_errors to the url.
the controller returns like this:
public ActionResult ChangeRiskCategory(Guid id)
{
//...
//call server side method, handle errors
//...
return View("ChangeRiskCategory", changeRiskCategoryModel);
}
and I can't see a way to inject the div id into the view at this point. I can see validating client side would solve this problem but it needs to work without js enabled so I think that rules that out.
You could try something like this:
public ActionResult ChangeRiskCategory(Guid id, bool error = false)
{
//...
//call server side method, handle errors
//...
if (!error && !ModelState.IsValid /*or other way of working out the error will be displayed*/)
{
return Redirect(Url.Action("ChangeRiskCategory") + "?id=" + id + "&error=true#server_error");
}
return View("ChangeRiskCategory", changeRiskCategoryModel);
}
(The "error" parameter is to stop it endlessly redirecting)
If you change your mind about using javascript you could simply emit:
location.href='#server_errors';
What about passing the div id as a property of the ViewModel to the View? Then you can do whatever you want with it in Javascript in the view. After all, the ViewModel represents the data and state of the View.
Use model based validation and have you view look something like this
#model SampleApplication.Models.BasicDemoModel
<form id="AjaxForm" action="/">
<table>
<tr>
<td>#Html.LabelFor(x => x.Name)</td>
<td>
#Html.TextBoxFor(x => x.Name)
#Html.ValidationMessageFor(x => x.Name, "*")
</td>
</tr>
<tr>
<td>#Html.LabelFor(x => x.Email)</td>
<td>
#Html.TextBoxFor(x => x.Email)
#Html.ValidationMessageFor(x => x.Email, "*")
</td>
</tr>
#{
Html.RenderPartial("Address", Model);
}
</table>
#if (!string.IsNullOrWhiteSpace(Model.Message))
{
<h2>#Model.Message</h2>
}
#if (!ViewContext.ViewData.ModelState.IsValid)
{
#Html.ValidationSummary()
}
<input type="submit" title="Submit Form" onclick="PostFormWithAjax();return false;" />
</form>
if you need extra validation messages you can just add it to the more state like this
public ActionResult ChangeRiskCategory(Guid id)
{
//...
//call server side method, handle errors
//...
ModelState.AddModelError("MyInput","This isn't Right");
return View("ChangeRiskCategory", changeRiskCategoryModel);
}