Successful Model Editing without a bunch of hidden fields - asp.net-mvc

In Short: How do I successfully edit a DB entry without needing to include every single field for the Model inside of the Edit View?
UPDATE
So I have an item in the DB (an Article). I want to edit an article. The article I edit has many properties (Id, CreatedBy, DateCreated, Title, Body). Some of these properties never need to change (like Id, CreatedBy, DateCreated). So in my Edit View, I only want input fields for fields that can be changed (like Title, Body). When I implement an Edit View like this, Model Binding fails. Any fields that I didn't supply an input for gets set to some 'default' value (like DateCreated gets set to 01/01/0001 12:00:00am). If I do supply inputs for every field, everything works fine and the article is edited as expected. I don't know if it's correct in saying that "Model Binding fails" necessarily, so much as that "the system fills in fields with incorrect data if no Input field was supplied for them in the Edit View."
How can I create an Edit View in such a way that I only need to supply input fields for fields that can/need editing, so that when the Edit method in the Controller is called, fields such as DateCreated are populated correctly, and not set to some default, incorrect value? Here is my Edit method as it currently stands:
[HttpPost]
public ActionResult Edit(Article article)
{
// Get a list of categories for dropdownlist
ViewBag.Categories = GetDropDownList();
if (article.CreatedBy == (string)CurrentSession.SamAccountName || (bool)CurrentSession.IsAdmin)
{
if (ModelState.IsValid)
{
article.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
article.LastUpdated = DateTime.Now;
article.Body = Sanitizer.GetSafeHtmlFragment(article.Body);
_db.Entry(article).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(article);
}
// User not allowed to edit
return RedirectToAction("Index", "Home");
}
And the Edit View if it helps:
. . .
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Article</legend>
<p>
<input type="submit" value="Save" /> | #Html.ActionLink("Back to List", "Index")
</p>
#Html.Action("Details", "Article", new { id = Model.Id })
#Html.HiddenFor(model => model.CreatedBy)
#Html.HiddenFor(model => model.DateCreated)
<div class="editor-field">
<span>
#Html.LabelFor(model => model.Type)
#Html.DropDownListFor(model => model.Type, (SelectList)ViewBag.Categories)
#Html.ValidationMessageFor(model => model.Type)
</span>
<span>
#Html.LabelFor(model => model.Active)
#Html.CheckBoxFor(model => model.Active)
#Html.ValidationMessageFor(model => model.Active)
</span>
<span>
#Html.LabelFor(model => model.Stickied)
#Html.CheckBoxFor(model => model.Stickied)
#Html.ValidationMessageFor(model => model.Stickied)
</span>
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Title)
#Html.ValidationMessageFor(model => model.Title)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Body)
</div>
<div class="editor-field">
#* We set the id of the TextArea to 'CKeditor' for the CKeditor script to change the TextArea into a WYSIWYG editor. *#
#Html.TextAreaFor(model => model.Body, new { id = "CKeditor", #class = "text-editor" })
#Html.ValidationMessageFor(model => model.Body)
</div>
</fieldset>
. . .
If I were to leave out these two inputs:
#Html.HiddenFor(model => model.CreatedBy)
#Html.HiddenFor(model => model.DateCreated)
when the Edit method is called, they're set to default values. CreatedBy is set to Null, Created is set to 01/01/0001 12:00:00am
Why are they not set to the values as they are currently set to in the DB?

After yet some more research I came upon some tools that assist in the ViewModel process - one being AutoMapper & the other InjectValues. I went with InjectValues primarily because it can not only "flatten" objects (map object a -> b) but it can also "unflatten" them (map object b -> a) - something that AutoMapper unfortunately lacks out-of-the-box - something I need to do in order to update values inside of a DB.
Now, instead of sending my Article model with all of its properties to my views, I created an ArticleViewModel containing only the following properties:
public class ArticleViewModel
{
public int Id { get; set; }
[MaxLength(15)]
public string Type { get; set; }
public bool Active { get; set; }
public bool Stickied { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; }
[Required]
[AllowHtml]
public string Body { get; set; }
}
When I Create an Article, instead of sending an Article object (with every property) I send the View a 'simpler' model - my ArticleViewModel:
//
// GET: /Article/Create
public ActionResult Create()
{
return View(new ArticleViewModel());
}
For the POST method we take the ViewModel we sent to the View and use its data to Create a new Article in the DB. We do this by "unflattening" the ViewModel onto an Article object:
//
// POST: /Article/Create
public ActionResult Create(ArticleViewModel articleViewModel)
{
Article article = new Article(); // Create new Article object
article.InjectFrom(articleViewModel); // unflatten data from ViewModel into article
// Fill in the missing pieces
article.CreatedBy = CurrentSession.SamAccountName; // Get current logged-in user
article.DateCreated = DateTime.Now;
if (ModelState.IsValid)
{
_db.Articles.Add(article);
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
ViewBag.Categories = GetDropDownList();
return View(articleViewModel);
}
The "missing pieces" filled in are Article properties I didn't want to set in the View, nor do they need to be updated in the Edit view (or at all, for that matter).
The Edit method is pretty much the same, except instead of sending a fresh ViewModel to the View we send a ViewModel pre-populated with data from our DB. We do this by retrieving the Article from the DB and flattening the data onto the ViewModel. First, the GET method:
//
// GET: /Article/Edit/5
public ActionResult Edit(int id)
{
var article = _db.Articles.Single(r => r.Id == id); // Retrieve the Article to edit
ArticleViewModel viewModel = new ArticleViewModel(); // Create new ArticleViewModel to send to the view
viewModel.InjectFrom(article); // Inject ArticleViewModel with data from DB for the Article to be edited.
return View(viewModel);
}
For the POST method we want to take the data sent from the View and update the Article stored in the DB with it. To do this we simply reverse the flattening process by 'unflattening' the ViewModel onto the Article object - just like we did for the POST version of our Create method:
//
// POST: /Article/Edit/5
[HttpPost]
public ActionResult Edit(ArticleViewModel viewModel)
{
var article = _db.Articles.Single(r => r.Id == viewModel.Id); // Grab the Article from the DB to update
article.InjectFrom(viewModel); // Inject updated values from the viewModel into the Article stored in the DB
// Fill in missing pieces
article.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
article.LastUpdated = DateTime.Now;
if (ModelState.IsValid)
{
_db.Entry(article).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(viewModel); // Something went wrong
}
We also need to change the strongly-typed Create & Edit views to expect an ArticleViewModel instead of an Article:
#model ProjectName.ViewModels.ArticleViewModel
And that's it!
So in summary, you can implement ViewModels to pass just pieces of your Models to your Views. You can then update just those pieces, pass the ViewModel back to the Controller, and use the updated information in the ViewModel to update the actual Model.

View model example:
public class ArticleViewModel {
[Required]
public string Title { get; set; }
public string Content { get; set; }
}
Binding example
public ActionResult Edit(int id, ArticleViewModel article) {
var existingArticle = db.Articles.Where(a => a.Id == id).First();
existingArticle.Title = article.Title;
existingArticle.Content = article.Content;
db.SaveChanges();
}
That is simple example, but you should look at ModelState to check if model doesn't have errors, check authorization and move this code out of controller to service classes, but
that is another lesson.
This is corrected Edit method:
[HttpPost]
public ActionResult Edit(Article article)
{
// Get a list of categories for dropdownlist
ViewBag.Categories = GetDropDownList();
if (article.CreatedBy == (string)CurrentSession.SamAccountName || (bool)CurrentSession.IsAdmin)
{
if (ModelState.IsValid)
{
var existingArticle = _db.Articles.First(a => a.Id = article.Id);
existingArticle.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
existingArticle.LastUpdated = DateTime.Now;
existingArticle.Body = Sanitizer.GetSafeHtmlFragment(article.Body);
existingArticle.Stickied = article.Stickied;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(article);
}
// User not allowed to edit
return RedirectToAction("Index", "Home");
}

another good way without viewmodel
// POST: /Article/Edit/5
[HttpPost]
public ActionResult Edit(Article article0)
{
var article = _db.Articles.Single(r => r.Id == viewModel.Id); // Grab the Article from the DB to update
article.Stickied = article0.Stickied;
// Fill in missing pieces
article.LastUpdatedBy = MyHelpers.SessionBag.Current.SamAccountName;
article.LastUpdated = DateTime.Now;
if (ModelState.IsValid)
{
_db.Entry(article0).State = EntityState.Unchanged;
_db.Entry(article).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index", "Home");
}
return View(article0); // Something went wrong
}

Use ViewModels.
Through my continued research of finding a solution to this issue I believe that using these things called "ViewModels" is the way to go. As explained in a post by Jimmy Bogard, ViewModels are a way to "show a slice of information from a single entity."
asp.net-mvc-view-model-patterns got me headed on the right track; I'm still checking out some of the external resources the author posted in order to further grasp the ViewModel concept (The blog post by Jimmy being one of them).

In addition to the answer, AutoMapper can also be used to unflatten it.
Using AutoMapper to unflatten a DTO

Related

ASP.NET MVC 4 Error Saving ViewModels

Can somebody help me on how to save and update data into multiple entities using a ViewModel?
I have a ViewModel that looks like this:
public class StudentViewModel
{
public Student student;
public StudentAddress studentAddress { get; set; }
public StudentPhoto studentPhoto { get; set; }
// Three entities are related to one to one relationship
public StudentViewModel()
{ }
}
My Controller is:
[HttpPost]
public ActionResult Create(StudentViewModel studentViewModel)
{
if (ModelState.IsValid)
{
return View(studentViewModel);
}
Student s = new Student()
{
Name =studentViewModel.Student.Name,
Speciality = studentViewModel.Student.Speciality,
DateOfReg = studentViewModel.Student.DateOfJoinig,
Qualification = studentViewModel.Student.Qualification,
Email = studentViewModel.Student.Email
};
StudentAddress sa = new StudentAddress()
{
StudentId= studentViewModel.Student.StudentId,
Address = studentViewModel.StudentAddress.Address,
Area = studentViewModell.StudentAddress.Area,
City = studentViewModel.StudentAddress.City,
State = studentViewModel.StudentAddress.State,
Mobile = studentViewModel.StudentAddress.Mobile
};
StudentPhoto sp = new StudentPhoto()
{
StudentId= studentViewModel.Student.StudentId,
Photo = studentViewModel.StudentPhoto.Photo
};
db.Students.Add(s);
db.StudentAddress.Add(sa);
db.StudentPhoto.Add(sp);
db.SaveChanges();
return RedirectToAction("Home");
}
View is:
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Doctor</legend>
#Html.EditorFor(model => model.Student.Name )
#Html.EditorFor(model => model.Student.Speciality)
#Html.EditorFor(model => model.Student.DateOfJoinig)
#Html.EditorFor(model => model.Student.Standard)
#Html.HiddenFor(model => model.Student.StudentId)
#Html.EditorFor(model => model.StudentAddress.Address)
#Html.EditorFor(model => model.StudentAddress.Area)
#Html.EditorFor(model => model.StudentAddress.City)
#Html.EditorFor(model => model.StudentAddress.State)
#Html.HiddenFor(model => model.Student.StudentId)
#Html.EditorFor(model => model.StudentPhoto.Photo)
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
I was able to retrieve and display the data (from multiple entities) into the view. However, now I'm stuck on how can I save and update the above entities with the new data. Most of the examples are 1-1 relationship the mapping is automatic, but in this case the data belongs to multiple entities.
My problem is when i try to save data it redirected to the create page. "ModelState.IsValid" is false always so no data saved. Please help me how do i proceed.
Thanks.
This line at the top of your Action is wrong:
if (ModelState.IsValid)
{
return View(studentViewModel);
}
It should be the opposite, only if the Model is NOT valid, then you should stop the process and re-render the View with the form.
Try:
if (!ModelState.IsValid)
{
return View(studentViewModel);
}
In Controller you check if(modelstate.isvalid) - if is valid you returned view without saving data from view.
The problem with your implementation is that your view model contains a several models(Entities). This is not a good implementation.
Try to create a viewmodel which just contains the fields (flattened version) that you want to be edited by the user when creating a student. Use Data Annotations in your view model like Required or StringLength to validate user inputs.

MVC Ajax Begin Form save and then using the ID returned after saving in the view

I have a model with two entities (linked with a foreign key) and each entity has its own tab rendered using a partial view. Each tab also has its own Ajax form. When I save the entity in the first tab I now have the ID of the entity which I want to return to the two partial views in order to enable the saving of the second entity or saving updates to the first entity. I cannot get this value back to the view.
The model:
public class Entity1
{
int ID1 { get; set; }
[Some attributes]
string field1 { get; set; }
}
public class Entity2
{
int ID2 { get; set; }
[Some attributes]
string field2 { get; set; }
}
public class MyModel
{
Entity1 entity1 = new Entity1()
Entity2 entity2 = new Entity2()
}
The controller:
public class MyController : Controller
{
[HttpGet]
public ActionResult Index()
{
var model = new MyModel();
model.entity1.ID1 = 0;
model.entity2.ID2 = 0;
return PartialView(model);
}
[HttpPost]
public ActionResult Index(MyModel model)
{
SaveMyModel(model)
// have tried ModelState.Clear(); here
return PartialView(model);
}
}
And finally one of the two partial views
#model MyModel
#using (Ajax.BeginForm("Index", "Home",
new AjaxOptions
{
HttpMethod = "POST"
}
))
{
#Html.LabelFor(m => m.Entity1.field1)
#Html.EditorFor(m => m.Entity1.field1)
#Html.HiddenFor(m => m.Entity1.ID1)
<div class="form-actions">
<button type="submit">
Next section</button>
</div>
}
My save function either inserts or updates depending on the value of ID1.
The problem is that the values of ID1 always stays at zero and the hidden field is not refreshed on the return. I have tried single stepping through the razor refresh and the correct ID is being sent to the view.
The above is a simplification but it does encapsulate the problem.
Thank you in advance.
UPDATE
I can get this to work if:
I only have a single entity in my model
I add ModelState.Clear(); before the save
I was running into the same issue on my project. The only way for me to resolve it was to not include the id when the it was 0. That way when it came back the id was replaced. So in your example you would do the following:
#model MyModel
#using (Ajax.BeginForm("Index", "Home",
new AjaxOptions
{
HttpMethod = "POST"
}
))
{
#Html.LabelFor(m => m.Entity1.field1)
#Html.EditorFor(m => m.Entity1.field1)
#if(Model.Entity1.ID1 !=0){
Html.HiddenFor(m => m.Entity1.ID1)
}
<div class="form-actions">
<button type="submit">
Next section</button>
</div>
}
You need to remove the value from the ModelState if you intend to modify it in your POST controller action:
ModelState.Remove("Entity1.ID1");
This way you don't need to clear the entire ModelState using ModelState.Clear but only the value you are actually modifying. This way the Html helper will pick the value from your model and not the one in the ModelState.

how to add some own forms in a ASP.NET MVC create view?

i have a problem, i had created a controller and a view for adding a new item from a specific model. the view looks like:
#modelModels.UserItem
#{
ViewBag.Title = "New";
}
<h2>New</h2>
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
<fieldset>
<legend>Device</legend>
<div class="editor-label">
#Html.LabelFor(model => model.name)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.name)
#Html.ValidationMessageFor(model => model.name)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
and the controller:
[HttpPost]
public ActionResult New(UserItem useritem)
{
if (ModelState.IsValid)
{
db.UserItems.AddObject(useritem);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(useritems);
}
how i want to add a dropdown to the form in the view like this:
<select id="Select1">
<option>MARS</option>
</select>
how to access the data from the form after it was submitted in the controller?
Have view model for your page,this view model will be used in your view. So, only include fields from your model that you really need. In Get action you should create this view model and get the needed properties from your model and map them to your view model.
public class UserItemViewModel
{
/* Properties you want from your model */
public string Property1 { get; set; }
public string Property2 { get; set; }
public string Property3 { get; set; }
/* Property to keep selected item */
public string SelectedItem { get; set; }
/* Set of items to fill dropdown */
public IEnumerable<SelectListItem> SelectOptions { get; set; }
/* Fill the SelectListHere. This will be called from index controller */
public void FillOptions()
{
var items = new[] { "Mars", "Venus" }.
Select(x => new SelectListItem { Value = x, Text = x });
SelectOptions= new SelectList(items, "Value", "Text");
}
}
Change controller for receiving ViewModel instead of Model itself.
[HttpPost]
public ActionResult New(UserItemViewModel useritem)
{
/* Repopulate the dropdown, since the values are not posted with model. */
userItem.FillOptions();
if (ModelState.IsValid)
{
/* Create your actual model and add it to db */
// TODO: Map your properties from model to view model.
// Let's say you created a model with name userItemModel
db.UserItems.AddObject(userItemModel);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(useritem);
}
You might need to change Index view controller little.(to fill dropdown)
[HttpGet]
public ActionResult Index()
{
/* Create new viewmodel fill the dropdown and pass it to view */
var viewModel = new UserItemViewModel();
viewModel.FillOptitons();
//TODO : From your model fill the required properties in view model as I mention.
return View(viewModel);
}
And your view,
/* Strongly typed view with viewmodel instead of model itself */
#modelModels.UserItemViewModel
/* This is the dropdown */
#Html.DropDownListFor(m => m.SelectedItem, Model.SelectOptions)
Add that property to your model
Use builtin EditorFor(preffered) or hand-written html to generate client-side input for that property.
Access submitted value by inspecting that property when user submits the form
I like emre's proposition of having a viewModel and I think is the Best solution to your question however just in case you don't want to go that way (you must have a really good reason because it is best) and still want a way to access the values of a form directly you can always use:
var x = Request["myFiledName"];
inside your controller to get to the values passed by your form.

In ASP.NET MVC, why does new input get persisted on postback?

This question is a bit different than most. My code works but I don't understand why it works.
I am trying to understand why changes made in the form get persisted after posting to the server.
Model:
public class TestUpdateModel
{
public int Id { get; set; }
public List<CarrierPrice> Prices { get; set; }
public TestUpdateModel() { } // parameterless constructor for the modelbinder
public TestUpdateModel(int id)
{
Id = id;
Prices = new List<CarrierPrice>();
using (ProjectDb db = new ProjectDb())
{
var carriers = (from c in db.Carriers
select c);
foreach (var item in carriers)
{
var thesePrices = item.Prices.Where(x => x.Parent.ParentId == Id);
if (thesePrices.Count() <= 0)
{
Prices.Add(new CarrierPrice
{
Carrier = item
});
}
else
Prices.Add(thesePrices.OrderByDescending(x => x.DateCreated).First());
}
}
}
}
Controller:
public ViewResult Test(int id = 1)
{
TestUpdateModel model = new TestUpdateModel(id);
return View(model);
}
[HttpPost]
public ViewResult Test(TestUpdateModel model)
{
model = new TestUpdateModel(model.Id);
return View(model); // when model is sent back to view, it keeps the posted changes... why?
}
View
#model Namespace.TestUpdateModel
#{ ViewBag.Title = "Test"; }
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Id)
#Html.EditorFor(x => x.Prices)
<input type="submit" value="Save" />
}
EditorTemplate
#model Namespace.CarrierPrice
<tr>
<th>
#Html.HiddenFor(model => model.CarrierPriceId)
#Html.HiddenFor(model => model.DateCreated)
#Model.Carrier.Name
</th>
<td>
$#Html.TextBoxFor(model => model.Fee)
</td>
</tr>
The Source of My Confusion
1) Load the page in the browser
2) Change the value of the model.fee TextBox
3) Submit
At this point I would expect that model = new TestUpdateModel(model.Id); would create a new TestUpdateModel object and wipe out my changes so the original values re-appear when the view is returned. But what actually happens is my changes in the form are persisted to the postback.
Why does this happen?
Thanks for any help.
No. The reason is that the View Engine looks in the ModelState to fill your values before it looks at the model when it renders your view. If there are posted values, then it will find those first and use them.
If you want to override this behavior, then you need to clear the ModelState first with:
ModelState.Clear();

asp.net mvc and multiple models and modelbinders

I wanna try to make this is as simple as possible.
Lets say I have a Project Model and a Task Model
I want to create a Project with 3 tasks assigned to that project in one single form
Whats the best way to do this??
Would the method simply receive a Project or what else do i need to have there.. will just saving the project (in repository) also save the related tasks?...
In the view... do i need a viewModel.. Im confused. please help
public ActionResult Create(Project p){
}
Here's how I would proceed:
public class TaskViewModel
{
public string Name { get; set; }
}
public class ProjectViewModel
{
public string ProjectName { get; set; }
public IEnumerable<TaskViewModel> Tasks { get; set; }
}
then have a controller:
public class ProjectsController: Controller
{
public ActionResult Index()
{
var project = new ProjectViewModel
{
// Fill the collection with 3 tasks
Tasks = Enumerable.Range(1, 3).Select(x => new TaskViewModel())
};
return View(project);
}
[HttpPost]
public ActionResult Index(ProjectViewModel project)
{
if (!ModelState.IsValid)
{
// The user didn't fill all required fields =>
// redisplay the form with validation error messages
return View(project);
}
// TODO: do something with the model
// You could use AutoMapper here to map
// the view model back to a model which you
// would then pass to your repository for persisting or whatever
// redirect to some success action
return RedirectToAction("Success", "Home");
}
}
and then the view (~/Views/Projects/Create.cshtml):
#model AppName.Models.ProjectViewModel
#using (Html.BeginForm())
{
<div>
#Html.LabelFor(x => x.ProjectName)
#Html.EditorFor(x => x.ProjectName)
#Html.ValidationMessageFor(x => x.ProjectName)
</div>
#Html.EditorFor(x => x.Tasks)
<input type="submit" value="Create!" />
}
and the corresponding task editor template (~/Views/Projects/EditorTemplates/TaskViewModel.cshtml):
#model AppName.Models.TaskViewModel
<div>
#Html.LabelFor(x => x.Name)
#Html.EditorFor(x => x.Name)
#Html.ValidationMessageFor(x => x.Name)
</div>
Add a collection of Task models to the Project model, and use a foreach loop to display the tasks, or repeat a partial view that knows how to display a single task.

Resources