I’m confused on how to work with updating two entities while using a ViewModel. I’ve done some searching but I’m not coming up with anything that seems to help me understand this.
I have two entities Person and Address. I use a ViewModel to combine the two together in order to display them in my Edit.cshtml. At the top of my View I declare the ViewModel to be used.
Now, I get to the actual Edit portion. I’m assuming I would have to make an update to each entity and then create a new ViewModel to return to the View since the Veiw is expecting a ViewModel?
I’m also curious if there is a way to let the View know that the update was successful so I can display a message at the top of the View or if it would be better to just redirect to the Index View.
Is this a correct way of coding this or is there a more streamlined way to accomplish the same thing?
Top of my .cshtml page
#model Project.Models.MemberViewModel
Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult MyAccount(MemberViewModel model)
{
if (model.PersonId == 0)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
MemberViewModel updatedVM = new MemberViewModel();
using (var db = new DataContext())
{
Person currentPerson = db.Person.Find(model.PersonId);
if (currentPerson == null)
{
return HttpNotFound();
}
db.Entry(currentPerson).State = System.Data.Entity.EntityState.Modified;
db.SaveChanges();
Address currentAddress = db.Address.Find(model.PersonId);
if (currentAddress == null)
{
return HttpNotFound();
}
db.Entry(currentAddress).State = System.Data.Entity.EntityState.Modified;
db.SaveChanges();
////update and return a ViewModel
//updatedVM.FirstName = currentPerson.FirstName;
//updatedVM.LastName = currentPerson.LastName;
//updatedVM.Address = currentAddress.Address1;
//updatedVM.City = currentAddress.City;
//updatedVM.State = currentAddress.State;
//updatedVM.Zip = currentAddress.Zip;
}
return View(updatedVM);
}
In my opinion, the best practice would be to redirect to Index.cshtml after successfully saving the data (I think this is the whole idea of keeping MVC clean):
return RedirectToAction("Index");
I would only show an error message if there is trouble with the data.This can be done similar to this:
You can add these to the ViewModel:
bool hasErrors;
string errorMessage;
and in the view you would implement at the top of the page:
#if(Model.hasErrors)
{
<div>Model.errorMessage</div>
}
The idea is to validate the data inside the ViewModel constructor, or inside the controller, and if there is something wrong just set hasErrors = true and a custom errorMessage and show it in the view.cshtml.
Hope it helps.
Related
I am creating an CRUD Application in Asp.Net Core
After Add Operation I am redirecting to same view with setting model value as null to get another entry
Below is my code
public IActionResult Add(OptionMasterVM model)
{
try
{
model.QuestionList = context.QuestionMaster.Select(x => new SelectListItem { Text = x.QuestionName, Value = x.QuestionId.ToString() }).ToList();
if (HttpContext.Request.Method == "POST")
{
OptionMaster _optionmaster = new OptionMaster();
_optionmaster = model.OptionMaster;
using (var ctx = new QuestionnaireEntities(_configuration))
{
ctx.OptionMaster.Add(_optionmaster);
ctx.SaveChanges();
}
TempData["Msg"] = "Option Added Successfully , Add Another Option";
model.OptionMaster.OptionValue = string.Empty;
model.OptionMaster.OptionRating = 0;
return View(model);
}
}
catch (Exception ex)
{
logger.LogError(ex);
}
finally
{
}
return View(model);
}
Here I am setting Option Value to empty and rating to Zero to take next entry , but on view it does not show empty and zero , on view it show previously filled value.
After Setting below code these two fields should be reset but they don't
model.OptionMaster.OptionValue = string.Empty;
model.OptionMaster.OptionRating = 0;
Is there any other way to set model object as null in Asp.net Core ?
This can happen because Razor helpers use values from ModelState, rather than the model itself. Your OptionValue is probably displayed using a helper, for example:
#Html.TextBoxFor(m => m.OptionMaster.OptionValue)
When you change model values within an action, you need remove the old values from ModelState before rendering the View.
The easiest way of doing this is to call ModelState.Clear()
model.OptionMaster.OptionValue = string.Empty;
model.OptionMaster.OptionRating = 0;
ModelState.Clear(); // ensure these changes are rendered in the View
return View(model);
The values displayed for bound form fields come from ModelState, which is composed based on values from Request, ViewData/ViewBag, and finally Model. After posting, obviously, you'll have values set in Request, which will therefore be the values in ModelState. It works this way, so that when there's a validation error and the user is returned to the form to correct their mistakes, the values they posted will be there for them to edit.
Long and short, you need to follow the PRG (Post-Redirect-Get) pattern. Essentially, after posting, you only return the view on error. If the post is successful, you redirect. This not only clears ModelState, but also prevents accidental re-posts if the user attempts to refresh the page.
If you want to take the user back to the same view, simply redirect to the same action, but you need to do a redirect, not return the view.
I have a problem transfering data from one view to another via the controler actions.
I the first view is a grid.mvc-Grid displayed. By select on row of the grid I get the ID for that object.
by transfering this to an action in the controler I try to filter the data. That works fine.
Here is the filter:
[HttpGet]
public ActionResult PersonenById(int id)
{
var personen = new ObservableCollection<Person>();
//Getting the data here :-)
foreach (DataRow r in access.Rows)
{
Person p = new Person();
//do some stuff
personen.Add(p);
}
//return PartialView("Personen", personen); //does not work
TempData["personen"] = personen;
return RedirectToAction("Personen"); // redirect to another view
}
In method II the view is filled:
public ActionResult Personen()
{
var persons = new ObservableCollection<Person>();
if (TempData["Persons"] == null)
{
}
return View(persons); //Works perfect
}
else
{
persons = (ObservableCollection<Person>) TempData["Persons"];
return View(persons);//does not redirect to that View
}
}
(Sorry for the strange formating. :-))
Is there any different way to send data from a view to another?
I tried:
return partial;
return View("Persons",persons);
and a lot other stuff.
You can redirect in a .cshtml view.
Eg:
Context.Response.StatusCode = 403;
Context.Response.Redirect(
$"{Context.Request.PathBase}/Error/403", false);
Should work like this:
return RedirectToAction("Personen", model);
Also, the "Personen" action should have the model as an argument, like this:
public ActionResult Personen(Person model) ...
LE: I have also noticed you have tried to send the data through the TempData object. Make sure the indexed object's name is the same (e.g. TempData["person"] everywhere)
Hope it answers your question.
Preface:
I'm working on a project using MVC 5. We've built up our database from which our model is created. Then, we've added the Controllers and Views using Scaffolding. However in the Views we've omitted some of the properties (they should not be shown to the users).
Problem:
In the actions like Edit, as I hit the save button (to update the model) I encounter an exception and I think it requires me to provide the value of all the properties.
Please let me know how can I update just some of the properties (shown in the view)?
Please note that I need a solution as general as possible, so as to be able to use it in many Edit actions that I have in this project (Indeed this is the hard part).
Code:
The following are some of the codes that I think are most related to this question:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "AreaCode,Tels,Address")] Area area)
{//Area has many more properties (defined as required in the database) and I just need to update
//these : AreaCode,Tels,Address
if (ModelState.IsValid)
{
db.Entry(area).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.Detector = new SelectList(db.Detectors, "DetectorCode", "detectorName", area.Detector);
ViewBag.Grade = new SelectList(db.Grades, "Gradeid", "GradeName", area.Grade);
return View(area);
}
SS:
Answers expressed in a simple way are highly appreciated.
Get your object to update from the database, and sets your model with the old value of properties
public ActionResult Edit([Bind(Include = "AreaCode,Tels,Address")] Area area)
{
YourDbContext _db= new YourDbContext();
Area oldArea = _db.Areas.Where(x => x.ID == area.ID).FirstOrDefault();
// Bind others properties marked with required from database
area.x = oldArea.x;
area.y = oldArea.y;
if (ModelState.IsValid)
{
db.Entry(area).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.Detector = new SelectList(db.Detectors, "DetectorCode", "detectorName", area.Detector);
ViewBag.Grade = new SelectList(db.Grades, "Gradeid", "GradeName", area.Grade);
return View(area);
}
I'm trying to use ViewModels and AutoMapper, as I know these are best practice for avoiding lots of issues.
I'm ok using AutoMapper to populate a viewmodel, but I'm not as sure about how to update my database, from the ViewModel being posted back to my controller.
My GET is:
public ActionResult Edit(int id = 0)
{
Customer customer = db.Customers.Find(id);
var offers = db.Offers.Where(x => x.CustomerId == id).ToList();
var email = db.Emails.FirstOrDefault();
var vm = new CreateViewModel();
vm.CustomerId = customer.CustomerId;
vm.ArrivalDate = customer.ArrivalDate;
vm.CustomerName = customer.CustomerName;
vm.EmailAddress = customer.EmailAddress;
vm.NumNights = customer.NumNights;
vm.NumPeople = customer.NumPeople;
vm.EmailBody = email.EmailBody;
vm.From = email.From;
vm.Subject = email.Subject;
// Map list of Offers into ViewModel
vm.Offers = Mapper.Map<IList<Offer>, IList<OfferVM>>(offers);
return View(vm);
}
My POST is:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(EditViewModel vme)
{
if (ModelState.IsValid)
{
// Update the Customer properties
Customer customer = db.Customers.Find(vme.CustomerId);
customer.NumPeople = vme.NumPeople;
customer.NumNights = vme.NumNights;
customer.ArrivalDate = vme.ArrivalDate;
customer.CustomerName = vme.CustomerName;
customer.EmailAddress = vme.EmailAddress;
// Update Offers table
foreach (var o in vme.Offers)
{
// find the offer
Offer offer = db.Offers.Find(o.OfferId);
if (offer != null)
{
// update the properties of Offer
offer.RoomRate = o.RoomRate;
offer.IncludeInOffer = o.IncludeInOffer;
}
}
db.SaveChanges();
return RedirectToAction("Index");
}
return View(vme);
}
So my Post is manually updating two database tables (Offers and Customers).
The GET method is elegant, using AutoMapper, the POST is not. I'm wondering if there is a more straightforward way of updating the database via AutoMapper, without having to manually go through each property I am looking to update? Or is my POST controller as efficient as it can be?
Thank you for any pointers,
Mark
There is an overload of Map that allows to map properties to one preexisting instance:
Mapper.Map<IObjectA, IObjectB>(objectA, ObjectB);
Thus, you just have to map from the parameter vme (CreateViewModel vme) to the customer recovered from the DB. This also applies to the offers part.
Of course, you'll have to configure the mappings in the reverse direction: you've done it from db objects to view model, and now you need to map them from viewmodel to db objects. If you've followed Automapper conventions it will be pretty easy or even unnnecessary.
EDIT: added interesting comment by Henk Mollema
You can configure AutoMapper to ignore properties, if your ViewModel doesn't contain all the properties from your domain model (which is most likely the case), it won't overwrite them with nulls.
You first need to define your mapping like this:
Mapper.CreateMap<EditViewModel, Customer>();
Your updated action method could look like this. What happens here is your action method receives your view model and checks its validity. If it is valid then it does the mapping from your view model to your domain model. This domain model is then passed to your service or repository layer to update the cusotmer in the database. How you update the record depends entirely on you.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(EditViewModel viewModel)
{
if (!ModelState.IsValid)
{
return View(viewModel);
}
Customer customer = Mapper.Map<Customer>(viewModel);
customerService.Edit(customer);
return RedirectToAction("List");
}
I hope this can help guide you in the right direction. Your HttpGet action method will work in a similar way.
I have been introduced to Razor as applied with MVC 3 this morning, so please forgive me if my question seems terribly uninformed!
I am working with an app whose workflow involves allowing a user to select a value (warehouse) from a drop down list, and add a record (material) from that warehouse to another record (Materials Request). Once the first material has been added to the Materials Request, I need to permanently set the value of the drop down to the warehouse that was first selected, then disable the drop down control (or set to read only, perhaps). The existing code in the razor file uses the DropDownListFor() method, including a ViewBag collection of Warehouse records. I have seen discussions which suggest abandoning the ViewBag design, but honestly I don't have the desire to rewrite major portions of the code; at least it looks like a major rewrite from the perspective of my experience level. Here's the original code:
#Html.LabelPlusFor(m => m.WarehouseId, "*:")
#Html.DropDownListFor(m => m.WarehouseId, (IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "")<br />
I believe I have been able to select a value based on a session object, though I'm still not sure how to disable the control. Here's my change:
#{
int SelectedWarehouseId = -1;
if (HttpContext.Current.Session["SelectedWarehouseId"] != null)
{
SelectedWarehouseId = Int32.Parse(HttpContext.Current.Session["SelectedWarehouseId"].ToString());
}
}
#Html.LabelPlusFor(m => m.WarehouseId, "*:")
#{
if (SelectedWarehouseId > -1)
{
#Html.DropDownListFor(m => m.WarehouseId, new SelectList((IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "WarehouseId", "WarehouseDescription", (int)SelectedWarehouseId))<br />
}
else
{
#Html.DropDownListFor(m => m.WarehouseId, (IEnumerable<SelectListItem>)ViewBag.WarehouseCodes, "")<br />
}
}
When the material is added to the Material Request, the WarehouseId is passed to the controller and I can access that value as "model.WarehouseId" in the controller class. However, I'm not sure how to get that value back to the View (apologies for the large code block here):
[HttpPost]
[TmsAuthorize]
public ActionResult Create(ItemRequestViewModel model)
{
string deleteKey = null;
//Removed code
else if (Request.Form["AddToRequest"] != null)
{
// If the user clicked the Add to Request button, we are only
// interested in validating the following fields. Therefore,
// we remove the other fields from the ModelState.
string[] keys = ModelState.Keys.ToArray();
foreach (string key in keys)
{
if (!_addToRequestFields.Contains(key))
ModelState.Remove(key);
}
// Validate the Item Number against the database - no sense
// doing this if the ModelState is already invalid.
if (ModelState.IsValid)
{
_codes.ValidateMaterial("ItemNumber", model.ItemNumber, model.WarehouseId);
Session["SelectedWarehouseId"] = model.WarehouseId;
}
if (ModelState.IsValid)
{
// Add the new Item Request to the list
model.Items.Add(new ItemViewModel() { ItemNumber = model.ItemNumber, Quantity = model.Quantity.Value, WarehouseId = model.WarehouseId });
ModelState.Clear();
model.ItemNumber = null;
model.Quantity = null;
model.WarehouseId = null;
}
}
//Removed code
return CreateInternal(model);
}
private ActionResult CreateInternal(ItemRequestViewModel model)
{
if (model != null)
{
if (!String.IsNullOrEmpty(model.SiteId))
{
ViewBag.BuildingCodes = _codes.GetBuildingCodes(model.SiteId, false);
if (!String.IsNullOrEmpty(model.BuildingId))
ViewBag.LocationCodes = _codes.GetLocationCodes(model.SiteId, model.BuildingId, false);
}
//Removed code
}
//Removed code
ViewBag.WarehouseCodes = _codes.GetWarehouseCodes(false);
return View("Create", model);
}
So my questions are, how do I disable the drop down list, and how can I pass a value for the selected WarehouseId back to the view? I've also considered adding the value to the ViewBag, but to be honest I don't know enough about the ViewBag to recognize any unintended consequences I may face by just randomly modifying it's contents.
Thanks for any help offered on this.
Without going into which approach is better...
Your dropdown should be rendered as an HTML select element, in order to disable this you'll need to add a disabled="disabled" attribute to it.
The DropDownListFor method has a htmlAttributes parameter, which you can use to achieve this:
new { disabled = "disabled" }
when your pass model to your view like
return View("Create", model);
if WareHouseID is set in model then
Html.DropDownListFor(x=>x.WareHouseID, ...)
will automatically set the selected value and u don't have to do that session processing for this. So far as disabling a field is required, stewart is right. you can disable drop down this way but then it won't be posted to the server when u submit the form. you can set it to readonly mode like
new{#readonly = "readOnly"}