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!
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.
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