MVC Model that includes property of a different Mo

2019-09-10 02:14发布

问题:

I am relatively new to MVC, and here's where I'm at:

I have 2 Models:

The Site model -

SiteID() As Integer
Name() As String
Latitude() As Double
Longitude() As Double

And the Device model -

DeviceID() As Integer
Make() As String
Model() As String
Serial() As String
Site() As Site

As you can see, a Device is linked to a Site. In the DB Context, the Devices table contains the foreign key "Site_SiteID" as shown:

What I'm trying to do is in the Device views for Create and Edit, make it so the Site field is a DropDownListFor, whose list contains the Sites that exist in the Site table. Then save the selected site as the device's site.

This is what I put in the Create view for creating the list that will be in the drop down...followed by the DropDownListFor:

@ModelType TestWebApplication2.Device
@Code
    ViewData("Title") = "Create"

    Dim selectSite As New List(Of SelectListItem)
    Dim db = New TestWebApplication2.Models.TestWebApplication2Context
    Dim listOfSites As List(Of Site)
    listOfSites = (From site In db.Sites Select site).ToList()

    For Each item In listOfSites
        selectSite.Add(New SelectListItem() With {.Value = item.SiteID, .Text = item.Name})
    Next

End Code

 <div class="form-group">
     @Html.LabelFor(Function(model) model.Site, htmlAttributes:=New With {.class = "control-label col-md-2"})
     <div class="col-md-10">
         @Html.DropDownListFor(Function(model) model.Site, selectSite), New With {.htmlAttributes = New With {.class = "form-control"}})
         @Html.ValidationMessageFor(Function(model) model.Site, "", New With {.class = "text-danger"})
     </div>
 </div>

This is the Create post in DevicesController:

    <HttpPost()>
    <ValidateAntiForgeryToken()>
    Async Function Create(<Bind(Include:="DeviceID,Make,Model,Serial,Site")> ByVal device As Device) As Task(Of ActionResult)
        If ModelState.IsValid Then
            db.Devices.Add(device)
            Await db.SaveChangesAsync()
            Return RedirectToAction("Index")
        End If
        Return View(device)
    End Function

This works in that I can get the list of Sites in the drop down in the Create page, but when I hit "Save", it gives me an error like "The value '1' is invalid". This is because it's trying to pass in a string (being that's the type that is the list item's Value) instead of a Site.

So one thing I tried was setting the Value for each drop down list item to the site item itself, like so:

    selectSite.Add(New SelectListItem() With {.Value = item, .Text = item.Name})

But it gives me an error that it can't cast a Site to a String, and so you can't pass back the whole object.

So then I tried instead to set the device's Site by getting it by ID in the Controller, like so (note "Site" was taken out of the Bind list):

    <HttpPost()>
    <ValidateAntiForgeryToken()>
    Async Function Create(<Bind(Include:="DeviceID,Make,Model,Serial")> ByVal device As Device) As Task(Of ActionResult)
        If ModelState.IsValid Then
            Dim thisSite As Integer = CInt(Request.Form.Get("Site"))
            device.Site = (From site In db.Sites Where site.SiteID = thisSite Select site).FirstOrDefault()
            db.Devices.Add(device)
            Await db.SaveChangesAsync()
            Return RedirectToAction("Index")
        End If
        Return View(device)
    End Function

This does work to set the Device's Site, but it is not actually saved to the DB (I'm assuming because the DB wants to only hold the foreign key, not an actual Site object). So the next time I go to like the Index or Details page, the device's site is Nothing.

I would imagine there must a way to do this, but I'm not sure what else to try from here. Any help is appreciated!

回答1:

Add Site_SiteId to your devices model and bind the drop down to that value.

Also, I wouldn't compose the list of items in the view - do that in the controller and pass it in either through a ViewModel (http://sampathloku.blogspot.com/2012/10/how-to-use-viewmodel-with-aspnet-mvc.html) or ViewBag.



回答2:

I was until now unaware of this thing called a "Navigation Property". So Site, being a property of a Device which is it's own Model, links the two tables together.

So what I was missing, as Steve Greene had clued me in on, was Including Site along with Device in the models for the views in which I wanted to use the Site's Name (in the list for Index, and using a Predicate for Details and Delete)...called "Eager Loading".

I also moved the code for generating the list of items for the DropDownFor to the Controller, as suggested, using a ViewData. Now as stated in the question, because that is passing back only the Site ID I have to manually set the Device's Site based on the ID of the item they selected.

So here is what my Devices Controller looks like now:

        Async Function Index() As Task(Of ActionResult)
            'Need to have this include Site so that it will pull in that data in order to reference site name in the View
            '(doing this is called Eager Loading)
            Dim devices = db.Devices.Include("Site").ToListAsync()
            Return View(Await devices)
        End Function

        ' GET: Devices/Details/5
        Async Function Details(ByVal id As Integer?) As Task(Of ActionResult)
            If IsNothing(id) Then
                Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
            End If
            Dim device As Device = Await db.Devices.FindAsync(id)
            If IsNothing(device) Then
                Return HttpNotFound()
            Else
                'Need to have this include Site using Predicate so that it will pull in that data in order to reference site name in the View
                device = db.Devices.Include("Site").Where(Function(x) x.DeviceID = id).FirstOrDefault()
            End If
            Return View(device)
        End Function

        ' GET: Devices/Create
        Function Create()
            'Create the list of items that the DropDownFor in the View will reference
            Dim selectSite As New List(Of SelectListItem)
            Dim db = New TestWebApplication2.Models.TestWebApplication2Context
            Dim listOfSites As List(Of Site)
            listOfSites = (From site In db.Sites Select site).ToList()

            For Each item In listOfSites
                selectSite.Add(New SelectListItem() With {.Value = item.SiteID, .Text = item.Name})
            Next
            ViewData("selectSite") = selectSite

            Return View()
        End Function

        ' POST: Devices/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()>
        Async Function Create(<Bind(Include:="DeviceID,Make,Model,Serial")> ByVal device As Device) As Task(Of ActionResult)
            If ModelState.IsValid Then
                'Because Site is selected in a DropDownFor which only passes the ID integer, need to set the device's site property accordingly
                'Note that with Site being a Navigation property, it looks like it doesn't get saved the way you would think...but it all works
                Dim thisSite As Integer = CInt(Request.Form.Get("Site"))
                device.Site = (From site In db.Sites Where site.SiteID = thisSite Select site).FirstOrDefault()
                db.Devices.Add(device)
                Await db.SaveChangesAsync()
                Return RedirectToAction("Index")
            End If
            Return View(device)
        End Function

        ' GET: Devices/Edit/5
        Async Function Edit(ByVal id As Integer?) As Task(Of ActionResult)
            If IsNothing(id) Then
                Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
            End If
            Dim device As Device = Await db.Devices.FindAsync(id)
            If IsNothing(device) Then
                Return HttpNotFound()
            Else
                'Create the list of items that the DropDownFor in the View will reference
                Dim selectSite As New List(Of SelectListItem)
                Dim db = New TestWebApplication2.Models.TestWebApplication2Context
                Dim listOfSites As List(Of Site)
                listOfSites = (From site In db.Sites Select site).ToList()

                For Each item In listOfSites
                    selectSite.Add(New SelectListItem() With {.Value = item.SiteID, .Text = item.Name})
                Next
                ViewData("selectSite") = selectSite
            End If

            Return View(device)
        End Function

        ' POST: Devices/Edit/5
        '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()>
        Async Function Edit(<Bind(Include:="DeviceID,Make,Model,Serial")> ByVal device As Device) As Task(Of ActionResult)
            If ModelState.IsValid Then
                'Because Site is selected in a DropDownFor which only passes the ID integer, need to set the device's site property accordingly
                'Note that with Site being a Navigation property, it looks like it doesn't get saved the way you would think...but it all works
                Dim thisSite As Integer = CInt(Request.Form.Get("Site"))
                device.Site = (From site In db.Sites Where site.SiteID = thisSite Select site).FirstOrDefault()
                db.Entry(device).State = EntityState.Modified
                Await db.SaveChangesAsync()
                Return RedirectToAction("Index")
            End If
            Return View(device)
        End Function

        ' GET: Devices/Delete/5
        Async Function Delete(ByVal id As Integer?) As Task(Of ActionResult)
            If IsNothing(id) Then
                Return New HttpStatusCodeResult(HttpStatusCode.BadRequest)
            End If
            Dim device As Device = Await db.Devices.FindAsync(id)
            If IsNothing(device) Then
                Return HttpNotFound()
            Else
                'Need to have this include Site using Predicate so that it will pull in that data in order to reference site name in the View
                device = db.Devices.Include("Site").Where(Function(x) x.DeviceID = id).FirstOrDefault()
            End If
            Return View(device)
        End Function

        ' POST: Devices/Delete/5
        <HttpPost()>
        <ActionName("Delete")>
        <ValidateAntiForgeryToken()>
        Async Function DeleteConfirmed(ByVal id As Integer) As Task(Of ActionResult)
            Dim device As Device = Await db.Devices.FindAsync(id)
            db.Devices.Remove(device)
            Await db.SaveChangesAsync()
            Return RedirectToAction("Index")
        End Function

        Protected Overrides Sub Dispose(ByVal disposing As Boolean)
            If (disposing) Then
                db.Dispose()
            End If
            MyBase.Dispose(disposing)
        End Sub