0

I'm almost new to MVC and trying to create a simple app for data management (MVC 5/VS 2022) using this tutorial. I have two classes along with their controllers and views: Site and Warehouse. Warehouse is site-dependent, so this is its model:

Public Class Warehouse Implements IValidatableObject Private mId As Integer Private mSite As Site Private mWarehouseID As String Private mWarehouseName As String <Display(Name:="Site"), Required> Public SiteID As Integer <Key> Public Property Id As Integer Get Return mId End Get Set(value As Integer) mId = value End Set End Property <Display(Name:="Site")> Public Overridable Property Site As Site Get Return mSite End Get Set(value As Site) mSite = value End Set End Property <Display(Name:="Warehouse ID"), Required, Index(IsUnique:=True), MaxLength(20, ErrorMessage:="Maximum allowed length is 20 characters")> Public Property WarehouseID As String Get Return mWarehouseID End Get Set(value As String) mWarehouseID = value End Set End Property <Display(Name:="Warehouse Name"), Required, MaxLength(100, ErrorMessage:="Maximum allowed length is 100 characters")> Public Property WarehouseName As String Get Return mWarehouseName End Get Set(value As String) mWarehouseName = value End Set End Property Public Function Validate(validationContext As ValidationContext) As IEnumerable(Of ValidationResult) Implements IValidatableObject.Validate Dim db As New Data.KFI_IPPContext Dim vresult As New List(Of ValidationResult) Dim validatename = db.Warehouses.FirstOrDefault(Function(w) (w.WarehouseID = WarehouseID) And (w.Id <> Id)) If validatename IsNot Nothing Then Dim errmsg As New ValidationResult($"Warehouse {WarehouseID} already exists.") vresult.Add(errmsg) End If Return vresult End Function End Class 

And this is controller:

Public Class WarehousesController Inherits System.Web.Mvc.Controller Private db As New KFI_IPPContext ' GET: Warehouses Public Async Function Index(SearchString As String) As Threading.Tasks.Task(Of ActionResult) Dim warehouses = From w In db.Warehouses If String.IsNullOrEmpty(SearchString) Then warehouses = From w In warehouses Select w Else warehouses = From w In warehouses Where w.WarehouseName.Contains(SearchString) Or w.WarehouseID.Contains(SearchString) Or w.Site.SiteName.Contains(SearchString) End If Return View(Await warehouses.ToListAsync) End Function ' GET: Warehouses/Details/5 Function Details(ByVal id As Integer?) As ActionResult If IsNothing(id) Then Return New HttpStatusCodeResult(HttpStatusCode.BadRequest) End If Dim warehouse As Warehouse = db.Warehouses.Find(id) If IsNothing(warehouse) Then Return HttpNotFound() End If Return View(warehouse) End Function ' GET: Warehouses/Create Function Create() As ActionResult PopulateSiteList() Return View() End Function ' POST: Warehouses/Create 'To protect from overposting attacks, enable the specific properties you want to bind to, for 'more details see https://go.microsoft.com/fwlink/?LinkId=317598. <HttpPost()> <ValidateAntiForgeryToken()> Function Create(<Bind(Include:="Id,WarehouseID,WarehouseName")> ByVal warehouse As Warehouse) As ActionResult If ModelState.IsValid Then db.Warehouses.Add(warehouse) db.SaveChanges() Return RedirectToAction("Index") End If PopulateSiteList(warehouse.SiteID) Return View(warehouse) End Function ' GET: Warehouses/Edit/5 Function Edit(ByVal id As Integer?) As ActionResult If IsNothing(id) Then Return New HttpStatusCodeResult(HttpStatusCode.BadRequest) End If Dim warehouse As Warehouse = db.Warehouses.Find(id) If IsNothing(warehouse) Then Return HttpNotFound() End If PopulateSiteList(warehouse.SiteID) Return View(warehouse) End Function ' POST: Warehouses/Edit/5 'To protect from overposting attacks, enable the specific properties you want to bind to, for 'more details see https://go.microsoft.com/fwlink/?LinkId=317598. <HttpPost()> <ValidateAntiForgeryToken()> Function Edit(<Bind(Include:="Id,WarehouseID,WarehouseName")> ByVal warehouse As Warehouse) As ActionResult If ModelState.IsValid Then db.Entry(warehouse).State = EntityState.Modified db.SaveChanges() Return RedirectToAction("Index") End If PopulateSiteList(warehouse.SiteID) Return View(warehouse) End Function ' GET: Warehouses/Delete/5 Function Delete(ByVal id As Integer?) As ActionResult If IsNothing(id) Then Return New HttpStatusCodeResult(HttpStatusCode.BadRequest) End If Dim warehouse As Warehouse = db.Warehouses.Find(id) If IsNothing(warehouse) Then Return HttpNotFound() End If Return View(warehouse) End Function ' POST: Warehouses/Delete/5 <HttpPost()> <ActionName("Delete")> <ValidateAntiForgeryToken()> Function DeleteConfirmed(ByVal id As Integer) As ActionResult Dim warehouse As Warehouse = db.Warehouses.Find(id) db.Warehouses.Remove(warehouse) db.SaveChanges() 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 Private Sub PopulateSiteList(Optional SelectedSite As Object = Nothing) Dim sitequery = From s In db.Sites Order By s.SiteID Select s ViewData("SiteID") = New SelectList(sitequery, "SiteID", "SiteName", SelectedSite) End Sub End Class 

And this is view for creating warehouses (Only "Using" part):

@Using (Html.BeginForm()) @Html.AntiForgeryToken() @<div class="form-horizontal"> <h4>Warehouse</h4> <hr /> @Html.ValidationSummary(True, "", New With {.class = "text-danger"}) <div class="form-group"> <label for="SiteID" class="custom-select150">Site</label> <div class="col-md-10"> @Html.DropDownList("SiteID", Nothing, htmlAttributes:=New With {.class = "custom-select150"}) @Html.ValidationMessageFor(Function(model) model.SiteID, "", New With {.class = "text-danger"}) </div> </div> <div class="form-group"> @Html.LabelFor(Function(model) model.WarehouseID, htmlAttributes:=New With {.class = "control-label col-md-2"}) <div class="col-md-10"> @Html.EditorFor(Function(model) model.WarehouseID, New With {.htmlAttributes = New With {.class = "form-control"}}) @Html.ValidationMessageFor(Function(model) model.WarehouseID, "", New With {.class = "text-danger"}) </div> </div> <div class="form-group"> @Html.LabelFor(Function(model) model.WarehouseName, htmlAttributes:=New With {.class = "control-label col-md-2"}) <div class="col-md-10"> @Html.EditorFor(Function(model) model.WarehouseName, New With {.htmlAttributes = New With {.class = "form-control"}}) @Html.ValidationMessageFor(Function(model) model.WarehouseName, "", New With {.class = "text-danger"}) </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 

Now the problem is that when I add a new warehouse, Site_Id is NULL in database (I have filled the first one manually via SSMS):

enter image description here

This causes empty site name/ID when I load warehouses from database. How can I fix this?

2
  • You should not be using EF entity classes as DTOs or ViewModels for form-bindings - it introduces security vulnerabilities ("overposting") as well as makes software harder to maintain because you can't version DTOs and forms separately from entity classes. Commented Jul 3, 2022 at 12:10
  • 1
    The issue is Public SiteID As Integer is a field, not a property. EF requires DB table columns to be mapped to properties, not fields. Also, why are you using the very, very old-style of excessively verbose VB properties instead of the far more succinct auto-implemented property syntax: learn.microsoft.com/en-us/dotnet/visual-basic/programming-guide/… Commented Jul 3, 2022 at 12:11

1 Answer 1

1

Entity Framework only binds database TABLE and VIEW columns to properties, not fields (EF Core does support updating fields directly, but I assume you're using EF, not EF Core, because to my knowledge EF Core does not support VB.NET).

Change your class' member from this....

 <Display(Name:="Site"), Required> Public SiteID As Integer 

...to this:

 <Display(Name:="Site"), Required> Public Property SiteId As Integer 

...or this (if you're a verbose-code masochist...):

 Private mSiteId As Integer ' [etc] <Display(Name:="Site"), Required> Public Property SiteId As Integer Get Return mSiteId End Get Set(value As Integer) mSiteId = value End Set End Property 

Other points:

  • You should not be using EF entity classes as DTOs or ViewModels for form-bindings - it introduces security vulnerabilities ("overposting") as well as makes software harder to maintain because you can't version DTOs and forms separately from entity classes.
  • Validation is not verification. You should not be accessing your database directly from within your IValidatableObject.Validate method: that's the responsibility of your Controller Action or some other part of your application's ASP.NET requesty-processing pipeline - the Validate method is intended to be synchronous and fast which implies you should not be doing any IO in there.
    • Validation is the process of ensuring submitted data conforms with some invariant constraints on that data (e.g. phone-number formatting, non-empty names, etc).
    • Verification involves doing actual leg-work to actually verify that submitted data is correct, instead of merely valid.
      • "Correct" data is a subset of the set of valid data.
Sign up to request clarification or add additional context in comments.

4 Comments

I tried this, but receive this error upon inserting records: SqlException: The INSERT statement conflicted with the FOREIGN KEY constraint "FK_dbo.Warehouses_dbo.Sites_SiteID". The conflict occurred in database "KFI_IPP", table "dbo.Sites", column 'Id'. The statement has been terminated. I checked code and found out that SiteID is set to zero
@MohammadJavahery Ensure SiteId or SiteID is cased consistently (VB.NET is not case-sensitive, which means things break easily...). The .NET convention is to use Id not ID (as it's an abbreviation, not an initialism).
I rechecked all my code and even converted all instances of SiteID to SiteId to ensure that I am working according to conventions, but it didn't work. I used element inspector in Chrome and found out that none of items in dropdown list (select element) is actually selected (none of them has "selected" property set, though Chrome displays one of them selected. Changing selection doesn't affect HTML tag status. I don't know if this may be root cause.
Finally figured out the reason. I had to make some other changes to my code. Many thanks for your help and lots of useful points. I will try to consider them all

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.