ICollection property is empty during OnValidSubmit() - entity-framework-core

I am trying the free community version of Syncfusion Blazor SfGrid for my basic CRUD of a ICollection property. But while I am able to add, edit, delete at runtime (data is perfectly shown in the datagrid), my ICollection property is always empty during OnValidSubmit()...
1st: My data models look like this:
public class Contact
{
public int ID { get; set; }
public string AccountName { get; set; }
public ICollection<ContactPerson> ContactPersons { get; set; }
}
public class ContactPerson
{
public int ID { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
}
2nd: My create.razor uses my ContactForm.razor for reusability
<h1>Create</h1>
<hr />
<div style="margin: 50px">
<ContactForm Contact="contact" OnValidSubmit="#CreateRecord" OnCancelButton="#CancelRecord"/>
</div>
#code {
private Contact contact = new Contact
{
ContactPersons = new ContactPerson[0],
ContactAddresses = new ContactAddress[0]
};
void CancelRecord()
{
uriHelper.NavigateTo("contact");
}
async Task CreateRecord()
{
await http.PostAsJsonAsync("api/Contact", contact);
uriHelper.NavigateTo("contact");
}
}
3rd: My ContactForm.razor looks like this (which I use for creating and editing a record):
#if (Contact == null)
{
<text>Loading...</text>
}
else
{
<EditForm Model="#Contact" OnValidSubmit="#OnValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-row">
<div class="form-group col-md-3">
<SfTextBox #bind-Value="#Contact.AccountName" Placeholder="Account Name"></SfTextBox>
<ValidationMessage For="#(() => Contact.AccountName)"></ValidationMessage>
</div>
</div>
<!-- List of Contact Persons -->
<div class="form-row">
<div class="form-group col-md-10">
<SfGrid DataSource="#Contact.ContactPersons" Toolbar="#(new List<string>() { "Add", "Edit", "Delete", "Cancel", "Update" })" AllowPaging="true">
<GridEditSettings AllowAdding="true" AllowEditing="true" AllowDeleting="true"></GridEditSettings>
<GridColumns>
<GridColumn Field=#nameof(ContactPerson.ID) IsPrimaryKey="true" HeaderText="ID"></GridColumn>
<GridColumn Field=#nameof(ContactPerson.FirstName) HeaderText="First Name"></GridColumn>
<GridColumn Field=#nameof(ContactPerson.MiddleName) HeaderText="Middle Name"></GridColumn>
<GridColumn Field=#nameof(ContactPerson.LastName) HeaderText="Last Name"></GridColumn>
</GridColumns>
</SfGrid>
</div>
</div>
<!-- Post and Back Link -->
<hr />
<div>
<SfButton type="submit" CssClass="e-primary">Sumbit</SfButton>
<SfButton type="button" CssClass="e-danger" #onclick="#OnCancelButton">Cancel</SfButton>
</div>
</EditForm>
}
#code {
[Parameter] public Contact Contact { get; set; }
[Parameter] public EventCallback OnValidSubmit { get; set; }
[Parameter] public EventCallback OnCancelButton { get; set; }
}
The problem I encounter is that Contact.ContactPersons is empty when I pass it to my controller (webapi), and when I break at CreateRecord() which is `` OnValidSubmit, and watch the variable Contact.ContactPersons in VS2019, the variable is empty and says
Unable to Evaluate.
Please help me understand what's going on.

Issue not related to Blazor...
Since ContactPersons is of ICollection<ContactPerson> type, you should instantiate the ContactPersons field with a collection of ContactPerson objects like this:
private Contact contact = new Contact
{
ContactPersons = new List<ContactPerson>()
};

Related

Insert into DB from ASP.Net form

I have an issue when I want to insert an object into the database.
My model is Colis class which has a foreign key to ZoneReserve (ZoneReserveId), which has a foreign key on Reserve (ReserveId).
In my form I choose an existing ZoneReserve and Reserve, but when I post my form, new lines are created in DB, in table ZoneReserve and Reserve. Entity framework do not retrieve the existing line or I don't know...
I don't know if I'm clear enough, sorry for my english ;)
Do you have any advice ? I'm stuck et I tried everything :(
Thank you guys
Colis Model Class :
public class Colis
{
public int ColisId { get; set; }
[Display(Name = "Code barre du colis")]
public string CodeBarreColis { get; set; }
public bool IndAVendreColis { get; set; }
public virtual TypeColis TypeColis { get; set; }
[Display(Name = "Type de colis")]
public int TypeColisId { get; set; }
public ZoneReserve ZoneReserve { get; set; }
[Display(Name = "Emplacement du colis")]
public int ZoneReserveId { get; set;
}
ZoneReserve Model Class :
public class ZoneReserve
{
public int Id { get; set; }
public string NomZoneReserve { get; set; }
public Reserve Reserve { get; set; }
public int ReserveId { get; set; }
}
Reserve Model Class :
public class Reserve
{
[Display(Name = "Réserve")]
public int Id { get; set; }
public string NomReserve { get; set; }
}
My Action in ColisController :
[HttpPost]
public ActionResult CreerColis(Colis colis)
{
_context.Colis.Add(colis);
_context.SaveChanges();
return RedirectToAction("ListeColis");
}
My Form in the view :
#using (Html.BeginForm("CreerColis", "Colis"))
{
<div class="form-group">
#Html.LabelFor(m => m.Colis.CodeBarreColis)
#Html.TextBoxFor(m => m.Colis.CodeBarreColis, new { #class = "form-control" })
</div>
<div class="checkbox">
<label>
#Html.CheckBoxFor(m => m.Colis.IndAVendreColis) A vendre ?
</label>
</div>
<div class="form-group">
#Html.LabelFor(m => m.Colis.TypeColisId)
#Html.DropDownListFor(m => m.Colis.TypeColisId, new SelectList(Model.TypeColis, "Id", "NomTypeColis"), "Selectionner un type", new { #class = "form -control" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Colis.ZoneReserve.Reserve.Id)
#Html.DropDownListFor(m => m.Colis.ZoneReserve.Reserve.Id, new SelectList(Model.Reserve, "Id", "NomReserve"), "Selectionner une zone", new { #class = "form -control" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Colis.ZoneReserveId)
#Html.DropDownListFor(m => m.Colis.ZoneReserve.Id, new SelectList(Model.ZoneReserve, "Id", "NomZoneReserve"), "Selectionner une zone", new { #class = "form -control" })
</div>
<button type="submit" class="bt, btn-primary">Enregistrer</button>
}
<script src="~/Scripts/jquery-3.4.1.min.js"></script>
<script>
$(document).ready(function () {
$("#Colis_ZoneReserve_Reserve_Id").change(function () {
$.get("/ZoneReserve/ListeZoneReserveParReserve", { ReserveId: $("#Colis_ZoneReserve_Reserve_Id").val() }, function (data) {
$("#Colis_ZoneReserve_Id").empty();
$.each(data, function (index, row) {
$("#Colis_ZoneReserve_Id").append("<option value='" + row.Id + "'>" + row.NomZoneReserve+ "</option>")
});
});
})
});
</script>
It looks like your razor page is posting info about navigation properties of the Colis object to the controller and creating the full objects instead of creating a new Colis object with just the int foreign key specified.
As is, when posted, '''colis.ZoneReserve''' is not null nor is '''colis.ZoneReserve.Reserve''' reference which tells entity framework to create those related object as well when you .Add(colis) to the context.
[HttpPost]
public ActionResult CreerColis(Colis colis)
{
_context.Colis.Add(colis);
_context.SaveChanges();
return RedirectToAction("ListeColis");
}
You are POSTing unintended parameters to your controller, specifically '''Colis.ZoneReserve.Id''' and '''Colis.ZoneReserve.Reserve.Id''' as you BOUND TO in your razor page (see comments in code):
<div class="form-group">
#Html.LabelFor(m => m.Colis.ZoneReserve.Reserve.Id)
<!-- DropDownListFor m.Colis.ZoneReserve.ReserveId will send that (navigation path) value to the server. //-->
#Html.DropDownListFor(m => m.Colis.ZoneReserve.Reserve.Id, new SelectList(Model.Reserve, "Id", "NomReserve"), "Selectionner une zone", new { #class = "form -control" })
</div>
<div class="form-group">
#Html.LabelFor(m => m.Colis.ZoneReserveId)
<!-- DropDownListFor m.Colis.ZoneResearch.Id will send that navigation property to the server //-->
#Html.DropDownListFor(m => m.Colis.ZoneReserve.Id, new SelectList(Model.ZoneReserve, "Id", "NomZoneReserve"), "Selectionner une zone", new { #class = "form -control" })
</div>
To fix your razor page (and not send unintended values to the server)
change the first drop down list for Reserve to NOT be for anything it'll bind to on the server (you don't even need to POST it's value if you can strip it before submit), one way is to change it's name to something meaningless such as "UnnecessaryData" that won't map in the controller when posted (pseudo-code, not tested)
<div class="form-group">
#Html.LabelFor(m => m.Colis.ZoneReserve.Reserve.Id)
#Html.DropDownList(new SelectList(Model.Reserve, "Id", "NomReserve"),
"Selectionner une zone",
new { #class = "form-control", name = "UnnecessaryData" })
</div>
Change the second drop-down-list to map to the correct property on the Colis object, notice all I did was change m => m.Colis.ZoneReserve.Id to the FK property of Colis m => m.Colis.ZoneReserveId:
<div class="form-group">
#Html.LabelFor(m => m.Colis.ZoneReserveId)
#Html.DropDownListFor(m => m.Colis.ZoneReserveId, new SelectList(Model.ZoneReserve, "Id", "NomZoneReserve"), "Selectionner une zone", new { #class = "form -control" })
</div>
When you POST the form, your Colis object in the controller should have a NULL ZoneReserve property and a non-zero ZoneReserveId property - this will prevent the other data records from being created by entity framework.
Note: You can also simply strip the data from the Colis in the POST controller - but that doesn't correct your implementation on the client razor page that's sending unintended structure to the server in the POST method.
Also note: Because you don't validate that the navigation properties of Colis are NULL in the controller, a malicious user COULD create a lot of crap data on the server by POSTing full object tree data that'll be added with the controller method as implemented.

Blazor validation over a MongoDB Datamodel

I try to link my MongoDB class model to my Blazor page component. I tried to keep all the System.ComponentModel.DataAnnotations.ValidationAttribute to an interface and let the 'real' class with the Bson decoration apart as:
public interface ITestIt
{
int id { get; set; }
[Required(ErrorMessage = "Material cost is required")]
[StringLength(5, ErrorMessage = "Name is too long.")]
string MyName { get; set; }
}
public class TestIt : ITestIt
{
[BsonId]
public int id { get; set; }
public string MyName { get; set; }
}
And include it in my page as:
<h1>Hello, world!</h1>
Welcome to your new app.
<EditForm Model=#testIt OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<label for="Name">Name</label>
<InputText #bind-Value=testIt.MyName class="form-control" id="Name" />
<ValidationMessage For="() => testIt.MyName" Description="Salut" />
</div>
<input type="submit" class="btn btn-primary" value="Save" />
</EditForm>
#code{
private ITestIt testIt;
private bool IsDone = false;
protected override async Task OnInitializedAsync()
{
if (IsDone) return;
testIt = new TestIt();
IsDone = true;
}
private void HandleValidSubmit()
{
Console.WriteLine("OnValidSubmit");
}
But it don't work, what is the best way to separate the both without having to decorate all my data model with the DataAnnotations tags and not having to copy one by one each property one by one to and other object?
Thanks!
I don't think Blazor reflects over data annotations of interfaces, only the properties of the implementing object.
I keep my validations in a separate project completely. To do this I use FluentValidation.
You can write a component that accepts an EditContext as a cascading parameter, hook into the events where it requests validation, and execute the FluentValidation code.
Or you can use a pre-made library such as https://www.nuget.org/packages/PeterLeslieMorris.Blazor.FluentValidation/

Passing collection data from partial view to controller

I'm need to pass dynamically created data from a partial view to the controller on submit from the main form.
Here's the Action that returns the partial view:
[HttpGet]
public virtual PartialViewResult AddItem()
{
var item = new QuotedItem();
ViewBag.StockID = new SelectList(db.StockItems.OrderBy(s => s.Name), "StockID", "Name");
return PartialView("EditorTemplates/QuotedItem", item);
}
Here's the EditorTemplate for QuotedItem:
#model StockSystem.Models.QuotedItem
<div id="itemRow">
<span>
#Html.DropDownListFor(model => model.StockID, null)
#Html.ValidationMessageFor(model => model.StockID, "", new { #class = "text-danger" })
</span>
<span>
#Html.EditorFor(model => model.Price)
#Html.ValidationMessageFor(model => model.Price, "", new { #class = "text-danger" })
</span>
<span>
#Html.EditorFor(model => model.Quantity)
#Html.ValidationMessageFor(model => model.Quantity, "", new { #class = "text-danger" })
</span>
</div>
Here's the View:
#model StockSystem.Models.Quote
#{
ViewBag.Title = "Create";
}
<h2>New Quote for #ViewBag.Customer</h2>
<hr />
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.Hidden("CustomerID", (object)ViewBag.CustomerID)
<div class="addItem">
#Ajax.ActionLink(" ", "AddItem",
new AjaxOptions
{
UpdateTargetId = "editorRows",
InsertionMode = InsertionMode.InsertAfter,
HttpMethod = "GET"
})
</div>
<div id="editorRows">
#Html.EditorFor(q => q.QuotedItems)
</div>
<p></p>
<div>
<input class="add" type="submit" value="" />
<a class="back" href="#Url.Action("Index", "Quotes", new { id = ViewBag.CustomerID })"></a>
</div>
}
Here's the Create Action:
public ActionResult Create(int id)
{
var customer = db.Customers.Find(id);
ViewBag.CustomerID = id;
ViewBag.Customer = customer.CustomerName;
var quote = new Quote() { QuotedItems = new List<QuotedItem>() };
return View(quote);
}
Here's the EF model for QuotedItem:
public partial class QuotedItem
{
public int QuotedItemID { get; set; }
public int QuoteID { get; set; }
public int StockID { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
public virtual Quote Quote { get; set; }
public virtual StockItem StockItem { get; set; }
}
And Quote:
public partial class Quote
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public Quote()
{
this.QuotedItems = new HashSet<QuotedItem>();
}
public int QuoteID { get; set; }
public int CustomerID { get; set; }
public int UserID { get; set; }
public System.DateTime QuoteDate { get; set; }
public string QuoteRef { get; set; }
public virtual Customer Customer { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<QuotedItem> QuotedItems { get; set; }
public virtual User User { get; set; }
}
I can add items to the page but they are not added to the quote on submit.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "QuoteID,CustomerID,UserID,QuoteDate,QuoteRef,QuotedItems")] Quote quote) //, List<QuotedItem> items
{
if (ModelState.IsValid)
{
//quote.QuotedItems is empty, how do I bind this data and save to database?
db.Quotes.Add(quote);
db.SaveChanges();
return RedirectToAction("Index", new { id = quote.CustomerID });
}
return View(quote);
}
The collection is not being sent to the controller. How is this done?
Thanks
Edit: I was thinking about trying this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "QuoteID,CustomerID,UserID,QuoteDate,QuoteRef,QuotedItems")] Quote quote, List<QuotedItem> items)
{
if (ModelState.IsValid)
{
quote.QuotedItems = items;
db.Quotes.Add(quote);
db.SaveChanges();
return RedirectToAction("Index", new { id = quote.CustomerID });
}
return View(quote);
}
But I thought there must be a better way. If I were try this approach I'm still not entirely sure how to send this to the controller, I assume as a parameter in Html.BeginForm()? I'm fairly new to MVC and still getting to grips with the conventions.
By using the Bind attribute in your method signature, you are preventing the QuotedItems property from being posted to the controller action.
public ActionResult Create([Bind(Include = "QuoteID,CustomerID,UserID,QuoteDate,QuoteRef")] Quote quote)
Adding the QuotedItems property name to the include list should solve this:
public ActionResult Create([Bind(Include = "QuoteID,CustomerID,UserID,QuoteDate,QuoteRef,QuotedItems")] Quote quote)
EDIT
Without seeing your QuotedItem editor template, it's hard to say for sure, but it sounds like your issue might be that each item added to your list doesn't have a unique id set on the elements that make up that item. Without this, the model binder will struggle to work out which element belongs to which object in the collection.
Take a look at these two articles and see if they help:
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
https://www.mattlunn.me.uk/blog/2014/08/how-to-dynamically-via-ajax-add-new-items-to-a-bound-list-model-in-asp-mvc-net/
Edit 2
Each element in your template should be given a unique ID, an index is a good way to do this, so are item GUIDs (if you have them); but as long as each element ID is unique to that item it should work.
#model StockSystem.Models.QuotedItem
<div id="itemRow">
<span>
#Html.DropDownListFor(model => model.StockID, null)
#Html.ValidationMessageFor(model => model.StockID, "", new { #class = "text-danger" })
</span>
<span>
#Html.EditorFor(model => model.Price, new { htmlAttributes = new { id = $"[{YOURINDEX}].Price" } })
#Html.ValidationMessageFor(model => model.Price, "", new { #class = "text-danger" })
</span>
<span>
#Html.EditorFor(model => model.Quantity, new { htmlAttributes = new { id = $"[{YOURINDEX}].Quantity" } })
#Html.ValidationMessageFor(model => model.Quantity, "", new { #class = "text-danger" })
</span>
In the code above the [YOURINDEX] value would need to be incremented for each new item added to the collection.
How you implement and increment that index is up to you, but I would suggest reading the two articles I linked to previously to get an understanding of why this is necessary.

Retrieve course id and course name selected in edit view(get) when edit data

Image for what i need
i need when edit employee data show to me courses that submit before
this is my secnario
in create view
courses drowpdown user select three courses
Delphi
Flash
c++
then when click submit button for employee.it will have 3 courses submitted
I need to retrieve courses that submitted before for employee martin
from database
in edit view(get)
I need to show :
course name course id
Delphi 1
Flash 2
c++ 3
and CourseId will be hidden
what i write in edit view to show selected course
my code as below for view and controller
Edit.cs.html view
#model WebCourse.Models.Customemployee2
<body>
<div>
#using (Html.BeginForm())
{
<div>
Name:#Html.TextBoxFor(a => a.Name)
<br />
Courses:#Html.DropDownList("CourseId")
<table id="tb2"></table>
<br />
<input type="submit" />
</div>
}
</div>
</body>
in empcourse controller
namespace WebCourse.Controllers
{
public class empcourseController : Controller
{
mycourseEntities db = new mycourseEntities();
// GET: empcourse
public ActionResult Edit(int id)
{
Employee old = db.Employees.Find(id);
if (old != null)
{
var vm = new Customemployee2();
vm.Name = old.Name;
ViewBag.CourseId = new SelectList(db.Courses.ToList(), "Id", "CourseName");
return View(vm);
}
}
}
}
model view Customemployee2
namespace WebCourse.Models
{
public class Customemployee2
{
public string Name { get; set; }
public int CourseId { get; set; }
public List<EmployeeCourse> Courses { get; set; }
}
}
I suggest you update your edit view model to have a collection of CourseVm
public class EditEmployeeVm
{
public int Id { set; get; }
public string Name { get; set; }
public List<SelectListItem> Courses { get; set; }
public int[] CourseIds { set; get; }
public List<CourseVm> ExistingCourses { set; get; }
}
public class CourseVm
{
public int Id { set; get; }
public string Name { set; get; }
}
Now in your Edit GET action, populate the ExistingCourse collection.
public ActionResult Edit(int id)
{
var vm = new EditEmployeeVm { Id=id };
var emp = db.Employees.FirstOrDefault(f => f.Id == id);
vm.Name = emp.Name;
vm.ExistingCourses = db.EmployeeCourses
.Where(g=>g.EmployeeId==id)
.Select(f => new CourseVm { Id = f.CourseId,
Name = f.Course.Name}).ToList();
vm.CourseIds = vm.ExistingCourses.Select(g => g.Id).ToArray();
vm.Courses = db.Courses.Select(f => new SelectListItem {Value = f.Id.ToString(),
Text = f.Name}).ToList();
return View(vm);
}
Now in your Edit view, just loop through the ExistingCourses collection and display it.
#model EditEmployeeVm
#using (Html.BeginForm())
{
#Html.HiddenFor(g=>g.Id)
#Html.LabelFor(f=>f.Name)
#Html.DropDownList("AvailableCourses" ,Model.Courses,"Select")
<h4>Existing courses</h4>
<div id="items"></div>
foreach (var c in Model.ExistingCourses)
{
<div class="course-item">
#c.Name Remove
<input type="text" name="CourseIds" value="#c.Id" />
</div>
}
<input type="submit"/>
}
You should have the below javascript code also in the view to handle the remove and add of a course.
#section scripts
{
<script>
$(function() {
$(document).on("click",".remove",function(e) {
e.preventDefault();
$(this).closest(".course-item").remove();
});
$('#AvailableCourses').change(function() {
var val = $(this).val();
var text =$("#AvailableCourses option:selected").text();
var existingCourses = $("input[name='CourseIds']")
.map(function() { return this.value; }).get();
if (existingCourses.indexOf(val) === -1) {
// Not exist. Add new
var newItem = $("<div/>").addClass("course-item")
.append(text+' Remove ');
newItem.append('<input type="text" name="CourseIds"
value="' + val + '" />');
$("#items").append(newItem);
}
});
})
</script>
}
So when you submit the form, The CourseIds property will have the course ids (as an array).
[HttpPost]
public ActionResult Edit(EditEmployeeVm model)
{
// to do : check model.ExistingCourses and save the data now
}
BTW, The same can be used for your create form.

Credit Card Dropdownlist

I a working on accepting payments with credit cards through PayPal, for the card type I have an enum
namespace AccessorizeForLess.Enums
{
public static class Enums
{
public enum CreditCardtTypes
{
Visa = 0,
Mastercard = 1,
Discover = 2,
AMEX = 3
}
}
}
Then in my form I have this
<div class="form-group">
#Html.LabelFor(model => model.CardType, "Card Type", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EnumDropDownListFor(model => model.Cardtypes, "- Please Select -", new { #class = "form-control", #style = "width:155px;" })
#Html.ValidationMessageFor(model => model.CardType, "", new { #class = "text-danger" })
</div>
</div>
But everytime I run it card type is null. Anyone got a better solution that I can use for this?
EDIT
This is the model I'm binding to
namespace AccessorizeForLess.ViewModels
{
public class PayWithCCViewModel
{
.... // other proeprties
[Required(ErrorMessage="Card type is required")]
public string CardType { get; set; }
public Enums.Enums.CreditCardtTypes Cardtypes { get; set; }
}
}
Change your property to
[Required(ErrorMessage="Card type is required")]
public CreditCardtTypes CardType { get; set; }
and remove the public Enums.Enums.CreditCardtTypes Cardtypes { get; set; } property.
Then in the view use
#Html.EnumDropDownListFor(model => model.CardType , "- Please Select -", new { .... })
so that your binding to the property of your model (the helper will generate an option for each value in the enum)