1 8_Updating Related Data with the Entity Framework in an ASP.NET MVC Application Mon Jul 06, 2015 8:11 am
Admin
Admin
In the previous tutorial you displayed related data; in this tutorial you'll update related data. For most relationships, this can be done by updating either foreign key fields or navigation properties. For many-to-many relationships, the Entity Framework doesn't expose the join table directly, so you add and remove entities to and from the appropriate navigation properties.
The following illustrations show some of the pages that you'll work with.
When a new course entity is created, it must have a relationship to an existing department. To facilitate this, the scaffolded code includes controller methods and Create and Edit views that include a drop-down list for selecting the department. The drop-down list sets the
In CourseController.cs, delete the four
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID")]Course course)
{
try
{
if (ModelState.IsValid)
{
db.Courses.Add(course);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Course course = db.Courses.Find(id);
if (course == null)
{
return HttpNotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var courseToUpdate = db.Courses.Find(id);
if (TryUpdateModel(courseToUpdate, "",
new string[] { "Title", "Credits", "DepartmentID" }))
{
try
{
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
return View(courseToUpdate);
}
private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
var departmentsQuery = from d in db.Departments
orderby d.Name
select d;
ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
}
Add the following
using System.Data.Entity.Infrastructure;
The
The
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
The
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Course course = db.Courses.Find(id);
if (course == null)
{
return HttpNotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
The
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
This code ensures that when the page is redisplayed to show the error message, whatever department was selected stays selected.
The Course views are already scaffolded with drop-down lists for the department field, but you don't want the DepartmentID caption for this field, so make the following highlighted change to the Views\Course\Create.cshtml file to change the caption.
@model ContosoUniversity.Models.Course
@{
ViewBag.Title = "Create";
}
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
@Html.ValidationSummary(true)
@Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.CourseID)
@Html.ValidationMessageFor(model => model.CourseID)
@Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
@Html.LabelFor(model => model.Credits, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.Credits)
@Html.ValidationMessageFor(model => model.Credits)
@Html.DropDownList("DepartmentID", String.Empty)
@Html.ValidationMessageFor(model => model.DepartmentID)
}
@Html.ActionLink("Back to List", "Index")
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Make the same change in Views\Course\Edit.cshtml.
Normally the scaffolder doesn't scaffold a primary key because the key value is generated by the database and can't be changed and isn't a meaningful value to be displayed to users. For Course entities the scaffolder does include an text box for the
In Views\Course\Edit.cshtml, add a course number field before the Title field. Because it's the primary key, it's displayed, but it can't be changed.
@Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
@Html.DisplayFor(model => model.CourseID)
There's already a hidden field (
In Views\Course\Delete.cshtml and Views\Course\Details.cshtml, change the department name caption from "Name" to "Department" and add a course number field before the Title field.
Department
@Html.DisplayFor(model => model.Department.Name)
@Html.DisplayNameFor(model => model.CourseID)
@Html.DisplayFor(model => model.CourseID)
Run the Create page (display the Course Index page and click Create New) and enter data for a new course:
Click Create. The Course Index page is displayed with the new course added to the list. The department name in the Index page list comes from the navigation property, showing that the relationship was established correctly.
Run the Edit page (display the Course Index page and click Edit on a course).
Change data on the page and click Save. The Course Index page is displayed with the updated course data.
When you edit an instructor record, you want to be able to update the instructor's office assignment. The
Open InstructorController.cs and look at the
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors.Find(id);
if (instructor == null)
{
return HttpNotFound();
}
ViewBag.ID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.ID);
return View(instructor);
}
The scaffolded code here isn't what you want. It's setting up data for a drop-down list, but you what you need is a text box. Replace this method with the following code:
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
if (instructor == null)
{
return HttpNotFound();
}
return View(instructor);
}
This code drops the
Replace the
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
return View(instructorToUpdate);
}
The reference to
The code does the following:
In Views\Instructor\Edit.cshtml, after the
@Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
Run the page (select the Instructors tab and then click Edit on an instructor). Change the Office Location and click Save.
Instructors may teach any number of courses. Now you'll enhance the Instructor Edit page by adding the ability to change course assignments using a group of check boxes, as shown in the following screen shot:
The relationship between the
The UI that enables you to change which courses an instructor is assigned to is a group of check boxes. A check box for every course in the database is displayed, and the ones that the instructor is currently assigned to are selected. The user can select or clear check boxes to change course assignments. If the number of courses were much greater, you would probably want to use a different method of presenting the data in the view, but you'd use the same method of manipulating navigation properties in order to create or delete relationships.
To provide data to the view for the list of check boxes, you'll use a view model class. Create AssignedCourseData.cs in the ViewModels folder and replace the existing code with the following code:
namespace ContosoUniversity.ViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}
In InstructorController.cs, replace the
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.ID == id)
.Single();
PopulateAssignedCourseData(instructor);
if (instructor == null)
{
return HttpNotFound();
}
return View(instructor);
}
private void PopulateAssignedCourseData(Instructor instructor)
{
var allCourses = db.Courses;
var instructorCourses = new HashSet(instructor.Courses.Select(c => c.CourseID));
var viewModel = new List();
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewBag.Courses = viewModel;
}
The code adds eager loading for the
The code in the
Next, add the code that's executed when the user clicks Save. Replace the
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int? id, string[] selectedCourses)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
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();
return;
}
var selectedCoursesHS = new HashSet(selectedCourses);
var instructorCourses = new HashSet
(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);
}
}
}
}
The method signature is now different from the
Since the view doesn't have a collection of
If no check boxes were selected, the code in
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List();
return;
}
The code then loops through all courses in the database and checks each course against the ones currently assigned to the instructor versus the ones that were selected in the view. To facilitate efficient lookups, the latter two collections are stored in
If the check box for a course was selected but the course isn't in the
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
If the check box for a course wasn't selected, but the course is in the
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
In Views\Instructor\Edit.cshtml, add a Courses field with an array of check boxes by adding the following code immediately after the
After you paste the code, if line breaks and indentation don't look like they do here, manually fix everything so that it looks like what you see here. The indentation doesn't have to be perfect, but the
This code creates an HTML table that has three columns. In each column is a check box followed by a caption that consists of the course number and title. The check boxes all have the same name ("selectedCourses"), which informs the model binder that they are to be treated as a group. The
When the check boxes are initially rendered, those that are for courses assigned to the instructor have
After changing course assignments, you'll want to be able to verify the changes when the site returns to the
In Views\Instructor\Index.cshtml, add a Courses heading immediately following the Office heading, as shown in the following example:
Last Name
First Name
Hire Date
Office
Courses
Then add a new detail cell immediately following the office location detail cell:
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
@{
foreach (var course in item.Courses)
{
@course.CourseID @: @course.Title
}
}
@Html.ActionLink("Select", "Index", new { id = item.ID }) |
@Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
@Html.ActionLink("Details", "Details", new { id = item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.ID })
Run the Instructor Index page to see the courses assigned to each instructor:
Click Edit on an instructor to see the Edit page.
Change some course assignments and click Save. The changes you make are reflected on the Index page.
Note: The approach taken here to edit instructor course data works well when there is a limited number of courses. For collections that are much larger, a different UI and a different updating method would be required.
In InstructorController.cs, delete the
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
db.Instructors.Remove(instructor);
var department = db.Departments
.Where(d => d.InstructorID == id)
.SingleOrDefault();
if (department != null)
{
department.InstructorID = null;
}
db.SaveChanges();
return RedirectToAction("Index");
}
This code makes the following change:
This code doesn't handle the scenario of one instructor assigned as administrator for multiple departments. In the last tutorial you'll add code that prevents that scenario from happening.
In InstructorController.cs, delete the
public ActionResult Create()
{
var instructor = new Instructor();
instructor.Courses = new List();
PopulateAssignedCourseData(instructor);
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment" )]Instructor instructor, string[] selectedCourses)
{
if (selectedCourses != null)
{
instructor.Courses = new List();
foreach (var course in selectedCourses)
{
var courseToAdd = db.Courses.Find(int.Parse(course));
instructor.Courses.Add(courseToAdd);
}
}
if (ModelState.IsValid)
{
db.Instructors.Add(instructor);
db.SaveChanges();
return RedirectToAction("Index");
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}
This code is similar to what you saw for the Edit methods except that initially no courses are selected. The
The HttpPost Create method adds each selected course to the Courses navigation property before the template code that checks for validation errors and adds the new instructor to the database. Courses are added even if there are model errors so that when there are model errors (for an example, the user keyed an invalid date) so that when the page is redisplayed with an error message, any course selections that were made are automatically restored.
Notice that in order to be able to add courses to the
instructor.Courses = new List();
As an alternative to doing this in controller code, you could do it in the Instructor model by changing the property getter to automatically create the collection if it doesn't exist, as shown in the following example:
private ICollection _courses;
public virtual ICollection Courses
{
get
{
return _courses ?? (_courses = new List());
}
set
{
_courses = value;
}
}
If you modify the
In Views\Instructor\Create.cshtml, add an office location text box and course check boxes after the hire date field and before the Submit button.
@Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
After you paste the code, fix line breaks and indentation as you did earlier for the Edit page.
Run the Create page and add an instructor.
As explained in the Basic CRUD Functionality tutorial, by default the Entity Framework implicitly implements transactions. For scenarios where you need more control -- for example, if you want to include operations done outside of Entity Framework in a transaction -- see Working with Transactions on MSDN.
You have now completed this introduction to working with related data. So far in these tutorials you've worked with code that does synchronous I/O. You can make the application use web server resources more efficiently by implementing asynchronous code, and that's what you'll do in the next tutorial.
Please leave feedback on how you liked this tutorial and what we could improve. You can also request new topics at Show Me How With Code.
Links to other Entity Framework resources can be found in ASP.NET Data Access - Recommended Resources.
The following illustrations show some of the pages that you'll work with.
Customize the Create and Edit Pages for Courses
When a new course entity is created, it must have a relationship to an existing department. To facilitate this, the scaffolded code includes controller methods and Create and Edit views that include a drop-down list for selecting the department. The drop-down list sets the
- Code:
Course.DepartmentID
- Code:
Department
- Code:
Department
In CourseController.cs, delete the four
- Code:
Create
- Code:
Edit
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID")]Course course)
{
try
{
if (ModelState.IsValid)
{
db.Courses.Add(course);
db.SaveChanges();
return RedirectToAction("Index");
}
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Course course = db.Courses.Find(id);
if (course == null)
{
return HttpNotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var courseToUpdate = db.Courses.Find(id);
if (TryUpdateModel(courseToUpdate, "",
new string[] { "Title", "Credits", "DepartmentID" }))
{
try
{
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
return View(courseToUpdate);
}
private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
var departmentsQuery = from d in db.Departments
orderby d.Name
select d;
ViewBag.DepartmentID = new SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment);
}
Add the following
- Code:
using
using System.Data.Entity.Infrastructure;
The
- Code:
PopulateDepartmentsDropDownList
- Code:
SelectList
- Code:
ViewBag
- Code:
selectedDepartment
- Code:
DepartmentID
- Code:
ViewBag
- Code:
SelectList
- Code:
DepartmentID
The
- Code:
HttpGet
- Code:
Create
- Code:
PopulateDepartmentsDropDownList
public ActionResult Create()
{
PopulateDepartmentsDropDownList();
return View();
}
The
- Code:
HttpGet
- Code:
Edit
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Course course = db.Courses.Find(id);
if (course == null)
{
return HttpNotFound();
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
}
The
- Code:
HttpPost
- Code:
Create
- Code:
Edit
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);
This code ensures that when the page is redisplayed to show the error message, whatever department was selected stays selected.
The Course views are already scaffolded with drop-down lists for the department field, but you don't want the DepartmentID caption for this field, so make the following highlighted change to the Views\Course\Create.cshtml file to change the caption.
@model ContosoUniversity.Models.Course
@{
ViewBag.Title = "Create";
}
Create
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
Course
@Html.ValidationSummary(true)
@Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.CourseID)
@Html.ValidationMessageFor(model => model.CourseID)
@Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
@Html.LabelFor(model => model.Credits, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.Credits)
@Html.ValidationMessageFor(model => model.Credits)
@Html.DropDownList("DepartmentID", String.Empty)
@Html.ValidationMessageFor(model => model.DepartmentID)
}
@Html.ActionLink("Back to List", "Index")
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
Make the same change in Views\Course\Edit.cshtml.
Normally the scaffolder doesn't scaffold a primary key because the key value is generated by the database and can't be changed and isn't a meaningful value to be displayed to users. For Course entities the scaffolder does include an text box for the
- Code:
CourseID
- Code:
DatabaseGeneratedOption.None
In Views\Course\Edit.cshtml, add a course number field before the Title field. Because it's the primary key, it's displayed, but it can't be changed.
@Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
@Html.DisplayFor(model => model.CourseID)
There's already a hidden field (
- Code:
Html.HiddenFor
In Views\Course\Delete.cshtml and Views\Course\Details.cshtml, change the department name caption from "Name" to "Department" and add a course number field before the Title field.
Department
@Html.DisplayFor(model => model.Department.Name)
@Html.DisplayNameFor(model => model.CourseID)
@Html.DisplayFor(model => model.CourseID)
Run the Create page (display the Course Index page and click Create New) and enter data for a new course:
Click Create. The Course Index page is displayed with the new course added to the list. The department name in the Index page list comes from the navigation property, showing that the relationship was established correctly.
Run the Edit page (display the Course Index page and click Edit on a course).
Change data on the page and click Save. The Course Index page is displayed with the updated course data.
Adding an Edit Page for Instructors
When you edit an instructor record, you want to be able to update the instructor's office assignment. The
- Code:
Instructor
- Code:
OfficeAssignment
- If the user clears the office assignment and it originally had a value, you must remove and delete the
- Code:
OfficeAssignment
- If the user enters an office assignment value and it originally was empty, you must create a new
- Code:
OfficeAssignment
- If the user changes the value of an office assignment, you must change the value in an existing
- Code:
OfficeAssignment
Open InstructorController.cs and look at the
- Code:
HttpGet
- Code:
Edit
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors.Find(id);
if (instructor == null)
{
return HttpNotFound();
}
ViewBag.ID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.ID);
return View(instructor);
}
The scaffolded code here isn't what you want. It's setting up data for a drop-down list, but you what you need is a text box. Replace this method with the following code:
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
if (instructor == null)
{
return HttpNotFound();
}
return View(instructor);
}
This code drops the
- Code:
ViewBag
- Code:
OfficeAssignment
- Code:
Find
- Code:
Where
- Code:
Single
Replace the
- Code:
HttpPost
- Code:
Edit
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
}
return View(instructorToUpdate);
}
The reference to
- Code:
RetryLimitExceededException
- Code:
using
- Code:
RetryLimitExceededException
The code does the following:
- Changes the method name to
- Code:
EditPost
- Code:
HttpGet
- Code:
ActionName
- Gets the current
- Code:
Instructor
- Code:
OfficeAssignment
- Code:
HttpGet
- Code:
Edit
- Updates the retrieved
- Code:
Instructor
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" })) - If the office location is blank, sets the
- Code:
Instructor.OfficeAssignment
- Code:
OfficeAssignment
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
} - Saves the changes to the database.
In Views\Instructor\Edit.cshtml, after the
- Code:
div
@Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
Run the page (select the Instructors tab and then click Edit on an instructor). Change the Office Location and click Save.
Adding Course Assignments to the Instructor Edit Page
Instructors may teach any number of courses. Now you'll enhance the Instructor Edit page by adding the ability to change course assignments using a group of check boxes, as shown in the following screen shot:
The relationship between the
- Code:
Course
- Code:
Instructor
- Code:
Instructor.Courses
The UI that enables you to change which courses an instructor is assigned to is a group of check boxes. A check box for every course in the database is displayed, and the ones that the instructor is currently assigned to are selected. The user can select or clear check boxes to change course assignments. If the number of courses were much greater, you would probably want to use a different method of presenting the data in the view, but you'd use the same method of manipulating navigation properties in order to create or delete relationships.
To provide data to the view for the list of check boxes, you'll use a view model class. Create AssignedCourseData.cs in the ViewModels folder and replace the existing code with the following code:
namespace ContosoUniversity.ViewModels
{
public class AssignedCourseData
{
public int CourseID { get; set; }
public string Title { get; set; }
public bool Assigned { get; set; }
}
}
In InstructorController.cs, replace the
- Code:
HttpGet
- Code:
Edit
public ActionResult Edit(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.ID == id)
.Single();
PopulateAssignedCourseData(instructor);
if (instructor == null)
{
return HttpNotFound();
}
return View(instructor);
}
private void PopulateAssignedCourseData(Instructor instructor)
{
var allCourses = db.Courses;
var instructorCourses = new HashSet
var viewModel = new List
foreach (var course in allCourses)
{
viewModel.Add(new AssignedCourseData
{
CourseID = course.CourseID,
Title = course.Title,
Assigned = instructorCourses.Contains(course.CourseID)
});
}
ViewBag.Courses = viewModel;
}
The code adds eager loading for the
- Code:
Courses
- Code:
PopulateAssignedCourseData
- Code:
AssignedCourseData
The code in the
- Code:
PopulateAssignedCourseData
- Code:
Course
- Code:
Courses
- Code:
Assigned
- Code:
true
- Code:
ViewBag
Next, add the code that's executed when the user clicks Save. Replace the
- Code:
EditPost
- Code:
Courses
- Code:
Instructor
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int? id, string[] selectedCourses)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var instructorToUpdate = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses)
.Where(i => i.ID == id)
.Single();
if (TryUpdateModel(instructorToUpdate, "",
new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
{
try
{
if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
{
instructorToUpdate.OfficeAssignment = null;
}
UpdateInstructorCourses(selectedCourses, instructorToUpdate);
db.SaveChanges();
return RedirectToAction("Index");
}
catch (RetryLimitExceededException /* dex */)
{
//Log the error (uncomment dex variable name and add a line here to write a log.
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
return;
}
var selectedCoursesHS = new HashSet
var instructorCourses = new HashSet
(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);
}
}
}
}
The method signature is now different from the
- Code:
HttpGet
- Code:
Edit
- Code:
EditPost
- Code:
Edit
Since the view doesn't have a collection of
- Code:
Course
- Code:
Courses
- Code:
Courses
- Code:
UpdateInstructorCourses
- Code:
Courses
- Code:
Courses
If no check boxes were selected, the code in
- Code:
UpdateInstructorCourses
- Code:
Courses
if (selectedCourses == null)
{
instructorToUpdate.Courses = new List
return;
}
The code then loops through all courses in the database and checks each course against the ones currently assigned to the instructor versus the ones that were selected in the view. To facilitate efficient lookups, the latter two collections are stored in
- Code:
HashSet
If the check box for a course was selected but the course isn't in the
- Code:
Instructor.Courses
if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
if (!instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Add(course);
}
}
If the check box for a course wasn't selected, but the course is in the
- Code:
Instructor.Courses
else
{
if (instructorCourses.Contains(course.CourseID))
{
instructorToUpdate.Courses.Remove(course);
}
}
In Views\Instructor\Edit.cshtml, add a Courses field with an array of check boxes by adding the following code immediately after the
- Code:
div
- Code:
OfficeAssignment
- Code:
div
name="selectedCourses" value="@course.CourseID" @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) /> @course.CourseID @: @course.Title @: |
After you paste the code, if line breaks and indentation don't look like they do here, manually fix everything so that it looks like what you see here. The indentation doesn't have to be perfect, but the
- Code:
@</tr><tr>
- Code:
@:<td>
- Code:
@:</td>
- Code:
@</tr>
This code creates an HTML table that has three columns. In each column is a check box followed by a caption that consists of the course number and title. The check boxes all have the same name ("selectedCourses"), which informs the model binder that they are to be treated as a group. The
- Code:
value
- Code:
CourseID.
- Code:
CourseID
When the check boxes are initially rendered, those that are for courses assigned to the instructor have
- Code:
checked
After changing course assignments, you'll want to be able to verify the changes when the site returns to the
- Code:
Index
- Code:
ViewBag
- Code:
Courses
- Code:
Instructor
In Views\Instructor\Index.cshtml, add a Courses heading immediately following the Office heading, as shown in the following example:
Then add a new detail cell immediately following the office location detail cell:
@if (item.OfficeAssignment != null)
{
@item.OfficeAssignment.Location
}
@{
foreach (var course in item.Courses)
{
@course.CourseID @: @course.Title
}
}
@Html.ActionLink("Select", "Index", new { id = item.ID }) |
@Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
@Html.ActionLink("Details", "Details", new { id = item.ID }) |
@Html.ActionLink("Delete", "Delete", new { id = item.ID })
Run the Instructor Index page to see the courses assigned to each instructor:
Click Edit on an instructor to see the Edit page.
Change some course assignments and click Save. The changes you make are reflected on the Index page.
Note: The approach taken here to edit instructor course data works well when there is a limited number of courses. For collections that are much larger, a different UI and a different updating method would be required.
Update the DeleteConfirmed Method
In InstructorController.cs, delete the
- Code:
DeleteConfirmed
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
Instructor instructor = db.Instructors
.Include(i => i.OfficeAssignment)
.Where(i => i.ID == id)
.Single();
db.Instructors.Remove(instructor);
var department = db.Departments
.Where(d => d.InstructorID == id)
.SingleOrDefault();
if (department != null)
{
department.InstructorID = null;
}
db.SaveChanges();
return RedirectToAction("Index");
}
This code makes the following change:
- If the instructor is assigned as administrator of any department, removes the instructor assignment from that department. Without this code, you would get a referential integrity error if you tried to delete an instructor who was assigned as administrator for a department.
This code doesn't handle the scenario of one instructor assigned as administrator for multiple departments. In the last tutorial you'll add code that prevents that scenario from happening.
Add office location and courses to the Create page
In InstructorController.cs, delete the
- Code:
HttpGet
- Code:
HttpPost
- Code:
Create
public ActionResult Create()
{
var instructor = new Instructor();
instructor.Courses = new List
PopulateAssignedCourseData(instructor);
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment" )]Instructor instructor, string[] selectedCourses)
{
if (selectedCourses != null)
{
instructor.Courses = new List
foreach (var course in selectedCourses)
{
var courseToAdd = db.Courses.Find(int.Parse(course));
instructor.Courses.Add(courseToAdd);
}
}
if (ModelState.IsValid)
{
db.Instructors.Add(instructor);
db.SaveChanges();
return RedirectToAction("Index");
}
PopulateAssignedCourseData(instructor);
return View(instructor);
}
This code is similar to what you saw for the Edit methods except that initially no courses are selected. The
- Code:
HttpGet
- Code:
Create
- Code:
PopulateAssignedCourseData
- Code:
foreach
The HttpPost Create method adds each selected course to the Courses navigation property before the template code that checks for validation errors and adds the new instructor to the database. Courses are added even if there are model errors so that when there are model errors (for an example, the user keyed an invalid date) so that when the page is redisplayed with an error message, any course selections that were made are automatically restored.
Notice that in order to be able to add courses to the
- Code:
Courses
instructor.Courses = new List
As an alternative to doing this in controller code, you could do it in the Instructor model by changing the property getter to automatically create the collection if it doesn't exist, as shown in the following example:
private ICollection
public virtual ICollection
{
get
{
return _courses ?? (_courses = new List
}
set
{
_courses = value;
}
}
If you modify the
- Code:
Courses
In Views\Instructor\Create.cshtml, add an office location text box and course check boxes after the hire date field and before the Submit button.
@Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
@Html.EditorFor(model => model.OfficeAssignment.Location)
@Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
name="selectedCourses" value="@course.CourseID" @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) /> @course.CourseID @: @course.Title @: |
After you paste the code, fix line breaks and indentation as you did earlier for the Edit page.
Run the Create page and add an instructor.
Handling Transactions
As explained in the Basic CRUD Functionality tutorial, by default the Entity Framework implicitly implements transactions. For scenarios where you need more control -- for example, if you want to include operations done outside of Entity Framework in a transaction -- see Working with Transactions on MSDN.
Summary
You have now completed this introduction to working with related data. So far in these tutorials you've worked with code that does synchronous I/O. You can make the application use web server resources more efficiently by implementing asynchronous code, and that's what you'll do in the next tutorial.
Please leave feedback on how you liked this tutorial and what we could improve. You can also request new topics at Show Me How With Code.
Links to other Entity Framework resources can be found in ASP.NET Data Access - Recommended Resources.