Basically I want a view which contains an Owner's details. The Owner can have multiple types of Vehicle's (or none), which can be selected with a Checkbox:
On submit, the database has to be updated, because next time the Owner is displayed in the View, the Owner must have the correct checkboxes checked.
I am unsure how to structure my EF Models, and not sure I am linking it correctly to get the result I want.
I do not want hard-code the Vehicle types as fields into the Owner object, because there are quiete a huge number of vehicles.
Can anyone point me in the right direction of how to link these models?
Will I need two or three tables in the database?
This is what I have at the moment, but it is probably wrong. If you have an idea please stop reading now to avoid confusion.
Any comments are welcome!
The Models :
public class Vehicle
{
public int VehicleID { get; set; }
public string Name { get; set; }
}
public class Owner
{
public int OwnerID { get; set; }
public string Name { get; set; }
public virtual ICollection<OwnerVehicle> OwnerVehicles { get; set; }
}
public class OwnerVehicle
{
public int OwnerVehicleID { get; set; }
public bool Ticked { get; set; }
//Not sure if this is needed, because ticked will always be true
//I delete OwnerVehicle if not needed
public int OwnerID { get; set; }
public virtual Owner Owner { get; set; }
public int VehicleID { get; set; }
public virtual Vehicle Vehicle { get; set; }
}
Controller :
public ActionResult Index()
{
//prepopulate Owner objects on the fly for this example, in my project it would fetched/created with EF into database
Owner owner = getOwner();
return View(owner); // we return the owner to view
}
public Owner getOwner()
{
//Create a owner
Owner owner = new Owner() { OwnerID = 1, Name = "JACK" };
//Create a list of vehicles
List<Vehicle> Vehicles = new List<Vehicle>();
Vehicles.Add(new Vehicle() { VehicleID = 1, Name = "I have a car"});
Vehicles.Add(new Vehicle() {VehicleID = 1, Name = "I have a bike" });
//the owner doesnt have any vehicles yet, therefor object OwnerVehicle is null at the moment
return owner;
}
[HttpPost]
public ActionResult Index(Owner owner)
{
//at this point, the owner needs have his list of Vehicles linked, and written to database
//
//ToDO
//return View();
}
The View below wont compile, because I am lost.
Index.cshtml
#model Owner
#using (Html.BeginForm())
{
<div class="editor-field">
#Html.EditorFor(model => model.OwnerVehicles)
</div>
}
EditorTemplates/OwnerVehicles.cshtml
#model OwnerVehicle
<div>
#Html.CheckBoxFor(x => x.Ticked)
#Html.LabelFor(x => x.TODO, Model.TODO)
#Html.HiddenFor(x => x.TODO)
</div>
I would create an object that would help in presenting what you are trying to output. It would be in the "model", but wouldn't be mapped to the database in any way. It would look something like this:
public class VehicleHelper //or whatever you would like to call it...
{
public Vehicle Vehicle { get; set; }
public bool Ticked { get; set; }
}
Then your Owner would have a list of these VehicleHelpers like so:
public class Owner
{
//include the properties you listed here...
[NotMapped]
public IEnumerable<VehicleHelper> VehicleHelpers { get; set; }
}
Then the Controller would populate the VehicleHelpers property as needed:
public ActionResult Edit(int id)
{
MyContext db = new MyContext();
Owner owner = db.Owners.FirstOrDefault(o => o.OwnerID == id);
if (owner == null) thrown new Exception("Invalid Owner ID");
owner.VehicleHelpers = db.Vehicles
.Select(v => new VehicleHelper() { Vehicle = v, Ticket = false });
foreach (Vehicle ownedVehicle in owner.OwnerVehicles.Select(ov => ov.Vehicle))
{
VehicleHelper helper = owner.VehicleHelpers
.FirstOrDefault(vh => vh.Vehicle == ownedVehicle);
if (helper == null) continue;
helper.Ticked = true;
}
return View(owner);
}
[Note: This following part is where I'm not certain the exact syntax, conventions and such, because I'm just learning MVC and Razer myself, but it should be close. Feel free to edit or suggest edits. ;)]
Then you would have an editor template for the VehicleHelper, which would look something like this:
#model VehicleHelper
<div>
#Html.CheckBoxFor(vh => vh.Ticked)
#model.Vehicle.Name
#Html.HiddenFor(vh => vh.Vehicle.VehicleID)
</div>
I hope that at least helps point you in the right direction. ;)
CptRobby helped me to crawl the web even deeper, and found my question to be a duplicate of this one
Ciaran Bruen created a great tutorial which can be found here, and there is a downloadable solution on the left of the page.
It shows how to create the many to many relationship, the checkboxfor and updating the database.
Related
I have an MVC 5 site, I would like to use a strongly typed DropDownListFor with a ViewModel - not with ViewBag.
I have found various articles on this - but they all seem to have huge holes - for example this one doesnt cover editing, and I do not understand how or when "SelectedFlavourId" should be used.
http://odetocode.com/blogs/scott/archive/2013/03/11/dropdownlistfor-with-asp-net-mvc.aspx
I have several requirements.
When editing the story I would like a drop down list of all places to
be displayed - with the associated place (if any) - selected.
I want to use the strongly typed DropDownListFOR (as opposed to
DropDownList).
I would like to use a ViewModel not the ViewBag.
I want to add a "No Associated Place" which will be
selected if PlaceId is null.
I want to add a css class = "form-control" to the DropDownListFor.
The below is as far as I have got after a day of frustration.
A story can be optionally associated with a PlaceId. A blank placeId is also valid. A place can also be associated with more than one story.
Models
public class Place
{
public Guid Id { get; set; }
public string PlaceName { get; set; }
}
public class Story
{
public Guid Id { get; set; }
public Guid? PlaceId { get; set; }
public string StoryName { get; set; }
}
public class StoryPlaceDropdown
{
public Story story { get; set; }
public Guid SelectedStoryId;
public IEnumerable<Place> places;
public IEnumerable<SelectListItem> placeItems
{
get
{
return new SelectList(places, "Id", "PlaceName");
}
}
}
Controller
public ActionResult Edit(Guid Id)
{
var spd = new StoryPlaceDropdown();
spd.places = PlaceRepo.SelectAll();
spd.story = StoryRepo.SelectStory(Id);
spd.selectedStoryID = apd.story.Id;
// Return view
return View(spd);
}
[HttpPost]
public ActionResult Edit(StoryPlaceDropdown spd)
{
// Never gets this far
spd.Places = PlaceRepo.SelectAll();
return View();
}
In View
#Html.DropDownListFor(m => m.SelectedStoryId, Model.PlaceItems)
This populates the DropDownList fine. However it does not select the correct item in edit view. Also when I submit the form I get this error:
Object reference not set to an instance of an object. on this line in the view #Html.DropDownListFor(m => m.SelectedStoryId, Model.PlaceItems)
How can I get this all working? Thanks.
I solved this - I had stupidly forgotten the { get; set; } accessors on the ViewModel, doh!
You can resolved this by these three steps:
Step 1:Create viewmodel
public class StoryPlaceDropdown
{
Required]
[Display(Name = "SelectedStory")]
public int SelectedStoryId { get; set; }
}
Step 2:After this on controller you can write:
public ActionResult Edit(Guid Id)
{
var spd = new StoryPlaceDropdown();
ViewBag.PlaceItems= PlaceRepo.SelectAll();
spd.story = StoryRepo.SelectStory(Id);
spd.selectedStoryID = apd.story.Id;
return View(spd);
}
Step 3: And on view you can write
<div class="col-sm-6">
#Html.DropDownListFor(m => m.SelectedStoryId, new SelectList(#ViewBag.PlaceItems, "Id", "PlaceName"), "---Select---", new { #class = "form-control select-sm" })
</div>
ASP.NET 4.5, MVC 5, EF6 code first
I'm a newbie and probably asking something long-known but I couldn't find solution on the web, probably because I don't know correct terminology to formulate this question.
To simplify things, let's say I have two model classes Teacher and Kid; One kid can be assigned only to one teacher, but one teacher can have many kids. As I'm using code first, my database is constructed from these model classes:
public class Kid
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual Teacher { get; set; }
}
public class Teacher
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
[Required]
public virtual ICollection<Kid> Kids { get; set; }
}
Now, I need to have a view for adding new kid with:
Textbox for Kid's name;
Dropdown with list of Teachers
So, I'm creating a data transfer object, specifically for that view:
public class AddNewKidViewDTO
{
public IEnumerable<SelectListItem> Teachers { get; set; }
public int SelectedTeacherId { get; set; }
public Kid Kid { get; set; }
}
I also have a method for populating IEnumerable Teachers:
public AddNewKidViewDTO LoadTeachersForDropDownList()
{
... //get the list of Teachers
AddNewKidViewDTO addNewKidViewDTO = new AddNewKidViewDTO();
List<SelectListItem> selectListItems = new List<SelectListItem>();
foreach (teacher in Teachers)
{
selectListItems.Add (new SelectListItem
{
Text = teacher.Name.ToString(),
Value = teacher.Id.ToString()
});
}
addNewKidViewDTO.Teachers = selectListItems;
return addNewKidViewDTO;
}
and in the view AddNewKid.cshtml
<form>
#Html.LabelFor(model => model.Kid.Name)
#Html.TextBoxFor(model => model.Kid.Name, new {id ="Name"}
<br/>
#Html.LabelFor(model => model.Kid.Teacher)
#Html.DropDownListFor(model => model.SelectedTeacherId, Model.Teachers)
</form>
Form gets submitted and in the controller I get my populated AddNewKidViewDTO model:
[HttpPost]
public ActionResult SaveNewKid (AddNewKidViewDTO addNewKidViewDTO)
{
if (ModelState.IsValid)
{
//here is where the problem comes
}
}
ModelState.IsValid in my case will always return false.
Because when it starts validating AddNewKidViewDTO.Kid, Teacher is compulsory field but in my addNewKidViewDTO model it's null. I have the necessary teacher Id contained in addNewKidViewDTO.SelectedTeacherId only.
My question is, what is an elegant way to validate my model before passing to my inner business logic methods?
Any help is appreciated.
There are multiple possible solutions:
Changing your AddNewKidViewDTO and decorating it with the DataAnnotaions for validation:
public class AddNewKidViewDTO
{
public IEnumerable<SelectListItem> Teachers { get; set; }
[Range(1, 2147483647)] //Int32 max value but you may change it
public int SelectedTeacherId { get; set; }
[Required]
public string KidName { get; set; }
}
Then you can create Kid object manually in case that your model valid.
UPDATE (to address your comment)
If you use this approach your action will look like this:
[HttpPost]
public ActionResult SaveNewKid (AddNewKidViewDTO addNewKidViewDTO)
{
if (ModelState.IsValid)
{
using (var dbContext = new yourContext())
{
var teacher = dbContext.Teachers.FirstOrDefault(t=>t.id == addNewKidViewDTO.SelectedTeacherId );
if(teacher == default(Teacher))
{
//return an error message or add validation error to model state
}
//It is also common pattern to create a factory for models
//if you have some logic involved, but in this case I simply
//want to demonstrate the approach
var kid = new Kid
{
Name = addNewKidViewDTO.KidName,
Teacher = teacher
};
dbContext.SaveChanges();
}
}
}
Write a custom model binder for AddNewKidViewDTO that will initialize Teacher property in Kid object so once you actually use Model.IsValid the property will be initialized.
I am having difficulty with my understanding of MVC coming from an aspx world.
I have a Model called CustomerGarment. This has a Order and a Customer along with a few garments.
public class CustomerGarment
{
public int CustomerGarmentId { get; set; }
public virtual Customer Customer { get; set; }
public virtual Order Order { get; set; }
public virtual GarmentJacket GarmentJacket { get; set; }
public virtual GarmentShirt GarmentShirt { get; set; }
}
I have a method for get and post. When the page loads, it creates a new CustomerGarment instance and querys the database to fill the Customer and Order variables. I then use the viewbag to show on the screen a list of GarmentJackets and GarmentShirts
The page then views and using the view I can access the model perfectly. Drop downs load with the viewbag contents and I can access all Customer and Order variables using the model I have passed.
The problem I then face is when I use the HttpPost. The model is not passed back with the information I passed to it.
public ActionResult AddGarments(int orderId, int customerId)
{
CustomerGarment cg = new CustomerGarment();
cg.Order = (from a in db.Orders where a.OrderId == orderId select a).FirstOrDefault();
cg.Customer = (from a in db.Customers where a.CustomerId == customerId select a).FirstOrDefault();
var jackets = from a in db.GarmentJackets orderby a.Type, a.SleeveLengthInches, a.ChestSizeInches select a;
var shirts= from a in db.GarmentKilts orderby a.PrimarySize, a.DropLength select a;
ViewBag.GarmentJacket = new SelectList(jackets, "GarmentJacketId", "GarmentJacketId");
ViewBag.GarmentShirt = new SelectList(shirts, "GarmentShirtId", "GarmentShirtId");
return View(cg);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddGarments(CustomerGarment cg)
{
// Here, I do not have the customer info for example
db.CustomerGarments.Add(cg);
db.SaveChanges();
return RedirectToAction("Index");
return View(cg);
}
This is a bit of my view
#Html.HiddenFor(model => model.Order.OrderId)
#Html.HiddenFor(model => model.Order.CustomerId)
<div class="display-field">
#Html.DisplayFor(model => model.Customer.Name)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.GarmentJacket, "Jacket")
</div>
<div class="editor-field">
#Html.DropDownListFor(m => m.GarmentJacket, (SelectList)ViewBag.GarmentJacket, new {style="width:312px;height:30px;margin-top:2px;margin-bottom:5px"})
</div>
EDIT
My Garment Jacket Model
public class GarmentJacket : Garment
{
public int GarmentJacketId { get; set; }
[Required]
public string Type { get; set; }
[Required]
[Display(Name = "Chest Size")]
public int ChestSizeInches { get; set; }
[Required]
[Display(Name = "Sleeve Length")]
public int SleeveLengthInches { get; set; }
}
public class Garment
{
[DataType(DataType.Date)]
public DateTime? DateRetired { get; set; }
[Required]
public string Barcode { get; set; }
[Required]
public bool Adults { get; set; }
}
In your CustomerGarment class, you should have:
public class CustomerGarment
{
public int CustomerGarmentId { get; set; }
public int CustomerId { get; set; }
public int OrderId { get; set; }
public int GarmentJacketId { get; set; }
public int GarmentShirtId { get; set; }
public virtual Customer Customer { get; set; }
public virtual Order Order { get; set; }
public virtual GarmentJacket GarmentJacket { get; set; }
public virtual GarmentShirt GarmentShirt { get; set; }
}
And, then, in your View, your DropDownList will look like:
#Html.DropDownListFor(m => m.GarmentJacketId, (SelectList)ViewBag.GarmentJacket, new {style="width:312px;height:30px;margin-top:2px;margin-bottom:5px"})
Your DropDownList only posts one value, which is the GarmentJacketId. You can't bind that Id to the whole GarmentJacket class.
By the way, you also need to replace your hidden inputs with these:
#Html.HiddenFor(model => model.OrderId)
#Html.HiddenFor(model => model.CustomerId)
I think I know your problem. As you suggested in you comment above you need to post everything you want retained in the view. This is one of the differences beteween webforms and MVC, webforms has viewstate that could contain information that you don't explicitly add to the view and post back, giving the impression of state. In MVC you have to add it to the view.
On the other hand you don't need to pass in more information than you need either. You pass inn the customerId as a hidden field. On post method you get the customer from the db using the Id, then you add the order to the customer.
I have some questions about your design, but given that a customer holds a collection of Orders, you could do something like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddGarments(CustomerGarment cg)
{
// Get the customer from the database
var customer = db.Customers.Find(c=>c.id==cb.Customer.Id)
var order = new Order();
//Create your order here using information from CustomerGarment model
//If the model already holds a valid Order object then just add it.
//i.e. you could get a Garment object from the DB using the GarmentId from
//the ViewModel if you really need more than just the Id to create the order
customer.Orders.Add(order);
db.SaveChanges();
return RedirectToAction("Index");
}
I'm new to MVC, so I apologize in advance if something doesn't make sense.
I have a base class (let's say "Person"), and 2 derived classes ("Student", "Professor").
I want to use 1 view for the Create functionality, with Partial views that contain the creation forms for either a student or professor. If I add a parameter, I can check against that to determine which partial view to show.
But my question is this: When the "Create" button is clicked, how can I determine which object is being created?
Edit (please bear w/ me, as I just created these to illustrate the problem)
Person class:
public class Person
{
public string Gender { get; set; }
public int ID { get; set; }
}
Student class:
public class Student : Person
{
public string LastName { get; set; }
public string FirstName { get; set; }
public List<Course> Courses { get; set; }
}
Professor class:
public class Professor : Person
{
public string LastName { get; set; }
public string FirstName { get; set; }
public double AnnualSalary { get; set; }
}
So then my Create controller looks like this:
public ActionResult Create(int personType) //1=student, 2=professor
{
var x = new {
Student = new Student(),
Professor = new Professor()
};
ViewBag.PersonType = personType;
return View(x);
}
Then my view looks like this:
<div>
#if (ViewBag.PersonType == 1)
{
#Html.Partial("CreateStudentPartialView", Model.Student)
}
else
{
#Html.Partial("CreateProfessorPartialView", Model.Professor)
}
So, the question is what would the associated create action look like, when the "Create" button is clicked in either partial view?
[HttpPost()]
public ActionResult Create(....) //What would I put as parameter(s)?
{
//no idea what to do here, since I don't know what object is being passed in
return RedirectToAction("Index");
}
Your best bet here is to have multiple POST actions in your controller.
So in the forms in your partial views, specify the action to hit
#using (Html.BeginForm("CreateStudent", "Create")) {
and
#using (Html.BeginForm("CreateProfessor", "Create")) {
Then your controller will look something like this:
[HttpPost]
public ActionResult CreateStudent(Student student)
{
//access the properties with the dot operator on the student object
//process the data
return RedirectToAction("Index");
}
and
[HttpPost]
public ActionResult CreateProfessor(Professor professor)
{
//access the properties with the dot operator on the professor object
//process the data
return RedirectToAction("Index");
}
I have declared some Non-Content data in an Orchard CMS by defining the records and schema like this:
public class CountyRecord
{
public virtual int Id { get; set; }
public virtual string CountyName { get; set; }
public virtual CountryRecord CountryRecord { get; set; }
}
public class CountryRecord
{
public CountryRecord()
{
CountyRecords = new List<CountyRecord>();
}
public virtual int Id { get; set; }
public virtual string CountryName { get; set; }
public virtual IList<CountyRecord> CountyRecords { get; set; }
}
public class Migrations: DataMigrationImpl
{
public int Create()
{
//COUNTIES
SchemaBuilder.CreateTable(typeof(CountyRecord).Name, table => table
.Column<int>("Id", col => col
.PrimaryKey()
.Identity())
.Column<string>("CountyName")
.Column<int>("CountryRecord_Id"));
//COUNTRIES
SchemaBuilder.CreateTable(typeof(CountryRecord).Name, table => table
.Column<int>("Id", col => col
.PrimaryKey()
.Identity())
.Column<string>("CountryName"));
}
}
I then have two controllers handling the admin pages for these two entities. In the country controller I have the following actions:
//DELETE
[HttpGet, Admin]
public ActionResult Delete(int countryId)
{
var country = CountryRepo.Get(countryId);
if (country == null)
{
return new HttpNotFoundResult("Couldn't find the country with ID " + countryId.ToString());
}
return View(country);
}
[HttpPost, Admin, ActionName("Delete")]
public ActionResult DeletePOST(CountryRecord country)
{
foreach (CountyRecord county in CountyRepo.Fetch(c=>c.CountryRecord.Id==country.Id))
{
CountyRepo.Delete(county);
}
CountryRepo.Delete(country);
OrchardServices.Notifier.Add(NotifyType.Information, T("Country '{0}' deleted successfully", country.CountryName));
return RedirectToAction("Index");
}
And this is the view that goes with that:
#model Addresses.Models.CountryRecord
<div class="manage">
#using (Html.BeginFormAntiForgeryPost("Delete"))
{
<h2>Are you sure you want to delete this country and ALL its counties?</h2>
#Html.HiddenFor(m => m.Id);
#Html.HiddenFor(m => m.CountryName);
#Html.ActionLink(T("Cancel").Text, "Index", "CountriesAdmin", new { AreaRegistration = "Addresses" }, new { style = "float:right; padding:4px 15px;" })
<button class="button primaryAction" style="float:right;">#T("Confirm")</button>
}
</div>
However, here's the issue, when I delete a country that still has counties assigned to it, it throws the following error:
a different object with the same identifier value was already associated with the session
Can anyone please help?
Thanks.
It's because your DeletePOST() parameter is a CountryRecord. Orchard records are all proxied by the NHibernate framework and MVC's ModelBinder can't properly create them for you.
What you need to do instead is like what you do in the non-POST method: accept just the integer ID of the CountryRecord, fetch the record fresh from the repository, then delete it.