I've noticed something in the mvc default projects that made me wonder how it works. When I create a ddefault MVC Project with Individual User Accounts authentication, visual Studio scaffolds an AccountController with two "ResetPassword" Actions. One that accepts a string parameter via GET request. The Action looks like this:
// GET: /Account/ResetPassword
[AllowAnonymous]
public ActionResult ResetPassword(string code)
{
return code == null ? View("Error") : View();
}
And the View looks like this:
#model SISGRAD_MVC.Models.ResetPasswordViewModel
#{
ViewBag.Title = "Reset password";
}
<h2>#ViewBag.Title.</h2>
#using (Html.BeginForm("ResetPassword", "Account", FormMethod.Post, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
<h4>Reset your password.</h4>
<hr />
#Html.ValidationSummary("", new { #class = "text-danger" })
#Html.HiddenFor(model => model.Code)
<div class="form-group">
#Html.LabelFor(m => m.Email, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.TextBoxFor(m => m.Email, new { #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.Password, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.PasswordFor(m => m.Password, new { #class = "form-control" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.ConfirmPassword, new { #class = "col-md-2 control-label" })
<div class="col-md-10">
#Html.PasswordFor(m => m.ConfirmPassword, new { #class = "form-control" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" class="btn btn-default" value="Reset" />
</div>
</div>
I access the Action with the code in the URL, GET-style, and the view knows to initialize the model property from the URL. One point of interest is that this only works if I use #Html.HiddenFor(). How does this work, and how does the view know when to pull data from the URL, and when not to?
Because you method is
public ActionResult ResetPassword(string code)
the DefaultModelBinder will add the value of code to ModelState
The HiddenFor(m => m.Code) method uses values from ModelState rather that the values from the model if they exist so it will render
<input type="hidden" name="Code" id="Code" value="###" />
where ### is the value you passed to the method.
Your statement that the "view knows to initialize the model property from the URL" is not correct. The model is not initialized and is in fact null which you can test using
<div>#Model.Code</div>
which will throw an "Object reference not set to an instance of an object." exception, whereas
<div>#ViewData.ModelState["Code"].Value.AttemptedValue</div>
will display the correct value.
Side note: From your comments, the reason that DisplayFor(m => m.Code) does not show the value is that its is using the value in the ViewData (which is null because the model is null). The default display template uses the following code (refer source code)
internal static string StringTemplate(HtmlHelper html)
{
return html.Encode(html.ViewContext.ViewData.TemplateInfo.FormattedModelValue);
}
as opposed to HiddenFor(m => m.Code) which uses the following code (refer source code
default:
string attemptedValue = (string)htmlHelper.GetModelStateValue(fullName, typeof(string));
tagBuilder.MergeAttribute("value", attemptedValue ?? ((useViewData) ? htmlHelper.EvalString(fullName, format) : valueParameter), isExplicitValue);
break;
Note also that if you define a route with url: "Account/ResetPassword/{code}" then you do not need to add the hidden input in your view. It will be added as a route value by default - the BeginForm() method will render
<form action="Account/ResetPassword/###" ... >
Related
I'm having some trouble with data being sent through my controller, here's a simplified example:
public ActionResult EditNote(NotesModel model)
{
model.Author = Session["UserName"].ToString();
model.Note = null;
model.Title = null;
return View(model);
}
On my views page the data shown from the model is the exact same as how it was received by the method and all changes are ignored, why?
Bigger picture:
I'm trying to have a user edit an existing note in the database, if they're the one who made it of course. based on whether or not they're the author they will either edit the existing note or create a new note, this is where the problem lies. The controller is supposed to set all the values of the model to null so that on the views page they will be empty.
Editing an existing note is no problem however emptying the model so the editing page is blank does not work.
EDIT
This is my view page:
#model WebsiteProject.Models.NotesModel
#{
ViewBag.Title = "";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#section Sidebar
{
<div id="sidebarheadericon" style="background-image: url('../Content/icons/apps.png')"></div>
<div id="headertext"><h1>Welcome</h1></div>
<hr id="seperator" />
<p class="psidebar">test</p>
<p>
#Html.ActionLink("Create New", "EditNote")
</p>
}
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h1>NotesModel</h1>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<p class="control-label">Note by #Session["UserName"].ToString()</p>
<div class="form-group">
#Html.LabelFor(model => model.Title, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Title, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Title, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Note, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Note, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Note, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="largebtn" />
</div>
</div>
<p class="text-danger">#ViewBag.NoteViewError</p>
</div>
}
<div>
#Html.ActionLink("Back to List", "NoteApp")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Here you can see the data that is received (dummy data)
Now here you'll see that the data of the model is changed
Yet on the website it is not changed
The biggest problem isn't the Note and Title not being changed because the user can do that, but the Id and Author, which the user cannot, and should not be able to change.
This is related to the fact that you are using EditorFor:
#Html.EditorFor(model => model.Note, new { htmlAttributes = ... })
It so happens that EditorFor not only uses the Model object, it also uses ModelState which keeps a copy of all values for all Model items that were received, parsed and validated, along with any validation errors that this may have produced.
As long as ModelState has a value for that model item, it will be shown by EditorFor. The reason is that user input may be invalid, like entering 12X for an int Model property. EditorFor will show 12X if the form is re-rendered, which is coming from ModelState, and which could never come from Model.
Unwanted values can be removed by calling Remove, like this:
ModelState.Remove("Note");
On a different note, the ViewModel class that you are using here is not suited to the needs of this action method. It may be better to create a dedicated ViewModel class for it, with fewer properties, which you can then convert to the ViewModel type that the View needs.
I think you want to tell the incoming notes model is a new one or an existing one, here is some code to try,
public ActionResult EditNote(NotesModel model)
{
if(model.Id > 0) //assuming existing notes has id or any other ways you want to check
{
//save data
return View(model);
}
else //if Id has a value <= 0, return a new model with only Author set, maybe the Id (depending on how you want to generate the Id)
{
var model = new NotesModel();
model.Author = Session["UserName"].ToString();
return Viwe(model);
}
}
I use the following code for the Editing Button, but clicking on the Edit button does not post it
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "PageID,GroupID,Title,ShortDescription,Text,Autor,Tags,Visit,ImageName,ShowInSlider,CreatDateTime")] Page page,HttpPostedFileBase imgUp)
{
if (ModelState.IsValid)
{
if (imgUp != null)
{
if (page.ImageName != null)
{
System.IO.File.Delete(Server.MapPath("/PageImages/" + page.ImageName));
}
page.ImageName = Guid.NewGuid() + Path.GetExtension(imgUp.FileName);
imgUp.SaveAs(Server.MapPath("/PageImages/" + page.ImageName));
}
pageRepository.UpdatePage(page);
pageRepository.save();
return RedirectToAction("Index");
}
....
I have separate data layer and repository and I use the following code for the Editing Pages Controller, but with clicking on the Edit button does not post form. Though it works well for creation and delete btn. my view code is:
<div class="form-horizontal">
<h4>Page</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.PageID)
#Html.HiddenFor(model => model.Visit)
#Html.HiddenFor(model => model.CreatDateTime)
#Html.HiddenFor(model => model.ImageName)
<div class="form-group">
#Html.LabelFor(model => model.GroupID, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("GroupID", null, htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.GroupID, "", new { #class = "text-danger" })
</div>
</div>
....
<div class="form-group">
#Html.LabelFor(model => model.ImageName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<input type="file" name="imgUp" id="imgUp"/>
#Html.ValidationMessageFor(model => model.ImageName, "", new { #class = "text-danger" })
#if (Model.ImageName != null)
{
<img src="/PageImages/#Model.ImageName" class="thumbnail" style="max-width:150px" />
}
.....
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
i trace my code and find this eror:
Application Insights Telemetry (unconfigured): {"name":"Microsoft.ApplicationInsights.Dev.Request","time":"2018-04-25T08:10:44.3663705Z","tags":{"ai.internal.sdkVersion":"web: 2.0.0.25000","ai.device.roleInstance":"Laptop-erfan","ai.operation.name":"GET PageGroup
above horizontal form tag is this code:
#model DataLayer.Page
#{
ViewBag.Title = "Edit";}<h2>Edit</h2>#using (Html.BeginForm("Edit", "Pages", FormMethod.Post, new { enctype = "multipart/form-data" })){ #Html.AntiForgeryToken()
As mentioned in the comments it doesn't look like you have a form in your view. You can add one using:
#using (Html.BeginForm()) {
<!-- form contentshere -->
}
BeingForm has several overloads as well that you can use to change where it goes to, form method (GET/PUT) and set html attributes.
You need to add the complete form page into a Form tag.
please Use
#using (Html.BeginForm()) {
//Put your complete code inside this form...
}
It will create the Form tag with specified actions and type.
You can also specific the custom action method and type of the method as well.
I cannot figure out why my view only passes back a NULL for a model to my controller.
This is for an Edit Post method. I checked other controllers with Edit Post methods that are structured the same way as this one and they work fine. It seems to be just this view and controller.
Here is my view:
#model Non_P21_Quote_System_v1._0.Models.gl_code
#{
ViewBag.Title = "Edit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Edit</h2>
#if (TempData["Message"] != null)
{
<div style="color:green">
#TempData["Message"]
</div><br />
}
#if (ViewBag.error != null)
{
<div style="color:red">
<h3>#ViewBag.error</h3>
</div><br />
}
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>gl_code</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ID)
<div class="form-group">
#Html.LabelFor(model => model.GL_code, "GL Code", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.GL_code, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.GL_code, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.GL_description, "Gl Description", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.GL_description, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.GL_description, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.expense_type_ID, "Expense", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("expense_type_ID", null, htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.expense_type_ID, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.eag, "Employee Account Group", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("eag", null, htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.eag, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "gl_Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Here is my controller method:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(Include = "ID,GL_code,GL_description,expense_type_ID,eag")] gl_code gl_code)
{
if (ModelState.IsValid)
{
db.Entry(gl_code).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("gl_Index");
}
ViewBag.eag = new SelectList(db.employee_account_group, "ID", "eag_name");
ViewBag.expense_type_ID = new SelectList(db.expense_type, "ID", "type", gl_code.expense_type_ID);
return View(gl_code);
}
When I debug it, I see the model being passed in is of value NULL. I am seeing this on the controller side at the the parameters part of the Edit method.
Its null because your model contains a property named gl_code and you have also named the parameter for your model gl_code in the POST method.
Change the name of one or the other and the model will bind correctly.
What is happening internally is that the form submits a name/value pair for each successful form control, in your case gl_code=someValue. The DefaultModelBinder first initializes a new instance of your model. It then reads the form values and finds a match for the property in your model and sets it to someValue. But it also finds a match in the method parameters and tries set the value of the parameter to someValue, which fails (because you cannot do gl_code gl_code = "someValue";) and the model becomes null.
It appears you have a property on your view model called gl_code. In your controller, you also refer to the view model as gl_code.
Try changing this.
public async Task<ActionResult> Edit(gl_code gl_code)
To
public async Task<ActionResult> Edit(gl_code model)
I cannot figure out why my view only passes back a NULL for a model to my controller.
This is for an Edit Post method. I checked other controllers with Edit Post methods that are structured the same way as this one and they work fine. It seems to be just this view and controller.
Here is my view:
#model Non_P21_Quote_System_v1._0.Models.gl_code
#{
ViewBag.Title = "Edit";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h2>Edit</h2>
#if (TempData["Message"] != null)
{
<div style="color:green">
#TempData["Message"]
</div><br />
}
#if (ViewBag.error != null)
{
<div style="color:red">
<h3>#ViewBag.error</h3>
</div><br />
}
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>gl_code</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ID)
<div class="form-group">
#Html.LabelFor(model => model.GL_code, "GL Code", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.GL_code, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.GL_code, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.GL_description, "Gl Description", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.GL_description, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.GL_description, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.expense_type_ID, "Expense", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("expense_type_ID", null, htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.expense_type_ID, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.eag, "Employee Account Group", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("eag", null, htmlAttributes: new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.eag, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
<div>
#Html.ActionLink("Back to List", "gl_Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
Here is my controller method:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit([Bind(Include = "ID,GL_code,GL_description,expense_type_ID,eag")] gl_code gl_code)
{
if (ModelState.IsValid)
{
db.Entry(gl_code).State = EntityState.Modified;
await db.SaveChangesAsync();
return RedirectToAction("gl_Index");
}
ViewBag.eag = new SelectList(db.employee_account_group, "ID", "eag_name");
ViewBag.expense_type_ID = new SelectList(db.expense_type, "ID", "type", gl_code.expense_type_ID);
return View(gl_code);
}
When I debug it, I see the model being passed in is of value NULL. I am seeing this on the controller side at the the parameters part of the Edit method.
Its null because your model contains a property named gl_code and you have also named the parameter for your model gl_code in the POST method.
Change the name of one or the other and the model will bind correctly.
What is happening internally is that the form submits a name/value pair for each successful form control, in your case gl_code=someValue. The DefaultModelBinder first initializes a new instance of your model. It then reads the form values and finds a match for the property in your model and sets it to someValue. But it also finds a match in the method parameters and tries set the value of the parameter to someValue, which fails (because you cannot do gl_code gl_code = "someValue";) and the model becomes null.
It appears you have a property on your view model called gl_code. In your controller, you also refer to the view model as gl_code.
Try changing this.
public async Task<ActionResult> Edit(gl_code gl_code)
To
public async Task<ActionResult> Edit(gl_code model)
I created a simple class to test this directly, on the Edit POST action I was under the impression if a property is not in the form it will use the value which exists in the database already, instead it is getting set to null and overwriting any values.
This is from the scaffold generation, lets say I comment out Password and Password Confirmation because I don't want them to edit it, so they will get left out of the POST values.
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>User</h4>
<hr />
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.ID)
<div class="form-group">
#Html.LabelFor(model => model.Email, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Email)
#Html.ValidationMessageFor(model => model.Email)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.FirstName, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.FirstName)
#Html.ValidationMessageFor(model => model.FirstName)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.LastName, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.LastName)
#Html.ValidationMessageFor(model => model.LastName)
</div>
</div>
#*<div class="form-group">
#Html.LabelFor(model => model.Password, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Password)
#Html.ValidationMessageFor(model => model.Password)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.PasswordConfirmation, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PasswordConfirmation)
#Html.ValidationMessageFor(model => model.PasswordConfirmation)
</div>
</div>*#
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
So the request would look like this:
__RequestVerificationToken=wcxt2SbPmLuI-FzGh7b0okDgfFIXacVKnvvuYpVkgJjpNEAgCbMzHTeMQv61xhbxch0kId6nh6mK-qoKML3CHpLOfk1SawIQIpdtVicWkys1&ID=1&Email=foo.bar%40test.com&FirstName=foo&LastName=bar
And here I simply want to save the values that were edited and any values which were not included use the original values instead.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="ID,Email,FirstName,LastName,Password,PasswordConfirmation")] User user)
{
if (ModelState.IsValid)
{
db.Entry(user).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(user);
}
This is just an example to help me understand how to object gets populated when it's not in the form, if anyone could elaborate or point to resources which clearly show how the model is populated when the values aren't explicitly in the POST request that would be awesome.
I was under the impression if a property is not in the form it will
use the value which exists in the database already
That is not true. How does the form (just some HTML in the client browser) knows what value you have in the database ?
You have 2 solutions
1) In your HttpGet method, Send all the properties of your entity to the form, but don't display those to user. Keep in hidden fields inside the form and when you post the form, It will be available.
public ActionResult Edit(int id)
{
var vm = new EditUserVM();
var user = GetUserFromYourDb(id);
vm.Name = user.Name
vm.Email= user.Email
//The below property you don't want user to edit in form. But still sending.
vm.Token= user.Token
return View(vm);
}
in your view
#model EditUserVM
#using(Html.Beginform())
{
#Html.TextBoxfor(s=>s.Name)
#Html.TextBoxfor(s=>s.Email)
<!-- Hidden field here -->
#Html.HiddenFor(s=>s.Token)
#Html.HiddenFor(s=>s.UserId)
<input type="submit" />
}
2) If you do not wish to send all the fields to form, You may try this.(I recommend this). Have only fields which you want the user to edit in your GET form and In the HttpPost action method, Read the existing entity again from the database and update the fields with the values you received from the form. Your other field values stays same.
public ActionResult Edit(int id)
{
var vm = new EditUserVM();
var user = GetUserFromYourDb(id);
vm.Name = user.Name
vm.Email= user.Email
return View(vm);
}
in your view
#model EditUserVM
#using(Html.Beginform())
{
#Html.TextBoxfor(s=>s.Name)
#Html.TextBoxfor(s=>s.Email)
#Html.HiddenFor(s=>s.UserId)
<input type="submit" />
}
And in your HttpPost
[HttpPost]
public ActionResult Edit(EditUserVM model)
{
var existingUser = GetUserFromYourDb(model.UserId);
existingUser.Name = model.Name;
existingUser.Email = model.Email;
SaveUser(existingUser);
return RedirectToAction("UserSaved");
}