Issue with many to many relationship using code first mvc - c#

I created two models of Class and Teacher, I want to configure many to many relationship using code first approach with two models.
When I add information into class and teacher tables, value not insert into TeacherClasses Table using many to many relationship Entity Framework code first approach.
Here code of models and controller below.
This is Class Model
public class Class
{
[Key]
public int ClassId { get; set; }
[Required]
public string ClassName { get; set; }
public virtual ICollection<Teacher> Teachers { get; set; }
}
This is Teacher Model
public class Teacher
{
[Key]
public int TeacherId { get; set; }
[Required]
public string TeacherName { get; set; }
[DataType(DataType.Date)]
public DateTime JoiningDate { get; set; }
public decimal Salary { get; set; }
public string Email { get; set; }
public virtual ICollection<Class> Classes { get; set; }
[NotMapped]
public virtual Class Class { get; set; }
[NotMapped]
public virtual ICollection<int> SelectedClassList { get; set; }
}
This is Teacher Controller
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "TeacherId,TeacherName,JoiningDate,Salary,Email,Classes,Class,SelectedClassList")] Teacher teacher)
{
if (ModelState.IsValid)
{
db.Teachers.Add(teacher);
db.SaveChanges();
teacher.Classes = new Collection<Class>();
foreach (var classId in teacher.SelectedClassList)
{
teacher.Classes.Add(new Class {ClassId = classId });
}
return RedirectToAction("Index");
}
return View(teacher);
}
this is image of database tables
in database class, teacher and teacherclasses tables shown
database
This is View of Teacher
<div class="form-group">
#Html.Label("Class List", htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#*#Html.ListBox("SelectedClassList", ViewData["ClassList"] as MultiSelectList)
#Html.ValidationMessageFor(model => model.Classes, "", new { #class = "text-danger" })*#
#Html.ListBoxFor(model => model.SelectedClassList, new MultiSelectList(ViewBag.ClassList, "value", "text"), new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.SelectedClassList, "", new { #class = "text-danger" })
</div>
</div>

From your description and the way you're saving the data there seem to be two possible scenarios you're trying to achieve:
Insert new teachers and connect them to existing classes.
Connect existing teachers to existing classes.
Whichever it is, the rule is always:
First attach existing entities to the context
Then add new entities
So for the first scenario that would be:
foreach (var classId in teacher.SelectedClassList)
{
var class = new Class { ClassId = classId }; // "stub" entity
db.Classes.Attach(class);
teacher.Classes.Add(class); // Assuming that teacher.Classes isn't null
}
db.Teachers.Add(teacher); // <= Add
db.SaveChanges();
And the second scenario:
db.Teachers.Attach(teacher); // <= Attach
foreach (var classId in teacher.SelectedClassList)
{
var class = new Class { ClassId = classId }; // "stub" entity
db.Classes.Attach(class);
teacher.Classes.Add(class); // Assuming that teacher.Classes isn't null
}
db.SaveChanges();
As you see, you never need to pull existing entities from the database. For creating associations all EF needs tho know is the ID values of the entities at both ends, so working with stub entities is the ideal approach here.
In the first scenario, if you don't attach the classes before adding the teacher, the classes will be marked as Added too, and you'll insert empty class records into the database (if they pass validation at all).

Related

Display FullName on DropdownListFor but submit Id - ASP.NET MVC C#

I'm trying to build a form for information about upcoming exams. My plan is to make a dropdown list which shows a List of teachers to chose from and who will be responsible for the exam.
Here's a simplified version of my Models:
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
}
public class Teacher
{
public int Id { get; set; }
public int PersonId { get; set; }
public Person Person { get; set; }
}
public class Exam
{
public int Id { get; set; }
public string Subject { get; set; }
public DateTime ExamDate { get; set; }
public int TeacherId { get; set; }
public Teacher Teacher { get; set; }
}
My ViewModel:
public class ExamViewModel
{
public Exam Exam { get; set; }
public IEnumerable<Person> People { get; set; }
public IEnumerable<Teacher> Teachers { get; set; }
}
And my Create action from ExamController:
public ActionResult Create()
{
var people = _context.People.ToList();
var teachers = _context.Teachers.ToList();
var viewModel = new ExamViewModel
{
People = people,
Teachers = teachers,
};
return View(viewModel);
}
I'd like to display People.Firstname + " " + People.Surname of all teachers on the dropdown mentioned above, but instead of submitting People.Id I'd like to submit Teachers.Id to Exams.TeacherId
I first tried to displaying a list of the all FirstName before trying displaying FirstName and Surname with the following razor html helper in my Create view but I already failed there as I was only able to access the properties from one single class (Teachers) to use is as dataValueField and dataTextField for new SelectList():
<h2>Plan exam</h2>
#using (Html.BeginForm("Create", "ExamController"))
{
<div class="form-floating mb-3">
#Html.DropDownListFor(m => m.Exam.TeacherId, new SelectList(Model.Teachers, "Id", "FirstName"), "-- Please select --", new { #class = "form-control", #placeholder = "Teacher" })
#Html.LabelFor(m => m.Exam.TeacherId)
</div>
}
I'm quite new to programming so I'd be very very grateful for any kind of help.
in your viewModel you can only send Exams , but in the query to fetch exams you must make a join to fetch for every exam the teacherID , teacherName , teacherSubname,
to Learn about that check this :
here
or this :
here
another thing , in your model ( classes ) definition , i think you have to add a relationship between Exam and Teacher , and between Teacher an People , if you do so it will be easier to fetch only the necessary data , and you will use only one property ( exams ) in your viewModel to send data to the view, to do so check this here
I found a solution:
First I created a second ViewModel with a property Id for Teacher.Id and FullName for Person.FirstName + " " + Person.Surname:
public class TeacherViewModel
{
public int Id { get; set; }
public string FullName { get ; set; }
}
Then I removed the unnecessary IEnumerable<Person> People from my ExamViewModel and added an IEnumerable<> for Teachers of Type TeacherViewModel:
public class ExamViewModel
{
public Exam Exam { get; set; }
public IEnumerable<TeacherViewModel> Teachers { get; set; }
}
I created a join as mentionen at step 1, converted it to a list in the new TeacherViewModel:
public ActionResult Create()
{
var teachers = (from t in _context.Teachers
join p in _context.People
on t.PersonId equals p.Id
select new TeacherViewModel
{
Id = t.Id,
FullName = p.FirstName + " " + p.Surname,
}).ToList();
var viewModel = new ExamViewModel
{
Teachers = teachers,
};
return View(viewModel);
}
I added a Save() method to the controller:
public ActionResult Save(Exams exam)
{
_context.Exams.Add(exam);
_context.SaveChanges();
return RedirectToAction("Create", "Exams");
}
Added the submit button to the View and changed the action name beside Html.BeginForm( to Save as it will handle saving the variables from the form.
#using (Html.BeginForm("Save", "Exams"))
{
<div class="form-floating mb-3">
#Html.DropDownListFor(m => m.Exam.TeacherId, new SelectList(Model.Teachers, "Id", "FullName"), "-- please select --", new { #class = "form-control", #placeholder = "Teacher" })
#Html.LabelFor(m => m.Exam.TeacherId)
</div>
<button type="submit" class="btn btn-primary">Submit</button>
}

'System.String[]' cannot be mapped to a primitive type [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 2 years ago.
Improve this question
I want to store array in database column.
public string[] carList { get; set; }
But it gives me the error:
The property 'carList' on the type 'SandStormAdmin.Models.PricePlan' has a property type of 'System.String[]' which cannot be mapped to a primitive type.
It only gives me this error on run time, i am using microsoft sql server to store data. carList data Type store in table is of nvarchar(50)
My Model class
public partial class PricePlan
{
public int PricePlanID { get; set; }
public int CarID { get; set; }
public string PricePlanName { get; set; }
public Nullable<System.DateTime> DateRangeFrom { get; set; }
public Nullable<System.DateTime> DateRangeTo { get; set; }
public string DayRangeFrom { get; set; }
public string DayRangeTo { get; set; }
public decimal Amount { get; set; }
public string[] carList { get; set; }
public virtual Car Car { get; set; }
}
Create.cshtml
<div class="form-group">
#Html.LabelFor(model => model.carList, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownList("carList", ViewBag.carlistname as SelectList, "Please Select Fruit name", new { #class = "form-control chosen-select", #multiple = "true" })
#Html.ValidationMessageFor(model => model.carList, "", new { #class = "text-danger" })
</div>
</div>
PricePlanController
public ActionResult Create()
{
//var result = (from carID in db.Cars select carID).ToList();
//var getcarlist = db.Cars.ToList();
SelectList list = new SelectList(db.Cars, "CarID", "Make");
ViewBag.carlistname = list;
//ViewBag.CarID = new SelectList(db.Cars, "CarID", "Make");
return View();
}
// POST: PricePlans/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see https://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "PricePlanID,CarID,PricePlanName,DateRangeFrom,DateRangeTo,DayRangeFrom,DayRangeTo,Amount,carList")] PricePlan pricePlan)
{
if (ModelState.IsValid)
{
db.PricePlans.Add(pricePlan);
db.SaveChanges();
return RedirectToAction("List");
}
ViewBag.CarID = new SelectList(db.Cars, "CarID", "Make", pricePlan.Car);
return View(pricePlan);
}
Database table data types picture
[1]: https://i.stack.imgur.com/Y2U9U.png
I think the problem is that you are trying to store an array of strings, which potentially contains multiple values, in a database column, which can only contain a single value.
There are two ways to solve this. The easiest would be to serialise the array into a single value before storing it. The following snippet uses string.Join to create a semicolon-separated string of all the values in carList:
var serialisedCarList = string.Join(";", carList);
The other option, which I would say is more idiomatic for relational databases, is to introduce a new table for the car values, and give them a foreign key to link them to a PricePlan record. This creates a one-to-many relationship from PricePlan records to Car records, effectively allowing a PricePlan to "contain" multiple car values.

Object reference not set to an instance of an object., model is null

I am trying to display a checklist that gets data from MySQL Database and displays it in a view and updates the value of the variable (IsChecked) of each element in the table by whether we have checked the amenity or not (i am displaying some amenities). The model of the view is Hotel_5.ViewModel.BookingRoom, where BookingRoom is a custom model i created where i use multiple models. I get the exception at Model.AmenitiesList.Count(). The model is null.
This is my view
<div class="form-group">
#for (var i = 0; i < Model.AmenitiesList.Count(); i++)
{
#Html.CheckBoxFor(m => m.AmenitiesList[i].IsChecked, new { #class = "form-control" });
<label>#Model.AmenitiesList[i].amenityType</label>
//If you need to hide any values and get them in your post
#Html.HiddenFor(m => m.AmenitiesList[i].AmenityId)
#Html.HiddenFor(m => m.AmenitiesList[i].AmenityPrice)
}
</div>
This is my ViewModel
public class BookingRoom
{
public Bookings bookings { get; set; }
public Rooms rooms { get; set; }
public List<Amenities> AmenitiesList { get; set; } = new List<Amenities>();
}
This is my Amenities Model
public class Amenities
{
[Key]
public int AmenityId { get; set; }
public double AmenityPrice { get; set; }
public AmenityType amenityType { get; set; }
public bool IsChecked { get; set; }
}
public enum AmenityType
{
tv,
wi_fi,
hair_dryer,
help
}
When Querying you should Include its AmenitiesList too, otherwise it will be null:
In Controller:
var bookingRoom = context.BookingRooms.Include(b => b.AmenitiesList).FirstOrDefault(b => b.Id == someId);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
please note that what I queried might not be what you want, it is just to demonstrate how to use Include() and also you should add using Microsoft.EntityFrameworkCore.

Drop down list value doesn't seem to make it into the database for MVC 5

I'm trying to create a drop down list that will let you specify which company an employee works for. Below is my EmployeeViewModel. CompanyId is a foreign-key constrained database field which links to the Company table.
public class EmployeeViewModel
{
public EmployeeViewModel()
{
}
public EmployeeViewModel(Employee Employee, string CompanyName)
{
Initialize(Employee, CompanyName);
}
public EmployeeViewModel(Employee Employee, CliFFEntities db)
{
Initialize(Employee, db.Companies.Find(Employee.CompanyId).Name);
}
private void Initialize(Employee employee, string CompanyName)
{
this.Id = employee.Id;
this.Name = employee.Name;
this.CompanyId = employee.CompanyId;
this.InternalId = employee.InternalId;
this.CompanyName = CompanyName;
}
public int Id { get; set; }
public string Name { get; set; }
public Nullable<int> InternalId { get; set; }
[Display(Name = "Company")]
public int CompanyId { get; set; }
public String CompanyName { get; set; }
//public List<Company> CompanyList { get; set; }
public IEnumerable<SelectListItem> CompanyList { get; set; } //to be set in controller on an as-needed basis
}
Relevant part of the Employees controller:
// GET: Employees/Create
public ActionResult Create()
{
var evm = new EmployeeViewModel();
evm.CompanyList = new SelectList(db.Companies, "Id", "Name");
return View(evm);
}
Relevant part of my create view:
<div class="form-group">
#Html.LabelFor(m => m.CompanyId)
<div class="col-md-10">
#Html.DropDownListFor(m => m.CompanyId, Model.CompanyList)
#Html.ValidationMessageFor(m => m.CompanyId)
</div>
</div>
So all that seems to work fine. In fact, when I look at the post data it even sets the CompanyId to the correct value, corresponding to a company ID in the database.
But if I enforce the foreign key on the database side, it throws an error because that CompanyId doesn't seem to make it into the Employee record. If I turn off the FK constraint, the CompanyId just shows up as a 0.
What gives?
Welp, looks like I'm so new to MVC 5 that I didn't realize that there's such thing as a post method. Here's what I changed it to to map the EmployeeViewModel back to Employee:
// POST: Employees/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]
public ActionResult Create([Bind(Include = "Id,Name,CompanyId,InternalId")] EmployeeViewModel evm)
{
var employee = new Employee { Name = evm.Name, CompanyId = evm.CompanyId, InternalId = evm.InternalId };
if (ModelState.IsValid)
{
db.Employees.Add(employee);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(employee);
}

EF 6 SaveChanges with multiple references to same (changed) object

I have a class with two references to the same class. When updating the main class, I may also update the referenced class. When I have two references to the same (modified) object, I get an InvalidOperationException:
Attaching an entity of type 'ns.entity' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.
Simple example:
public class Example {
public int OldestFriendId {get; set;}
public int BestFriendId {get; set;}
public virtual Friend Oldest {get; set; }
public virtual Friend Best {get; set; }
}
If while updating Example, I want to update the Middle name of my Oldest/Best friend, it works as long as they aren't the same. But if they are the same, then I get the above exception.
I can't figure out how to get this to work. I've tried setting references to null, saving them independently of the parent class, setting all references in them to null (EF is automatically creating two list of Examples in Friend).
How can I save an object that has changed when there are multiple references to it?
UPDATE: not yet working the way I want, but I have had some progress after removing the list of Examples from Friend. Also, the update is the result of a POST. Still investigating...
As sample code was asked for...this is from a post on a web app, no change was actually made
public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId, Oldest, Best")] Example example)
{
if (ModelState.IsValid)
{
using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
{
using (var _db = new exampleEntities())
{
//example.Best= example.Oldest; // this line would allow the update to work.
//next line is where the exception occurs
_db.Entry(example).State = EntityState.Modified;
_db.SaveChanges();
}
}
}
}
The EditorFor template:
#model Testing.Friend
<div class="col-md-10">
#Html.HiddenFor(model => model.FriendId)
#Html.EditorFor(model => model.FirstName)
#Html.EditorFor(model => model.LastName)
</div>
The Edit view for Example
#model Testing.Example
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Example</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
#Html.HiddenFor(model => model.ExampleId)
<div class="form-group">
#Html.LabelFor(model => model.OldestFriendId, "OldestFriendId", htmlAttributes: new { #class = "control-label col-md-2" })
#Html.HiddenFor(model => model.OldestFriendId)
#Html.EditorFor(model => model.Oldest)
</div>
<div class="form-group">
#Html.LabelFor(model => model.BestFriendId, "BestFriendId", htmlAttributes: new { #class = "control-label col-md-2" })
#Html.HiddenFor(model => model.BestFriendId)
#Html.EditorFor(model=> model.Best)
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
}
EDIT
The most likely cause is because when you retrieve the object back, it deserializes the 2 friends as 2 completely different objects (even when they are the same). Same problem as below, but rather than EF deserializing into 2 objects, ASP.NET MVC is doing it.
What you will have to do is something like the following:
Check if the 2 Friend ID's are the same (as ID is the PK). If not continue as normal
If they have the same ID, check if the 2 friend objects are the same.
If they are the same go to step 5.
Combine the changes together, however you want to deal with conflicts.
Set one of the Freinds to the same as the other Friend reference, e.g. Best = Oldest
SaveChanges()
Original Answer
My guess is that this is the classic problem of Include when you are retrieving the data.
When you do
Context.Examples.Include(x => x.Oldest).Include(x => x.Best).ToList()
What is happening is EF will create TWO objects of friend(Oldest and Best), even if they point to the same record. This is a known problem with include.
So when you go to save after update, EF sees them as 2 separate entities with the same key (and data) and complains.
If this is the case you have a couple of options:
Retrieve a list of all Friends for the current example and then the Example without the include
Let EF use LazyLoading and load the Friends when and as you need them.
My solution to the problem was to stop binding the whole object, and bind to the individual objects.
public ActionResult SaveEdit(int id, [Bind(Include = "OldestFriendId, BestFrinedId")] Example example,
Bind(Prefix="Oldest", Include = "FriendId, FirstName, MiddleName, LastName")] Friend oldest,
Bind(Prefix="Best", Include = "FriendId, FirstName, MiddleName, LastName")] Friend best) {
if (ModelState.IsValid)
{
using (((WindowsIdentity)ControllerContext.HttpContext.User.Identity).Impersonate())
{
using (var _db = new exampleEntities())
{
// do whatever processing you want on best and/or oldest
example.BestFriendId = best.FriendId;
example.OldestFriendId = oldest.FriendId;
_db.Entry(example).State = EntityState.Modified;
_db.SaveChanges();
}
}
}
}
EDIT: Replaced with full sample code
This example works for me.
I think is does what you are trying.
using System;
using System.Data.Common;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Migrations;
namespace Ef6Test {
public class Program {
public static void Main(string[] args) {
ExecDb1();
}
private static void ExecDb1() {
Database.SetInitializer(new MigrateDatabaseToLatestVersion<Ef6Ctx, Ef6MigConf>());
WhichDb.DbName = "MSAMPLEDB";
WhichDb.ConnType = ConnType.CtxViaDbConn;
var sqlConn = GetSqlConn4DBName(WhichDb.DbName);
var context = new Ef6Ctx(sqlConn);
context.Database.Initialize(true);
Console.WriteLine(WhichDb.DbName, context.Database.Exists() );
AddJunk(context);
}
public static class WhichDb {
public static string DbName { get; set; }
public static string ConnectionName { get; set; }
public static ConnType ConnType { get; set; }
}
public enum ConnType {
CtxViaDbConn,
CtxViaConnectionName
}
private static void AddJunk(DbContext context) {
var friend = new Friend();
friend.Name = "Fred";
friend.Phone = "555-1232424";
context.Set<Friend>().Add(friend);
context.SaveChanges();
// break here and check db content.
var eg = new Example();
eg.Best = friend; // set them equal
eg.Oldest = friend;
friend.Name = "Fredie"; // change the name of the fly
friend.Phone = "555-99999"; // and phone is also different
context.Set<Example>().Add(eg); Add the new example
context.SaveChanges();
// result... 2 records.
// The original friend record should be chnaged
}
public static DbConnection GetSqlConn4DBName(string dbName) {
var sqlConnFact = new SqlConnectionFactory(
"Data Source=localhost; Integrated Security=True; MultipleActiveResultSets=True");
var sqlConn = sqlConnFact.CreateConnection(dbName);
return sqlConn;
}
}
public class MigrationsContextFactory : IDbContextFactory<Ef6Ctx> {
public Ef6Ctx Create() {
switch (Program.WhichDb.ConnType) {
case Program.ConnType.CtxViaDbConn:
var sqlConn = Program.GetSqlConn4DBName(Program.WhichDb.DbName); //
return new Ef6Ctx(sqlConn);
case Program.ConnType.CtxViaConnectionName:
return new Ef6Ctx(Program.WhichDb.ConnectionName);
default:
throw new ArgumentOutOfRangeException();
}
}
}
public class Ef6MigConf : DbMigrationsConfiguration<Ef6Ctx> {
public Ef6MigConf() {
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = true;
}
}
public class Friend {
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
}
public class Example
{
public int Id { get; set; }
public int? BestFriendId { get; set; }
public int? OldestFriendId { get; set; }
public virtual Friend Best { get; set; }
public virtual Friend Oldest { get; set; }
}
public class Ef6Ctx : DbContext {
public Ef6Ctx(DbConnection dbConn) : base(dbConn, true) { }
public Ef6Ctx(string connectionName) : base(connectionName) { }
public DbSet<Friend> Friends { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Example>()
.HasOptional(t=>t.Best)
.WithMany()
.HasForeignKey(x=>x.BestFriendId);
modelBuilder.Entity<Example>()
.HasOptional(t => t.Oldest)
.WithMany()
.HasForeignKey(x => x.OldestFriendId);
}
}
}

Categories