Changes to DbContext not saved - MVC Entity framework - c#

I have created a database and then an MVC solution using VisualStudio Exrpess 2013 for WEB and
I created a ViewModel to display an Edit view with dropdown lists.
The view model includes just the property 'parlists' of type PartnerList, which is the model representing the main table of the database, and 2 properties of type SelectList which I use to create the dropdown lists in the view.
The code of the viewmodel is as follows:
public class FileStatusEdit
{
public SelectList HoldingsStatus { get; set; }
public SelectList RealGainStatus { get; set; }
public PartnerList parlists { get; set; }
}
In the controller I have the following code for the HttpGet edit method:
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var viewModel = new FileStatusEdit
{
HoldingsStatus = new SelectList(db.Statuses, "Status_ID", "Status1", db.PartnerLists.Where(p => p.IntermediaryID == id).Single().AssetDataSource.HoldingsFile.Status_ID),
RealGainStatus = new SelectList(db.Statuses, "Status_ID", "Status1", db.PartnerLists.Where(p => p.IntermediaryID == id).Single().AssetDataSource.RealGainFile.Status_ID),
parlists = db.PartnerLists
.Include(p => p.AssetDataSource)
.Where(p => p.IntermediaryID == id)
.Single()
};
if (viewModel.parlists == null)
{
return HttpNotFound();
}
return View(viewModel);
}
This code is working fine and the view is correctly displaying a form with the dropdown lists. I omit the view code as it is quite long and propably not relevant.
So far so good.
My Http Post Edit method, however, is not saving the changes to the database. The code is as follows:
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(FileStatusEdit newParList)
{
if (TryUpdateModel(newParList.parlists, "",
new string[] { "Firstname", "Surname", "Category", "ClientID", "IntermediaryID", "ExternalRef", "RecordStatus", "Asset_Data_Source_ID", "New_Communication_Issued", "AssetDataSource", "HoldingsFile", "RealGainFile"}))
{
try
{
db.Entry(newParList.parlists).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException)
{
ModelState.AddModelError("", "Unable to save changes.");
}
}
return View(newParList);
}
As you can see I am passing the viewmodel (newParList) to the EditPost method and then I update it using TryUpdateModel. By stepping into the debugging process I can see that the database record newParList.parlists is correctly updated with the user input, however when the step db.SaveChanges() is executed the program redirects to the Index view without saving the changes to the database.
I tried using Attach as suggested in some posts but I believe the attach step is already included in the line 'db.Entry(newParList.parlists).State = EntityState.Modified;' and that did not indeed solve the problem.
I examined a lot of posts and tried different solutions but none of them worked so I would appreciate some help.

I suspect you're missing the context add or update.
Here's an example of how I handle the creation of a new record.
For an update you would find the record first then save the changes.
public void SaveCreatedMessage(Message message)
{
var dbEntry = _context.Message.Add(message);
if (dbEntry != null)
{
// Create the new record
dbEntry.CustomerID = message.CustomerID;
dbEntry.MessageID = message.MessageID;
dbEntry.Description = message.Description;
dbEntry.Text = message.Text;
dbEntry.IsRead = message.IsRead;
dbEntry.CreatedOn = message.CreatedOn;
dbEntry.CreatedBy = message.CreatedBy;
_context.Message.Add(message);
}
_context.SaveChanges();
}

I think I found the solution. I was not updating the correct entity.
In my HttpPost Edit method I now replaced the following line:
db.Entry(newParList.parlists).State = EntityState.Modified;
with:
db.Entry(newParList.parlists.AssetDataSource.HoldingsFile).State = EntityState.Modified;
db.Entry(newParList.parlists.AssetDataSource.RealGainFile).State = EntityState.Modified;
Now my entities HoldingsFile and RealGainFile are updated after SaveChages() is executed.

Related

ASP.NET MVC5 problem in passing data between Controllers

I am building a basic Car Rental Application. The user can view the cars and click the Rent button. After clicking it, I need to return a new View which contains a form, that the user has to complete in order to finish the order. I am having problems passing the Car data as well as the Customer data between the controllers in order to complete the Rent.
On the main page, I have a Rent link under every car. Here is the code:
<div class="col-md-12">
<p>#Html.ActionLink("Rent", "Rent" , new { Id = car.Id})</p>
</div>
Rent method from HomeController
public ActionResult Rent(string id)
{
return RedirectToAction("Create", "Rents");
}
Create method from RentsController
[HttpPost]
public ActionResult Create(string carId, Rent rent)
{
if (!ModelState.IsValid)
return View();
var carToRent = context.Cars.SingleOrDefault(c => c.Id == carId);
if (carToRent == null)
return Content($"Car not found!");
rent.Car = carToRent;
var customer = context.Customers.SingleOrDefault(c => c.UserId == User.Identity.Name);
if (customer == null)
return Content($"Customer not found!");
rent.Customer = customer;
context.Rents.Add(rent);
context.SaveChanges();
return RedirectToAction("Index");
}
I am getting an HTTP 404 Error every time I try to access Rents/Create.
You can simplify what you're attempting to do. Main points to note are the following:
You don't need to link to the Rent action if all it does is
redirect to the Create action- just link to the Create action
directly. There is another overload of ActionLink that will let you specify
the controller (see below).
From what you've posted it doesn't look like the Create action
needs to take in a parameter for Rent rent- this can be created
inside the Create action and simplify the data that you need to
pass from view to controller.
Please see my comments in code for further explanantion:
View:
//call the Create action on the RentsController directly from the view
<div class="col-md-12">
<p>#Html.ActionLink("Rent", "Create", "Rents" , new { Id = car.Id }, null)</p>
</div>
Controller:
//modify signature to remove passing a Rent object it
//you can create this object inside of this method
//and do not need to pass one in so remove it from the method signature
[HttpPost]
public ActionResult Create(string carId)
{
if (!ModelState.IsValid)
return View();
var carToRent = context.Cars.SingleOrDefault(c => c.Id == carId);
if (carToRent == null)
return Content($"Car not found!");
var rent = new Rent(); //this line has been added since the method signature was changed
rent.Car = carToRent;
var customer = context.Customers.SingleOrDefault(c => c.UserId == User.Identity.Name);
if (customer == null)
return Content($"Customer not found!");
rent.Customer = customer;
context.Rents.Add(rent);
context.SaveChanges();
return RedirectToAction("Index");
}
and finally you can remove the following:
//delete this action entirely, if youre doing nothing other than redirecting
//to an action then just link directly to the action you want
//notice the ActionLink in the view is modified to hit the Create action directly
public ActionResult Rent(string id)
{
return RedirectToAction("Create", "Rents");
}
As you can see below you can pass parameters in RedirectToAction() method.
RedirectToAction(String, String, RouteValueDictionary)
Redirects to the specified action using the action name, controller name, and route values. Try to redirect Create action with the carId and Rent object.
I dont know using multiple post object, but you can post one post object like that
public class MyPostObject
{
public string carId { get; set; }
public Rent rent{ get; set; }
}
and post it like that
[HttpPost]
public ActionResult Create(MyPostObject myPostObject)
{
string carId=myPostObject.carId;
Rent rent = myPostObject.rent;
....
}
UPDATE : Or you can use multiple post object with Ajax
$("#btnSave").on('click', function () {
var url = '#Url.Action("Create", "Rent")';
//Rent class properties
var data=
{
Brand: 'Renault',
Model: 'Megan',
};
$.ajax({
url:url,
type:"POST",
data:{
carId:'12',
rent:data
},
datatype:'json',
ContentType:'application/json;utf-8'
}).done(function(resp){
alert('Success ' +resp);
}).error(function(err){
alert("Error " + err.status);
});
});
As mentioned in the comments, you will have to pass the required parameters into the redirect statement.
public ActionResult Rent(string id)
{
Rent rentItem = new Rent();
return RedirectToAction("Create", "Rents", new { carId = id, rent = rentItem});
}
You either have not passed the parameters or you are missing the below method if you are looking to return a view with your redirect
public ActionResult Create()
{
return View();
}

Post IEnumerable collections from view in mvc

I am developing an ASP.NET MVC application, populating dropdownlist to view working fine in get method. I am populating to view like this in my controller
[HttpGet]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, int i = 0)
{
employeeSuperClass.FamilyDetailsFields = new FamilyList();
employeeSuperClass.FamilyDetailsFields.familyMembersList.Insert(0, new EmployeeFamilyTable());
*employeeSuperClass.FamilyDetailsFields.employee_RelationTable = dt.GetRelations();*
*employeeSuperClass.FamilyDetailsFields.employee_BloodGroupTable = dt.GetBloodGroups();*
*employeeSuperClass.FamilyDetailsFields.employee_NationalityTable = dt.GetNationalities();*
return View("CreateEmployeeFamilyDetails", employeeSuperClass);
}
Please look into starred lines
In case if there is any errors in model am getting null reference
In post action method look like this
[HttpPost]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, string Command)
{
if (ModelState.IsValid)
{
return("some view");
}
else
{
return view(employeeSuperClass);
}
}
I know again we have to create instance to populate dropdownlist this is rubbish to do same again and again
Can anyone explain how to store dropdown list collection in view separately and post them also with model?
(note: employee_relationTable is IEnumerable collection and it is a relationtable type this table contains relation id and relationname fields and am using this table in this class like below
public IEnumerable<EmployeeRelationTable> employee_RelationTable { get; set; }
For rest also am using same approach
Can we post employee_RelationTable from view and how?
Please help and your help would be greatly appreciated
Assuming dt is a member field available to all methods, what you can do is DRY up the population of the DropDowns in the View Model in a separate method, e.g.
private void PopulateDropDownsOnViewModel(EmployeeSuperClass model)
{
model.FamilyDetailsFields = new FamilyList
{
employee_RelationTable = dt.GetRelations(),
employee_BloodGroupTable = dt.GetBloodGroups(),
employee_NationalityTable = dt.GetNationalities()
}
}
Which can be used in the Get:
[HttpGet]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, int i = 0)
{
PopulateDropDownsOnViewModel(employeeSuperClass);
return View("CreateEmployeeFamilyDetails", employeeSuperClass);
}
and in the Post (and any other controller actions which need the drop downs)
[HttpPost]
public ActionResult CreateEmployeeFamilyDetails(EmployeeSuperClass employeeSuperClass, string Command)
{
if (ModelState.IsValid)
{
return("some view");
}
else
{
PopulateDropDownsOnViewModel(employeeSuperClass);
return View(employeeSuperClass);
}
}
If the drop downs are static, you can also look at caching these to prevent wasted IO to the database.
But no, don't serialize the the data in the View somehow (remember WebForms ViewState?) or fetch data from the View directly - this violates the MVC paradigm - the controller is responsible for providing data for the View to render.

Persisting Dropdown information when ModelState is not valid

I'm having some issues with my DropDownLists, because when I post the information and my Model is not valid it comes back "empty" to the page triggering an error exactly like this question.
I've used the solution proposed there and it fixed my problem. Anyway, I wanted to avoid querying the database every time my ModelState is not valid and I came with this approach. I would like to know if it is valid or if there are better ways to do it now, considering that instead of MVC2 (which was the MVC version from the question) I'm now using MVC 5, maybe they added something new to tackle this.
What I've done was to use the TempData to persist the information when my model is not valid.
public class ViewModel
{
[DisplayName("Project")]
public int ProjectID { get; set; }
public List<SelectListItem> Projects { get; set; }
//Other fields
}
Now my Create() Action (that populates Projects)
[HttpGet]
public ActionResult Create()
{
ViewModel vmodel = new ViewModel();
vmodel.Projects = db.GetProjects(User.Identity.Name).Select(x => new SelectListItem { Text = x.Description, Value = x.Id }).ToList();
TempData["Projects"] = vmodel.Projects;
return View(vmodel);
}
And my post would be like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(ViewModel vmodel)
{
//Clear TempData (in theory will clear my tempdata when read, so if this controller redirects to another action my tempdata will be clear)
List<SelectListItem> projects = (TempData["Projects"] as List<SelectListItem>);
if (ModelState.IsValid)
{
//...
}
//If it got here it's going back to the screen.
//Repopulate the TempData (allowing it to exist one more trip)
TempData["Projects"] = projects;
vmodel.Projects = projects
return View(atendimento);
}
Is this approach a good one? Is there a better way to achieve that without querying the database every single time?
Thanks a lot!
You don't need to use TempData at all as you have a property in your view model to hold the dropdown items.
public ActionResult Create()
{
ViewModel vmodel = new ViewModel();
vmodel.Projects = GetProjects();
return View(vmodel);
}
private List<SelectListItem> GetProjects()
{
return db.GetProjects(User.Identity.Name)
.Select(x => new SelectListItem { Text = x.Description,
Value = x.Id }).ToList();
}
And in the view
#Html.DropDownListFor(s=>s.ProjectID,Model.Projects)
And in your HttpPost action, If ModelState is not valid, Reload the Projects collection again (because http is stateless)
if(ModelState.IsValid)
{
// to do :Save and redirect
}
model.Projects = GetProjects();
return View(model);
You may cache the Projects so that you do not need to hit the database every time, if you are too much worried about performance.
Personally, I wouldn't worry about querying the database each time for this kind of operation.
What if projects are added/deleted? This could be the reason the save failed (selected project deleted) and the user would never realise it.
I usually write a method to populate all of my view model's SelectListItems and then use this in my Get and in my Post if the validation fails.

Passing newly created object to an another page

I'm working on a mvc4 project and I cant seem to figure out how to pass a newly created object from one page to another page.
on page A
im submitting a form to the database which is creating a new object in placing it into the table.
but after the form is submitted I want a confirmation page to appear afterwards. I dont know how to pull the newly created object from the previous page that saved it to the database. I could call it with the Id that was made but I dont know how to call it without knowing the id
public ActionResult Pay(int id,Paid paid)
{
PaidAdapter cAdapter = new PaidAdapter();
paid.CId = id;
if (ModelState.IsValid)
{
cAdapter.StorePaid(paid);
return RedirectToAction("SubmitPayment");
}
return View(paid);
}
public ActionResult SubmitPayment(int id)
{
var cAdapter = new PaidAdapter();
var model = cAdapter.GetPaidViewModel(id);
return View(model);
}
public Paid StorePaid (Paid paid)
FContext db = new FContext();
paid= db.Paid.Add(paid);
db.SaveChanges();
//return the paidId
return paid;
}
public PaidViewModel GetPaidViewModel(int id)
{
var model = new PaidViewModel();
FContext db = new FContext();
model.Paid= db.Paid.Where(c => c.PaidId == id).FirstOrDefault();
return model;
}
Use the overload of RedirectToAction that accepts route parameters. Assuming you're using the default route and it has an id parameter the code would be:
return RedirectToAction("SubmitPayment", new { id = paid.PaidId });

How to Create an Action Method invoking two tables

Thanks in advance for your help. I cannot move forward creating or editing fields in asp.net mvc using two tables or more. As an example: I´ve been using MVC Music Store example to learn
//
// GET: /StoreManager/Create
public ActionResult Create()
{
var viewModel = new StoreManagerViewModel
{
Album = new Album(),
Genres = storeDB.Genres.ToList(),
Artists = storeDB.Artists.ToList()
};
return View(viewModel);
}
//
// POST: /StoreManager/Create
[HttpPost]
public ActionResult Create(Album album)
{
if (ModelState.IsValid)
{
//Save Album
storeDB.AddToAlbums(album);
storeDB.SaveChanges();
return RedirectToAction("Index");
}
// Invalid – redisplay with errors
var viewModel = new StoreManagerViewModel
{
Album = album,
Genres = storeDB.Genres.ToList(),
Artists = storeDB.Artists.ToList()
};
return View(viewModel);
}
This example takes into consideration that Album is only one table at the model. My main question resides in the way I have to code in order to include another table with FK to Album. As an example if AlbumVariant table - eg: field albumid- displays a FK to Album Table - eg: field albumid-.
I´ve been trying with
[Authorize]
public ActionResult Create(int? id)
{
var albumCreate= db.Album.Include(AlbumVariant)SingleOrDefault(a => a.AlbumId == id);
var viewModel = new AlbumViewModelCreate()
{
AlbumCreate = albumCreate,
};
}
but with no success. Any Help will be appreciated. brdgs
You have to understand that AlbumVariant table takes AlbumId as a foreign key that means AlbumId has to be generated first! You can try this:
storeDB.AddToAlbums(album);
storeDB.SaveChanges();
AlbumVariant av = new AlbumVariant {
...
AlbumId = album.Id //generates after calling the 1st save changes
}
storeDB.AddToAlbumVariant(av);
storeDB.SaveChanges();

Categories