Using MS VS 2012, C#.net, Nhibernate.
Im inserting a new row.
[HttpPost]
public ActionResult AddQuestion(ModuleAddQuestionModel model)
{
if (!_moServices.Authoriser.Authorise(AcademyPermissions.AccessDeveloperArea))
return new HttpUnauthorizedResult();
if (model == null)
return HttpNotFound();
int modelId = 0;
var module = _moduleService.GetModule(model.ModuleId);
// if null
if(model != null)
{
modelId = model.ModuleId;
}
var addQuestion = new Question()
{
Text = model.Question,
QuestionType = model.QuestionType,
Published = model.Published,
Country = module.Country,
Module = module
};
_moduleService.SaveQuestion(addQuestion);
return RedirectToAction(
"Edit",
"Module",
new { Id = modelId }
);
}
redirect calls
public ActionResult Edit(int Id) {
if (!_moServices.Authoriser.Authorise(AcademyPermissions.AccessDeveloperArea))
return new HttpUnauthorizedResult();
var module = _moduleService.GetModule(Id);
if(module == null)
return HttpNotFound();
var model = new ModuleEditModel {
Id = module.Id,
Name = module.Name,
Questions = module.Questions
.Where(x => !x.Deleted)
.Select(x => new ModuleEditModel.ModuleQuestionModel {
Id = x.Id,
Question = x.Text
}).ToList(),
Videos = module.Videos.Select(x => new ModuleEditModel.ModuleVideoModel {
Id = x.Id,
Title = x.Name
}).ToList()
};
return View(model);
}
the view displays the results, it should display a list that has the new row, but doesn't, even after f5. I suspect it was caching, but we don't have any caching involved for this whole procedure. Next step was to look into Nhibernate and caching.
the has the 2 relevent methods
public Module GetModule(int id) {
_moduleRepository.Flush();
return _moduleRepository.Get(id);
}
public void SaveQuestion(Question question) {
if (question.Id > 0)
UpdateQuestion(question);
else
InsertQuestion(question);
}
public void InsertQuestion(Question question) {
_questionRepository.Create(question);
_questionRepository.Flush();
_signals.Trigger(ModuleSignalChanged);
}
the .flush command was recently added by me to see if that would help clear the results, but didn't change anything.
looked into Isession.Clear(); but as i understand in our cms this lives outside the scope.
Im wondering what the issue is here, was i on the right tract? is this a NHibernate caching issue? Can anyone offer a solution?
If you require any additional information let me know.
ADDITIONAL INFO:
Yes the insert does populate the Database (MS SQL server 2008)
It appears after a few minutes, if one refreshes the page, the new record is displayed.
Heres the answer
var addQuestion = new Question()
{
Text = model.Question,
QuestionType = model.QuestionType,
Published = model.Published,
Country = module.Country,
Module = module
};
module.Questions.Add(addQuestion);
_moduleService.UpdateModule(module);
Instead of trying to insert a question, i updated the module instead that question is a child element of. This i assume cascades due to the relationship which forces nhibernate to clear cache and check the DB.
I cant confirm this, but this appears to at least solve this problem, if anyone can enlighten me as to why the previous method hasn't worked, ill swap my confirmed answer
Related
I'm teaching myself C# and MVC but have a background in SQL. When updating an existing master-detail set of records in a single action (let's say for instance a customer order and order details), updating the master record is no problem. Regarding the detail records, I'm seeing examples that simply delete all existing details and then add them back in rather than add, delete or update only what's changed. That seems easy and effective but involves unnecessary changes to database records and might be an issue in complex relationships.
I've tried writing code that checks the existing values against posted values to determine the right EntityState (Added, Deleted, Modified, Unchanged) for each detail. Accomplishing this using LINQ Except and Intersect works but seems to cause an unexpected performance hit.
(Instead, I could load the original values in an "oldValue" hidden field in the original GET request to compare to the POST values except that would be unreliable in a multi-user environment and seems like a bad idea.)
I'll be happy to provide code examples, but my question is more about best practices. Is there a preferred method for updating existing master-detail sets of records?
EDIT: I've added the code below in response to questions. In this example, our application allows additional attributes to be attached to a product, kept in a separate table ProductAttributes. The view allows the user to edit both the product and the attributes on the same webpage and save at the same time. The code works fine but seems slow and lags at SaveChanges.
public ActionResult Edit(Product product)
{
if (ModelState.IsValid)
{
db.Entry(product).State = EntityState.Modified;
// Establish entity states for product attributes.
List<ProductAttribute> existingAttributes = new List<ProductAttribute>();
existingAttributes = db.ProductAttributes.AsNoTracking()
.Where(x => x.Sku == product.Sku).ToList();
// Review each attribute that DID NOT previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Except(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value didn't exist and still doesn't.
db.Entry(pa).State = EntityState.Unchanged;
}
else
{
// New value exists that didn't before.
db.Entry(pa).State = EntityState.Added;
}
}
// Review each attribute that DID previously exist.
foreach (ProductAttribute pa in product.ProductAttributes
.Intersect(existingAttributes, new ProductAttributeComparer()))
{
if (pa.Value is null)
{
// Value existed and has been cleared.
db.Entry(pa).State = EntityState.Deleted;
}
else
{
if (pa.Value != existingAttributes
.FirstOrDefault(x => x.Attribute == pa.Attribute).Value)
{
// Value has been altered.
db.Entry(pa).State = EntityState.Modified;
}
else
{
// Value has not been altered.
db.Entry(pa).State = EntityState.Unchanged;
}
}
}
db.SaveChanges();
return RedirectToAction("Details", new { id = product.ProductId });
}
return View(product);
}
internal class ProductAttributeComparer : IEqualityComparer<ProductAttribute>
{
public bool Equals(ProductAttribute x, ProductAttribute y)
{
if (string.Equals(x.Attribute, y.Attribute,
StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
public int GetHashCode(ProductAttribute obj)
{
return obj.Attribute.GetHashCode();
}
}
}
When I try to update this object in EF6 I get an error stating more than 1 entity has this primary key. Looking at this DB I know this to be untrue(from what I can see).
I need to be able to update a second object based on one of the properties on the posted object. The code below produces the error. I have left in commented out pieces that I have tried to get this to work.
public async Task<ActionResult> Edit(PricingRule pricingRule)
{
if(ModelState.IsValid)
{
var currentUser = await serv.UserManager.FindByIdAsync(User.Identity.GetUserId());
var company = currentUser.Company;
//var entityRule = serv.PricingService.PricingRules.Get(pricingRule.PricingRuleId);
//If this is the first rule, set it to the company default
var rulesCount = company.PricingRules.Count;
if (rulesCount <= 1 || company.DefaultPricingRule == null)
pricingRule.DefaultPricingRule = true;
//Make sure no other rules are marked as default, and update the company with this rule as default
if (pricingRule.DefaultPricingRule)
{
if (company.DefaultPricingRule != null)
{
var oldRule = serv.PricingService.PricingRules.Get(company.DefaultPricingRule.PricingRuleId);
oldRule.DefaultPricingRule = false;
//serv.PricingService.PricingRules.Update(oldRule);
}
company.DefaultPricingRule = pricingRule;
serv.CoreService.Companies.Update(company);
}
serv.PricingService.PricingRules.Update(pricingRule);
await serv.SaveAllChangesAsync();
return RedirectToAction("Index");
}
return View(pricingRule);
}
Whether or not it is the best practice or how it should technically be done, this is how I solved my problem.
The edited object I was passing in, needed to be marked as modified first, before doing any other operations. I am assuming this is because the context could then grab it and all other operations regarding it would be done "within context". Other wise I think it was trying to add a new object if I tried to attach it to company.DefaultPricingRule.
public async Task<ActionResult> Edit(PricingRule pricingRule)
{
if(ModelState.IsValid)
{
serv.PricingService.PricingRules.Update(pricingRule);
var currentUser = await serv.UserManager.FindByIdAsync(User.Identity.GetUserId());
var company = currentUser.Company;
//If this is the first rule, set it to the company default
var rulesCount = company.PricingRules.Count;
if (rulesCount <= 1 || company.DefaultPricingRule == null)
pricingRule.DefaultPricingRule = true;
//Make sure no other rules are marked as default, and update the company with this rule as default
if (pricingRule.DefaultPricingRule)
{
if (company.DefaultPricingRule != null)
{
var oldRule = serv.PricingService.PricingRules.Get(company.DefaultPricingRule.PricingRuleId);
oldRule.DefaultPricingRule = false;
serv.PricingService.PricingRules.Update(oldRule);
}
company.DefaultPricingRule = pricingRule;
serv.CoreService.Companies.Update(company);
}
await serv.SaveAllChangesAsync();
return RedirectToAction("Index");
}
return View(pricingRule);
}
If Anyone has a comment on if this is best practice or if there is a better way to do it, I gladly take criticism.
I read tutorial "Creating an Entity Framework Data Model for an ASP.NET MVC Application" from http://www.asp.net/ and wanted to use multiple checkbox list from part 6 - http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/updating-related-data-with-the-entity-framework-in-an-asp-net-mvc-application
In this tutorial, it is only possible to access these checkbox options (to assign courses to each instrucor) from edit page of instructor. I want to render these checkboxes on the create page, but couldn't adjust codes I've found in this tutorial.
These are parts of the codes that are used to display and use checkboxes for new inputs to database.
HttpGet Edit method
public ActionResult Edit(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
PopulateAssignedCourseData(instructor);
return View(instructor);
}
private void PopulateAssignedCourseData(Instructor instructor)
{
var allCourses = db.Courses;
var instructorCourses = new HashSet<int>(instructor.Courses.Select(c => c.CourseID));
var viewModel = new List<AssignedCourseData>();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewBag.Courses = viewModel;
}
HttpPost Edit method
[HttpPost]
public ActionResult Edit(int id, FormCollection formCollection, string[] selectedCourses)
{
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.InstructorID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "", null, new string[] { "Courses" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
db.Entry(instructorToUpdate).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
PopulateAssignedCourseData(instructorToUpdate);
return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List<Course>();
return;
}
var selectedCoursesHS = new HashSet<string>(selectedCourses);
var instructorCourses = new HashSet<int>
(instructorToUpdate.Courses.Select(c => c.CourseID));
foreach (var course in db.Courses)
{
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
}
}
And the view
<div class="editor-field">
<table>
<tr>
#{
int cnt = 0;
List<ContosoUniversity.ViewModels.AssignedCourseData> courses = ViewBag.Courses;
foreach (var course in courses) {
if (cnt++ % 3 == 0) {
#: </tr> <tr>
}
#: <td>
<input type="checkbox"
name="selectedCourses"
value="#course.CourseID"
#(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
#course.CourseID #: #course.Title
#:</td>
}
#: </tr>
}
</table>
</div>
So, I want to insert multiple choices (select choices) to database, but would like to display this list of checkboxes immediately on the create page, not the edit page like it was done in the tutorial. If you have another solution that is not related to this tutorial, I would also be grateful. I know that I can't use these methods because they're methods for Edit, so they're takin "id" as parameter, but I hope that I can adjust them to be used in create page of my entity.
Consider using a partial view to isolate the control you're trying to render on both pages, you just need to make sure your model lines up with what the model for your view is.
http://mvc4beginner.com/Tutorial/MVC-Partial-Views.html
I have a silverlight mvvm with ria project. I have a UI in which admin users can enter info to create new work orders. However, I am having trouble calling the db and adding a new record to the table. I have no code-behind for the UI, the controls are tied to the model through Commands and Command Parameters. So when a user clicks, 'Add new job' it comes here,
public class EditJobViewModel : ViewModelBase
{
private Job _job;
public Job CurrentJob
{
get { return _job; }
set
{
_job = value;
OnPropertyChanged("CurrentJob");
}
}
public ICommand NewJob
{
get
{
return new DelegateCommand(BeginNewJob, (o) => true);
}
}
public void BeginNewJob(object o)
{
_job = new Job();
//_job.JobNumber = _job.JobID.ToString();
_job.AssignedTo = App.userID;
_job.AddedBy = App.userID;
_job.FileTypeJob = "PDF";
_job.AddedTS = DateTime.Now;
_job.OpenDate = DateTime.Now;
BeginSave(o);
}
}
Where Im having trouble is creating a new record in the 'Job' table. On my breakpoint it returns all the columns it needs to, just not a new 'JobID' which is my primary key. This is how I was supposedly trying to create a new record.
public void BeginSave(object o)
{
if (!IsDesignTime)
{
try
{
if (CurrentJob.EntityState == EntityState.New)
{
CurrentJob.AddedBy = App.userID;
CurrentJob.AddedTS = DateTime.Now;
}
CurrentJob.UpdatedBy = App.userID;
CurrentJob.UpdatedTS = DateTime.Now;
// This is here because of a bug in infragistics grid/Entity Framework
foreach (JobFileType ft in CurrentJob.JobFileTypes)
{
if (ft.EntityState != EntityState.Unmodified)
(ft as IEditableObject).EndEdit();
}
foreach (JobTag tag in CurrentJob.JobTags)
{
if (tag.EntityState != EntityState.Unmodified)
(tag as IEditableObject).EndEdit();
}
//(CurrentJob as IEditableObject).EndEdit();
SubmitOperation s = _context.SubmitChanges();
if (s.HasError)
{ }
}
catch (Exception ex)
{ }
}
}
Except that it never hits the EntityState.new. That's just the way I thought to try it. Im thinking there a way to do it from the 'BeginNewJob' command but unable to find a way to create a new JobID or record in general. The Database already has 10000 records and has multiple users creating jobs, so I need a way to get the last job created (getMaxID()??) and increment appropriately, creating a new job on the spot.
İf you use guid type for id column, you will not need to find next id and this approach will decouple new objects from previous objects.
Currently I am attempting to add a new row to a database table through AJAX which is working fine. But then I try to update a different table and I get an error. Here is my code and the error I am encountering.
Error
The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state.
Line 41: _db.ChampionCounters.Attach(champion);
Code
[HttpPost]
public ActionResult VoteYes(int id)
{
string results;
if (Request.IsAuthenticated)
{
var checkFirst =
from c in _db.UserCounterLinks
where c.counterId == id && c.userName == User.Identity.Name
select c;
if (checkFirst.Any())
{
results = "You have already voted on this counter.";
return Json(results);
}
var userVoteLink = new UserCounterLink { counterId = id, userName = User.Identity.Name, userAgree = true };
_db.UserCounterLinks.AddObject(userVoteLink);
var champion = _db.ChampionCounters.SingleOrDefault(c => c.id == id);
if (champion != null)
{
champion.positiveVotes++;
_db.ChampionCounters.Attach(champion);
}
_db.SaveChanges();
results = "Voted";
} else
{
results = "You must be logged in to vote.";
}
return Json(results);
}
Summary
The code above is from the controller that handles the Ajax post. Like I said the userVoteLink table creates a record just fine. But when I try to update the other table ChampionCounters the error is thrown.
Thanks in advance!
You don't need to attach the instance because the context is already tracking that instance. Just remove the _db.ChampionCounters.Attach(champion); line.