MVC 5 with EF 6 in Visual Basic - Updating Related Data

5 (2 votes)

This tutorial is the eighth in a series of 12 that teach you how to build MVC 5 applications using Entity Framework for data access and Visual Basic. 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 original tutorial series, produced by Tom Dykstra and Rick Anderson ( @RickAndMSFT ) was written using the C# language. My versions keep as close to the originals as possible, changing only the coding language. The narrative text is largely unchanged from the original and is used with permission from Microsoft.

This tutorial series teaches you how to create ASP.NET MVC 5 applications using the Entity Framework 6 and Visual Studio 2013 Express for Web. This series uses the Code First workflow. For information about how to choose between Code First, Database First, and Model First, see Entity Framework Development Workflows.

The tutorial series comprises 12 sections in total. They are intended to be followed sequentially as each section builds on the knowledge imparted in the previous sections. Progress through the sections is reflected in a Visual Studio Express for Web project download that accompanies each section which features the web application that you build through the series.

Download the code

The code for this section is available here. Save the .zip file to a convenient location and then extract the contents. Make sure you have an edition of Visual Studio 2013 installed (Express for Web, Professional, Premium or Ultimate) and double click the .sln file. Once the project is opened in your IDE, press Shift+Ctrl+B to build the solution. This will ensure that all packages are restored from Nuget and may take a while depending on your Internet connection speed.

The navigation path through the series is as follows:

  1. Creating an Entity Framework Data Model
  2. Implementing Basic CRUD Functionality
  3. Sorting, Filtering and Paging
  4. Connection Resiliency and Command Interception
  5. Code First Migrations and Deployment
  6. Creating a More Complex Data Model
  7. Reading Related Data
  8. Updating Related Data
  9. Async and Stored Procedures
  10. Handling Concurrency
  11. Implementing-Inheritance
  12. Advanced Entity Framework Scenarios

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 Course.DepartmentID foreign key property, and that's all the Entity Framework needs in order to load the Department navigation property with the appropriate Department entity. You'll use the scaffolded code, but change it slightly to add error handling and sort the drop-down list.

In CourseController.vb, delete the four Create and Edit methods and replace them with the following code:

' GET: /Course/Create
Function Create() As ActionResult
    PopulateDepartmentsDropDownList()
    Return View()
End Function

' POST: /Course/Create
'To protect from overposting attacks, please enable the specific properties you want to bind to, for 
'more details see http://go.microsoft.com/fwlink/?LinkId=317598.
<HttpPost()>
<ValidateAntiForgeryToken()>
Function Create(<Bind(Include:="CourseID,Title,Credits,DepartmentID")> ByVal course As Course) As ActionResult
    Try
        If ModelState.IsValid Then
            db.Courses.Add(course)
            db.SaveChanges()
            Return RedirectToAction("Index")
        End If
    Catch rex As RetryLimitExceededException
        'Log the error
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.")
    End Try
    PopulateDepartmentsDropDownList(course.DepartmentID)
    Return View(course)
End Function

' GET: /Course/Edit/5
Function Edit(ByVal id As Integer?) As ActionResult
    If IsNothing(id) Then
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If
    Dim course As Course = db.Courses.Find(id)
    If IsNothing(course) Then
        Return HttpNotFound()
    End If
    PopulateDepartmentsDropDownList(course.DepartmentID)
    Return View(course)
End Function

Private Sub PopulateDepartmentsDropDownList(Optional ByVal selectedDepartment As Object = Nothing)
    Dim departmentsQuery = db.Departments.OrderBy(Function(d) d.Name)
    ViewBag.DepartmentID = New SelectList(departmentsQuery, "DepartmentID", "Name", selectedDepartment)
End Sub
' POST: /Course/Edit/5
<HttpPost(), ActionName("Edit")>
<ValidateAntiForgeryToken()>
Function EditPost(ByVal id? As Integer) As ActionResult
    If id Is Nothing Then
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If

    Dim courseToUpdate = db.Courses.Find(id)
    If TryUpdateModel(courseToUpdate, "", New String() {"Title", "Credits", "DepartmentID"}) Then
        Try
            db.Entry(courseToUpdate).State = EntityState.Modified
            db.SaveChanges()
            Return RedirectToAction("Index")
        Catch rex As RetryLimitExceededException
            'Log the error
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.")
        End Try
    End If
    PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID)
    Return View(courseToUpdate)
End Function

Add the following Imports statement at the beginning of the file:

Imports System.Data.Entity.Infrastructure

The PopulateDepartmentsDropDownList method gets a list of all departments sorted by name, creates a SelectList collection for a drop-down list, and passes the collection to the view in a ViewBag property. The method accepts the optional selectedDepartment parameter that allows the calling code to specify the item that will be selected when the drop-down list is rendered. The view will pass the name DepartmentID to the DropDownList helper, and the helper then knows to look in the ViewBag object for a SelectList named DepartmentID.

The HttpGet Create method calls the PopulateDepartmentsDropDownList method without setting the selected item, because for a new course the department is not established yet:

Function Create() As ActionResult 
    PopulateDepartmentsDropDownList() 
    Return View() 
End Function

The HttpGet Edit method sets the selected item, based on the ID of the department that is already assigned to the course being edited:

Function Edit(ByVal id As Integer?) As ActionResult
    If IsNothing(id) Then
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If
    Dim course As Course = db.Courses.Find(id)
    If IsNothing(course) Then
        Return HttpNotFound()
    End If
    PopulateDepartmentsDropDownList(course.DepartmentID)
    Return View(course)
End Function

The HttpPost methods for both Create and Edit also include code that sets the selected item when they redisplay the page after an error:

Catch rex As RetryLimitExceededException
    'Log the error
    ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.")
End Try
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.vbhtml file to change the caption.

@ModelType ContosoUniversity.Models.Course
@Code
    ViewData("Title") = "Create"
End Code

<h2>Create</h2>

@Using (Html.BeginForm()) 
    @Html.AntiForgeryToken()
    
    @<div class="form-horizontal">
        <h4>Course</h4>
        <hr />
        @Html.ValidationSummary(true)
        <div class="form-group">
            @Html.LabelFor(Function(model) model.CourseID, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.CourseID)
                @Html.ValidationMessageFor(Function(model) model.CourseID)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(Function(model) model.Title, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.Title)
                @Html.ValidationMessageFor(Function(model) model.Title)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(Function(model) model.Credits, New With { .class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(Function(model) model.Credits)
                @Html.ValidationMessageFor(Function(model) model.Credits)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(Function(model) model.DepartmentID, "Department", New With {.class = "control-label col-md-2"})
            <div class="col-md-10">
                @Html.DropDownList("DepartmentID", String.Empty)
                @Html.ValidationMessageFor(Function(model) model.DepartmentID)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
End Using

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@Section Scripts 
    @Scripts.Render("~/bundles/jqueryval")
End Section

Make the same change in Views\Course\Edit.vbhtml.

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 CourseID field because it understands that the DatabaseGeneratedOption.None attribute means the user should be able enter the primary key value. But it doesn't understand that because the number is meaningful you want to see it in the other views, so you need to add it manually.

In Views\Course\Edit.vbhtml, add a course number field before the Title field. Because it's the primary key, it's displayed, but it can't be changed.

<div class="form-group">
     @Html.LabelFor(Function(model) model.CourseID, New With {.class = "control-label col-md-2"})
     <div class="col-md-10">
         @Html.DisplayFor(Function(model) model.CourseID)
     </div>
</div>

There's already a hidden field (Html.HiddenFor helper) for the course number in the Edit view. Adding an Html.LabelFor helper doesn't eliminate the need for the hidden field because it doesn't cause the course number to be included in the posted data when the user clicks Save on the Edit page.

In Views\Course\Delete.vbhtml and Views\Course\Details.vbhtml, change the department name caption from "Name" to "Department" and add a course number field before the Title field.

<dt>
    Department
</dt>

<dd>
    @Html.DisplayFor(Function(model) model.Department.Name)
</dd>
<dt>
    @Html.DisplayNameFor(Function(model) model.CourseID)
</dt>

<dd>
    @Html.DisplayFor(Function(model) model.CourseID)
</dd>

Run the Create page (display the Course Index page and click Create New) and enter data for a new course:

MVC5 With EF6

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.

MVC5 With EF6

Run the Edit page (display the Course Index page and click Edit on a course).

MVC5 With EF6

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 Instructor entity has a one-to-zero-or-one relationship with the OfficeAssignment entity, which means you must handle the following situations:

  • If the user clears the office assignment and it originally had a value, you must remove and delete the OfficeAssignment entity.
  • If the user enters an office assignment value and it originally was empty, you must create a new OfficeAssignment entity.
  • If the user changes the value of an office assignment, you must change the value in an existing OfficeAssignment entity.

Open InstructorController.vb and look at the HttpGet Edit method:

If IsNothing(id) Then
    Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
End If
Dim instructor As Instructor = db.Instructors.Find(id)
If IsNothing(instructor) Then
    Return HttpNotFound()
End If
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:

If IsNothing(id) Then
    Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
End If
Dim instructor As Instructor = db.Instructors _
    .Include(Function(i) i.OfficeAssignment) _
    .Where(Function(i) i.ID = id) _
    .Single()
If IsNothing(instructor) Then
    Return HttpNotFound()
End If
ViewBag.ID = New SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.ID)
Return View(instructor)

This code drops the ViewBag statement and adds eager loading for the associated OfficeAssignment entity. You can't perform eager loading with the Find method, so the Where and Single methods are used instead to select the instructor.

Replace the HttpPost Edit method with the following code. which handles office assignment updates:

<HttpPost(), ActionName("Edit")>
<ValidateAntiForgeryToken()>
Function EditPost(ByVal id As Integer?) As ActionResult
    If Not id.HasValue Then
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If
    Dim instructorToUpdate = db.Instructors _
                             .Include(Function(i) i.OfficeAssignment) _
                             .Where(Function(i) i.ID = id) _
                             .Single()
    If TryUpdateModel(instructorToUpdate, "", New String() {"LastName", "FirstMidName", "HireDate", "OfficeAssignment"}) Then
        Try
            If String.IsNullOrEmpty(instructorToUpdate.OfficeAssignment.Location) Then instructorToUpdate.OfficeAssignment = Nothing
            db.Entry(instructorToUpdate).State = EntityState.Modified
            db.SaveChanges()
            Return RedirectToAction("Index")
        Catch rex As RetryLimitExceededException
           'Log the error 
           ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.")
        End Try
    End If
    Return View(instructorToUpdate)
End Function

The reference to RetryLimitExceededException requires an Imports statement; to add it, click once on RetryLimitExceededException and press Shift+Alt+F10. Select Import ' System.Data.Entity.Infrastructure'.

The code does the following:

  • Changes the method name to EditPost because the signature is now the same as the HttpGet method (the ActionName attribute specifies that the /Edit/ URL is still used).

  • Gets the current Instructor entity from the database using eager loading for the OfficeAssignment navigation property. This is the same as what you did in the HttpGet Edit method.

  • Updates the retrieved Instructor entity with values from the model binder. The TryUpdateModel overload used enables you to whitelist the properties you want to include. This prevents over-posting, as explained in the second tutorial.

    If TryUpdateModel(instructorToUpdate, "", New String() {"LastName", "FirstMidName", "HireDate", "OfficeAssignment"}) Then
  • If the office location is blank, sets the Instructor.OfficeAssignment property to null so that the related row in the OfficeAssignment table will be deleted.
    If String.IsNullOrEmpty(instructorToUpdate.OfficeAssignment.Location) Then instructorToUpdate.OfficeAssignment = Nothing
  • Saves the changes to the database.

In Views\Instructor\Edit.vbhtml, after the div elements for the Hire Date field, add a new field for editing the office location:

<div class="form-group">
     @Html.LabelFor(Function(model) model.OfficeAssignment.Location, New With {.class = "control-label col-md-2"})
     <div class="col-md-10">
         @Html.EditorFor(Function(model) model.OfficeAssignment.Location)
         @Html.ValidationMessageFor(Function(model) model.OfficeAssignment.Location)
     </div>
</div>

Run the page (select the Instructors tab and then click Edit on an instructor). Change the Office Location and click Save.

MVC5 With EF6

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:

MVC5 With EF6

The relationship between the Course and Instructor entities is many-to-many, which means you do not have direct access to the foreign key properties which are in the join table. Instead, you add and remove entities to and from the Instructor.Courses navigation property.

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.vb in the ViewModels folder and replace the existing code with the following code:

Namespace ViewModels
    Public Class AssignedCourseData
        Public Property CourseID As Integer
        Public Property Title As String
        Public Property Assigned As Boolean
    End Class
End Namespace

In InstructorController.vb, replace the HttpGet Edit method with the following code. The changes are highlighted.

Function Edit(ByVal id As Integer?) As ActionResult
    If IsNothing(id) Then
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If
    Dim instructor = db.Instructors _
        .Include(Function(i) i.OfficeAssignment) _
        .Include(Function(i) i.Courses) _
        .Where(Function(i) i.ID = id) _
        .Single()
    PopulateAssignedCourseData(instructor)
    If IsNothing(instructor) Then
        Return HttpNotFound()
    End If
    Return View(instructor)
End Function

Private Sub PopulateAssignedCourseData(ByVal instructor As Instructor)
    Dim allCourses = db.Courses
    Dim instructorCourses = New HashSet(Of Integer)(instructor.Courses.Select(Function(c) c.CourseID))
    Dim viewModel = New List(Of AssignedCourseData)
    For Each c In allCourses
        viewModel.Add(New AssignedCourseData With
        {
            .CourseID = c.CourseID,
            .Title = c.Title,
            .Assigned = instructorCourses.Contains(c.CourseID)
        })
    Next
    ViewBag.Courses = viewModel
End Sub

The code adds eager loading for the Courses navigation property and calls the new PopulateAssignedCourseData method to provide information for the check box array using the AssignedCourseData view model class.

The code in the PopulateAssignedCourseData method reads through all Course entities in order to load a list of courses using the view model class. For each course, the code checks whether the course exists in the instructor's Courses navigation property. To create efficient lookup when checking whether a course is assigned to the instructor, the courses assigned to the instructor are put into a HashSet collection. The Assigned property  is set to True  for courses the instructor is assigned. The view will use this property to determine which check boxes must be displayed as selected. Finally, the list is passed to the view in a ViewBag property.

Next, add the code that's executed when the user clicks Save. Replace the EditPost method with the following code, which calls a new method that updates the Courses navigation property of the Instructor entity. The changes are highlighted.

<HttpPost()>
<ValidateAntiForgeryToken()>
Function Edit(ByVal id As Integer?, ByVal selectedCourses As String()) As ActionResult
    If Not id.HasValue Then
        Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
    End If
    Dim instructorToUpdate = db.Instructors _
                             .Include(Function(i) i.OfficeAssignment) _
                             .Include(Function(i) i.Courses) _
                             .Where(Function(i) i.ID = id) _
                             .Single()
    If TryUpdateModel(instructorToUpdate, "", New String() {"LastName", "FirstMidName", "HireDate", "OfficeAssignment"}) Then
        Try
            If String.IsNullOrEmpty(instructorToUpdate.OfficeAssignment.Location) Then instructorToUpdate.OfficeAssignment = Nothing
            UpdateInstructorCourses(selectedCourses, instructorToUpdate)
            db.Entry(instructorToUpdate).State = EntityState.Modified
            db.SaveChanges()
            Return RedirectToAction("Index")
        Catch rex As RetryLimitExceededException
            'Log the error 
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.")
        End Try
    End If
    PopulateAssignedCourseData(instructorToUpdate)
    Return View(instructorToUpdate)
End Function

Private Sub UpdateInstructorCourses(ByVal selectedCourses As String(), ByVal instructorToUpdate As Instructor)
    If selectedCourses Is Nothing Then
        instructorToUpdate.Courses = New List(Of Course)()
        Return
    End If
    Dim selectedCoursesHS = New HashSet(Of String)(selectedCourses)
    Dim instructorCourses = New HashSet(Of Integer)(instructorToUpdate.Courses.Select(Function(c) c.CourseID))
    For Each c In db.Courses
        If selectedCoursesHS.Contains(c.CourseID.ToString()) Then
            If Not instructorCourses.Contains(c.CourseID) Then
                instructorToUpdate.Courses.Add(c)
            End If
        Else
            If instructorCourses.Contains(c.CourseID) Then
                instructorToUpdate.Courses.Remove(c)
            End If
        End If
    Next
End Sub

The method signature is now different from the HttpGet Edit method, so the method name changes from EditPost back to Edit.

Since the view doesn't have a collection of Course entities, the model binder can't automatically update the Courses navigation property. Instead of using the model binder to update the Courses navigation property, you'll do that in the new UpdateInstructorCourses method. Therefore you need to exclude the Courses property from model binding. This doesn't require any change to the code that calls TryUpdateModel because you're using the whitelisting overload and Courses isn't in the include list.

If no check boxes were selected, the code in UpdateInstructorCourses initializes the Courses navigation property with an empty collection:

If selectedCourses Is Nothing Then
    instructorToUpdate.Courses = New List(Of Course)()
    Return
End If

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 HashSet objects.

If the check box for a course was selected but the course isn't in the Instructor.Courses navigation property, the course is added to the collection in the navigation property.

If selectedCoursesHS.Contains(c.CourseID.ToString()) Then
    If Not instructorCourses.Contains(c.CourseID) Then
        instructorToUpdate.Courses.Add(c)
    End If

If the check box for a course wasn't selected, but the course is in the Instructor.Courses navigation property, the course is removed from the navigation property.

Else
    If instructorCourses.Contains(c.CourseID) Then
        instructorToUpdate.Courses.Remove(c)
    End If
End If

In Views\Instructor\Edit.vbhtml, add a Courses field with an array of check boxes by adding the following code immediately after the div elements for the OfficeAssignment field and before the div element for the Save button:

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            @Code
                Dim courses As List(Of ContosoUniversity.ViewModels.AssignedCourseData) = ViewBag.Courses

                Dim data = courses.Select(Function(value, index) New With {
                                                    .Index = Convert.ToInt32((index - 1) / 3),
                                                    .Value = value}) _
                                    .GroupBy(Function(pair) pair.Index)
            @For Each group In data
                    Dim total = group.Count()
            @<tr>
                @For Each item In group
                    @<td>
                        <input type="checkbox" name="selectedCourses" value="@item.Value.CourseID" checked="@item.Value.Assigned" />
                        @item.Value.CourseID  @item.Value.Title
                    </td>
                Next
                @For i = 0 To (3 - total)
                    @<td>&nbsp;</td>
                Next
            </tr>
            Next
            End Code
        </table>
    </div>
</div>

This code creates an HTML table that has three columns. The data is projected into a collection of anonymous types using the overload of the Select function that incorporates each elements index. Each type is given an Index property that is calculated by dividing its index (-1) by 3 and then converted to an integer to remove any decimal places. The first three elements will have an Index value of 0. The next 3 will have an Index value of 1 and so on. The data is then grouped by that Index value, so the first group consists of the first 3 elements; the second group consist of the 4th, 5th and 6th element and so on. Each group is output as a row in the table.

Each column contains 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 value attribute of each check box is set to the value of CourseID. When the page is posted, the model binder passes an array to the controller that consists of the CourseID values for only the check boxes which are selected.

When the check boxes are initially rendered, those that are for courses assigned to the instructor have checked attributes, which selects them (displays them checked).

After changing course assignments, you'll want to be able to verify the changes when the site returns to the Index page. Therefore, you need to add a column to the table in that page. In this case you don't need to use the ViewBag object, because the information you want to display is already in the Courses navigation property of the Instructor entity that you're passing to the page as the model.

In Views\Instructor\Index.vbhtml, add a Courses heading immediately following the Office heading, as shown in the following example:

<tr>
    <th>
        Last Name
    </th>
    <th>
        First Name
    </th>
    <th>
        Hire Date
    </th>
    <th>
        Office
    </th>
    <th>
        Courses
    </th>
    <th></th>
</tr>

Then add a new detail cell immediately following the office location detail cell:

<td>
    @If item.OfficeAssignment IsNot Nothing Then
        @item.OfficeAssignment.Location
    End If
</td>
<td>
        @For Each c In item.Courses
            @c.CourseID   @c.Title @<br />
        Next
</td>
<td>
    @Html.ActionLink("Select", "Index", New With {.id = item.ID}) |
    @Html.ActionLink("Edit", "Edit", New With {.id = item.ID}) |
    @Html.ActionLink("Details", "Details", New With {.id = item.ID}) |
    @Html.ActionLink("Delete", "Delete", New With {.id = item.ID})
</td>

Run the Instructor Index page to see the courses assigned to each instructor:

MVC5 With EF6

Click Edit on an instructor to see the Edit page.

MVC5 With EF6 

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.vb, delete the DeleteConfirmed method and insert the following code in its place.

<HttpPost()>
<ActionName("Delete")>
<ValidateAntiForgeryToken()>
Function DeleteConfirmed(ByVal id As Integer) As ActionResult
    Dim instructor As Instructor = db.Instructors _
                                   .Include(Function(i) i.OfficeAssignment) _
                                   .Single(Function(i) i.ID = id)
    instructor.OfficeAssignment = Nothing
    
    db.Instructors.Remove(instructor)
    
    Dim dept = db.Departments _
               .SingleOrDefault(Function(d) d.InstructorID = id)
    If dept IsNot Nothing Then dept.InstructorID = Nothing
     
    db.SaveChanges()
    Return RedirectToAction("Index")
End Function

This code makes two changes:

  • Deletes the office assignment record (if any) when the instructor is deleted.
  • 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.

Add office location and courses to the Create page

In InstructorController.vb, delete the HttpGet and HttpPost Create methods, and then add the following code in their place:

' GET: /Instructor/Create
Function Create() As ActionResult
    Dim instructor As New Instructor()
    instructor.Courses = New List(Of Course)
    PopulateAssignedCourseData(instructor)
    Return View()
End Function

' POST: /Instructor/Create
'To protect from overposting attacks, please enable the specific properties you want to bind to, for 
'more details see http://go.microsoft.com/fwlink/?LinkId=317598.
<HttpPost()>
<ValidateAntiForgeryToken()>
Function Create(<Bind(Include:="ID,LastName,FirstMidName,HireDate,OfficeAssignment")> ByVal instructor As Instructor, ByVal selectedCourses As String()) As ActionResult
    If selectedCourses IsNot Nothing Then
        instructor.Courses = New List(Of Course)
        For Each id In selectedCourses
            Dim courseToAdd = db.Courses.Find(Integer.Parse(id))
            instructor.Courses.Add(courseToAdd)
        Next
    End If

    If ModelState.IsValid Then
        db.Instructors.Add(instructor)
        db.SaveChanges()
        Return RedirectToAction("Index")
    End If
    PopulateAssignedCourseData(instructor)
    Return View(instructor)
End Function

This code is similar to what you saw for the Edit methods except that initially no courses are selected. The HttpGet Create method calls the PopulateAssignedCourseData method not because there might be courses selected but in order to provide an empty collection for the For Each loop in the view (otherwise the view code would throw a null reference exception).

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 Courses navigation property you have to initialize the property as an empty collection:

instructor.Courses = New List(Of Course)

In Views\Instructor\Create.vbhtml, add an office location text box and course check boxes after the hire date field and before the Submit button.

<div class="form-group">
    @Html.LabelFor(Function(model) model.OfficeAssignment.Location, New With {.class = "control-label col-md-2"})
    <div class="col-md-10">
        @Html.EditorFor(Function(model) model.OfficeAssignment.Location)
        @Html.ValidationMessageFor(Function(model) model.OfficeAssignment.Location)
    </div>
</div>

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            @Code
                Dim courses As List(Of ContosoUniversity.ViewModels.AssignedCourseData) = ViewBag.Courses

                Dim data = courses.Select(Function(value, index) New With {
                                                .Index = Convert.ToInt32((index - 1) / 3),
                                                .Value = value}) _
                                    .GroupBy(Function(pair) pair.Index)
                @For Each group In data
                        Dim total = group.Count()
                    @<tr>
                        @For Each item In group
                            @<td>
                                <input type="checkbox" name="selectedCourses" value="@item.Value.CourseID" checked="@item.Value.Assigned" />
                                @item.Value.CourseID  @item.Value.Title
                            </td>
                        Next
                        @For i = 0 To (3 - total)
                            @<td>&nbsp;</td>
                        Next
                    </tr>
                Next
            End Code
        </table>
    </div>
</div>

Run the Create page and add an instructor.

MVC5 With EF6

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.

 

You might also like...

Date Posted:
Last Updated:
Posted by:
Total Views to date: 15415

2 Comments

- Caty Hespel

Hi,

Thank you for this very good tutorial. I managed to create my first MVC app thanks to you!

I found a a small error in the example code of ' POST: /Course/Edit/5
There's an "Edit" too much in front of the "Post". Probably a result of copy 7 paste.

<HttpPost(), ActionName("Edit")>
<ValidateAntiForgeryToken()>
Function Edit Post(ByVal id? As Integer) As ActionResult.

Should be:

' POST: /Course/Edit/5
<HttpPost(), ActionName("Edit")>
<ValidateAntiForgeryToken()>
Function Post(ByVal id? As Integer) As ActionResult

Kind regards,
Caty

- Mike

@Caty,

Thanks for pointing that out. It should actually be one word: "EditPost"

Recent Comments

Pam 30/08/2017 11:30
In response to Sending Email in Razor Pages
Mike, RazorPages sound like a nice choice for somebody still working in ASP classic who wants to to...

Robby Robson 15/08/2017 00:43
In response to Routing in Razor Pages
Mike: great stuff. Now that .Core Standard 2.0 is formally out, how soon will you rewrite your book...

Satyabrata Mohapatra 28/07/2017 08:59
In response to Sending Email in Razor Pages
Bit off topic, but congratulation sir for your MVP award. You deserve it !!!...

Satyabrata Mohapatra 23/07/2017 16:43
In response to Razor Pages - The Elevator Pitch
@Dale Severin You can continue to build apps using asp.net web pages....

Satyabrata Mohapatra 23/07/2017 16:40
In response to Sending Email in Razor Pages
Thanks for sharing...learned a lot...

Gfw 22/07/2017 11:53
In response to Sending Email in Razor Pages
Question... Does System.Net.Mail support SSL?...

Dale Severin 20/07/2017 03:38
In response to Razor Pages - The Elevator Pitch
I work with razor web pages extensively. I appreciate the rapid development it permits me to I am as...

Obinna Okafor 14/07/2017 01:19
In response to Routing in Razor Pages
Thank you, Mike. Good post....

Satyabrata Mohapatra 11/07/2017 16:02
In response to Routing in Razor Pages
Very powerful routing system!!...

Cyrus 05/07/2017 03:41
In response to Razor Pages - Getting Started With The Preview
How can I trim packages and services as much as possible to use just razor pages? I don’t want to to...