EntityFramework MVC Create multiple different Entities from one view - c#

I'm working on an EntityFramework project and running into an interesting problem. What I want to do is create a database entity using a view, but in order to do that I need to create another database entity of a different type that the first entity needs to be associated with, and I'm trying to do this from the same view.
For example, we have a Person, and each Person will have a recurring appointment. However, appointments can be recurring on different types of criteria. Right now I'm trying to get this working on appointments that will be on a daily basis (meaning, for example, every Monday, Wednesday, and Friday) So here, my model is something like:
DailyAppointment implements the abstract class AppointmentFrequency
Person has an AppointmentFrequency associated with it. Here is the code behind my model (database generated using code-first migrations).
AppointmentFrequency :
public abstract class AppointmentFrequency
{
[KeyAttribute]
public int Identity { get; set; }
}
DailyAppointment :
public class DailyAppointment : AppointmentFrequency
{
public bool Monday { get; set; }
// ... Variable for each day of the week.
}
Person:
public class Person
{
[Key]
public int Identity { get; set; }
//... Other information
[ForeignKey("AppointmentFrequency_Identity")]
public virtual AppointmentFrequency AppointmentFrequency { get; set; }
public int? AppointmentFrequency_Identity { get; set; }
}
So in our view, when we create a Person, we want to have an AppointmentFrequency associated with them.
Currently, my approach involves a partial view inside the view that creates a Person:
#using (Html.BeginForm("AddPerson", "ControllerName", FormMethod.Post, new { role = "form", #class = "form-inline" }))
{
... //This is where we get information about the Person
Model.Person.AppointmentFrequency = new DailyAppointment();
var dailyAppointment = Model.Person.AppointmentFrequency as DailyAppointment;
if (dailyFrequency != null)
{
#Html.Partial("_DailyAppointmentEditor", Model.Person.AppointmentFrequency as DailyAppointment);
}
<div class="form-group">
<button class="btn btn-primary" type="submit">Add</button>
</div>
}
(I have also tried doing this a few similar ways, such as sending the dailyAppointment variable into the partial view instead)
My partial view looks like this:
#model Database.Entities.DailyAppointment
#Html.LabelFor(model => model.Monday)
#Html.CheckBoxFor(model => model.Monday, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Monday, null, new { #class = "text-danger" })
#Html.LabelFor(model => model.Tuesday)
#Html.CheckBoxFor(model => model.Tuesday, new { #class = "form-control" })
#Html.ValidationMessageFor(model => model.Tuesday, null, new { #class = "text-danger" })
... //The rest of the days and script bundling
Inside my controller, I am only creating a Person, and was hoping that the Appointment would be created by the Framework, but that doesn't seem to be the case.
[HttpPost]
public ActionResult AddPerson(Person person){
this.db.People.AddOrUpdate(person);
this.db.SaveChanges();
return this.View();
}
I know one way of doing this would be to collect the data and then use it in the form post sent to the controller, creating the frequency, and then adding the reference to the person object and creating it in the database. I have actually done that and know that it works, but I feel like there must be a more friendly way of doing this within the framework.
Part of the challenge here is that I'm hoping to make this extensible while still using the same design. Let me know if I can give any more information, but if you have any suggestions for this approach or for a different approach I can take, I would greatly appreciate it! Thank you.

Related

Html.DropDownListFor with null selectList parameter

This is taken from VS Add New Scaffolded Item... when creating a new controller.
In the controller:
// GET: Drivers/Create
public ActionResult Create()
{
ViewBag.Tenant = new SelectList(db.Tenants, "TenantID", "TenantName");
return View();
}
The view then renders a drop-down list:
#Html.DropDownListFor(model => model.Tenant, null, htmlAttributes: new { #class = "form-control" })
The relevant model information:
public partial class Driver
{
public int DriverID { get; set; }
public int Tenant { get; set; }
public virtual Tenant Tenant1 { get; set; }
}
public partial class Tenant
{
public Tenant()
{
this.Drivers = new HashSet<Driver>();
}
public int TenantID { get; set; }
public string TenantName { get; set; }
public virtual ICollection<Driver> Drivers { get; set; }
}
Can someone explain why this works?
I looked at other questions and documentation and couldn't find the answer. I suspect it is something along the lines of "convention over configuration" and it is pulling from the ViewBag using the name of the property. In fact, I changed the ViewBag property to Tenantz and got the following exception:
There is no ViewData item of type 'IEnumerable' that
has the key 'Tenant'.
So is setting the property name of the ViewBag the same as the model property you want to update a good practice? It seems ok but I always hear how you should avoid ViewBag and dynamic types.
As you have already discovered there's a convention. The following line in your view:
#Html.DropDownListFor(model => model.Tenant, null, htmlAttributes: new { #class = "form-control" })
is exactly the same as this line:
#Html.DropDownList("Tenant", null, htmlAttributes: new { #class = "form-control" })
Now if you look at how the DropDownList helper is implemented in the source code you will notice that it simply does that:
object obj = htmlHelper.ViewData.Eval(name) as IEnumerable<SelectListItem>;
where name is the first argument passed to the DropDownList helper. And guess what? It discovers the corresponding value that you have set in your controller: ViewBag.Tenant = ....
This being said using ViewBag is an absolutely, disastrously, terribly bad practice. You've already find out why. It can bite you like a dog without you even knowing what's going on. The best way to protect against those dogs (ViewBag) is to search them inside your solution and give them poison. Simply get rid of absolutely any ViewBag calls in your code and use view models. Then you will not get bad surprises and everything will have a reasonable explanation and questions like this wouldn't be necessary on StackOverflow.
Like for example you could write a normal view model:
public class DriverViewModel
{
public int? SelectedTenantID { get; set; }
public IEnumerable<SelectListItem> Tenants { get; set; }
}
and a normal controller action that will query your datastore for the required information and project your entity model to the view model:
// GET: Drivers/Create
public ActionResult Create()
{
var viewModel = new DriverViewModel();
viewModel.Tenants = new SelectList(db.Tenants, "TenantID", "TenantName");
return View(viewModel);
}
and finally the corresponding strongly typed view:
#model DriverViewModel
...
#Html.DropDownListFor(
model => model.SelectedTenantID,
Model.Tenants,
htmlAttributes: new { #class = "form-control" }
)
In this case you are using a strongly typed view, with a strongly typed view model and a helper. There are no longer any doubts (and dogs that bite). The code is readable and you cannot ask, why this convention over configuration is doing this or that. So as long as there's no trace of ViewBag in your application there will be no such questions.

Http Post with duplicate name values MVC

In MVC I have a form that uses a lot of duplicate information. Basically they click a button and there is another "form"(text box drop down list etc) that pops up. When they click the submit button however all of that comes back with the same name. How would I go about either making the names different in the Post or be able to throw the items into a list?
My code:
#Html.TextBox("Textbox1", "", new { placeholder = "", size = "77", maxlength = "76" })
#Html.DropDownList("Country", ViewData["CountryOptions"] as SelectList,"Select", new { id = "Country"})</div>
<div class="pad5">
<span class="FieldLabel">Application *</span><span>#Html.DropDownListFor(model => model.ProductsID, Model.HowProductsAreUsedOptions, new { #class = "General", id = "General-1" })
#Html.DropDownListFor(model => model.ApplicationValue, Model.Options, "--Choose One--", new { id = "Option1", style = "display:none" })
</div>
These can be repeated up to 9 times by the time they submit. However this will give me this in the Post
FormID=8&Textbox1=This&Country=USA&ProductsID=1&ApplicationValue=2&
Textbox13=Is&Country=Canada&ProductsID=2&ApplicationValue=3&
Textbox14=A&Country=Canada&ProductsID=2&ApplicationValue=1&
Textbox15=Test&Country=Canada&ProductsID=1&ApplicationValue=8&
Textbox16=For&Country=Canada&ProductsID=2&ApplicationValue=1&
Textbox17=Stack&Country=USA&ProductsID=1&ApplicationValue=9&
Textbox18=Overflow&Country=USA&ProductsID=2&ApplicationValue=2
How can I make something so that way it will be able to seperate these into 7 different value sets instead of only giving me one of each?
If I were you I would create a strongly typed view model to represent your data.
One to represent each item with the properties within them.
public class FooModel
{
public string Text { get; set; }
public string Country { get;set;}
public int ProductsID { get; set; }
public int ApplicationValue { get; set; }
}
Then create a model to hold them and represent them
public class FooViewModel
{
public List<FooModel> Foos { get; set; }
}
You can then return an instance of FooViewModel from your controller.
Within your view you use the name indexing of the collection as follows:
#for (var i = 0; i < Model.Foos.Count; i++)
{
...
#Html.TextBoxFor(model => Model[i].Text)
#Html.DropDownListFor(model => Model[i]Country, ViewData["CountryOptions"] as SelectList,"Select")
#Html.HiddenFor(model => Model[i].ProductsID)
...
}
The HiddenFor's will post those values back too.
Now in your action you just need to change your parameter to take an instance of FooViewModel and they will all be populated server side.
For more info on it see here:
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/

MVC Model child object null on HTTP post

Hope someone can help me. I am new to MVC, coming from a winforms/console/vb6background.
Apologies if this has already been answered, I am stuggling to understand how I can resolve the below issue.
I have a view model :
public class testvm
{
public int id { get; set; }
public DateTime date { get; set; }
public student studentID { get; set; }
public testvm() { }
public testvm (student s)
{
studentID = s;
}
}
I am pre-populating the student child object of this ViewModel before it is passed to the view.
Student Model :
public class student
{
[Key]
public int ID { get; set; }
public string Name { get; set; }
}
The problem I have is when the model is returned to the create HTTP post method the student child object is blank.
The controller code :
// GET: testvms/Create
public ActionResult Create(int sId)
{
student a = db.students.Find(sId);
testvm b = new testvm(a);
return View(b);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "id,date,student")] testvm testvm)
{
if (ModelState.IsValid)
{
db.testvws.Add(testvm);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(testvm);
}
View code:
#model WebApplication2.Models.testvm
#{
ViewBag.Title = "Create";
}
<h2>Create</h2>
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>testvm</h4>
<hr />
#Html.HiddenFor(model => model.studentID.ID)
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.date, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.date, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.date, "", new { #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>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
The model object on the view is populated with the student information. When this is passed back to Create POST controller the student child object is null!
Can somebody please advise where I am going wrong or of the correct way to achieve this?
My application will have many forms that will all need to be pre-populated with student information. Each student will have many forms that will need to be filled out for them.
Many thanks in advance,
Rob
For every property in domain model (in your case testvm) you must have an EditorFor or Input element (like TextBoxFor or so) on your view(or HiddenFor for ID or other non user ui data).It may be a pain binding nested models in MVC as the DefaultModelBinder may not be able to bind whole object.However it would be safer approach to expose only the required properties on view like
#Html.HiddenFor(model => model.studentID.ID)
#Html.HiddenFor(model => model.studentID.Name)
and later on Controller Side
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
var originalobj=db.get //get fresh copy from data store
originalobj.Name=testvm.Name;
// ...other properties
//perform required operations on originalobj
}
you may use AutoMapper for this Purpose as
Mapper.CreateMap<testvm,testvm>();
originalobj=Mapper.Map<testvm,testvm>(testvm,originalobj);
you may find more information about Automapper on :
https://github.com/AutoMapper/AutoMapper/wiki/Getting-started
Your property name is called studentId (even though standard C# property naming convention dictates that it should have been called StudentId):
public student studentID { get; set; }
But in your Bind attribute you seem to have specified some student property which doesn't really exist on your view model:
[Bind(Include = "id,date,student")]
So you probably want to get rid of this Bind attribute from your controller action:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(testvm testvm)
{
...
}
Also note that you only have a hidden field for the student id inside your form:
#Html.HiddenFor(model => model.studentID.ID)
You don't have a corresponding hidden field for the student name property, so it will never be posted back to your controller action.
Your attribute [Bind(Include = "id,date,student")] should include the names of the properties that you want to be set, student isn't in your model, but studentID is, they have to match.
You don't have to explicitly specify all of the field names that you want to be bound to your model, by default they will be bound anyway unless you tell the binder NOT to bind it by using [Bind(Exclude = "id,date,student")]. Therefore as it currently stands, I'd recommend removing your Include attribute to ease maintenance unless there is an important reason for using it and simply ensure that the models that you bind to only include the values you need.
Secondly, you have to make sure that any values that you are posting back from a form in your view have the same parameter names and are structured the same as the ones that you want to be bound to the request model.
This:
#Html.HiddenFor(model => model.studentID.ID)
Is not the same as:
#Html.HiddenFor(model => model.studentID)

How to create master detail detail form

I am trying to create a form which allows the creation and editing of a hierarchy of entities using MVC.
The top level is project which can have 1 or many environments associated with it. The environments are predefined, eg Dev, Test and Prod.
For each environment that is added it is possible to add several interfaces.
So a user would enter the project information. Select which environments which are relevant and then for each environment section add several interfaces.
I've created 3 view models, project, environment and interface. Like so
public class ProjectViewModel
{
public int Id { get; set; }
public string ProjectTitle { get; set; }
public List<SelectListItem> EnvironmentChoices { get; set; }
public List<EnvironmentViewModel> EnvironmentModel { get; set; }
}
public class EnvironmentViewModel
{
public IList<InterfaceViewModel> Interfaces { get; set; }
public string Environment { get; set; }
}
public class InterfaceViewModel
{
public string InterfaceName { get; set; }
}
Then created 1 project template and 2 editor templates for the environment model and the interface model.Like so
<p>
#Html.LabelFor(x => x.ProjectTitle)
#Html.TextBoxFor(x => x.ProjectTitle)
#Html.ValidationMessageFor(x => x.ProjectTitle)
</p>
<p>
#Html.LabelFor(x => x.EnvironmentModel)
#for (int i = 0; i < Model.EnvironmentChoices.Count; i++)
{
#Html.CheckBoxFor(m => m.EnvironmentChoices[i].Selected, new { id = Model.EnvironmentChoices[i].Value })
#Html.HiddenFor(m => m.EnvironmentChoices[i].Value)
#Html.DisplayFor(m => m.EnvironmentChoices[i].Text)
}
</p>
<p>
#Html.EditorFor(x => x.EnvironmentModel)
</p>
for the environment template
#model EnvironmentViewModel
<fieldset style="margin-left: -10px;">
<legend>#Model.Environment</legend>
#Html.ActionLink("Add Interface", "AddInterface", Model, new { #class = "button icon-file-plus" })
#Html.EditorFor(x => x.Interfaces)
</fieldset>
for the interface template
#model InterfaceViewModel
<p>
#Html.LabelFor(x => x.InterfaceName)
#Html.TextBoxFor(x => x.InterfaceName)
#Html.ValidationMessageFor(x => x.InterfaceName)
</p>
What I am finding is that when I click the add button on the environment section. The controller only picks up the environment model and loses the project model context so cannot modify it to add the new interface model.
Am I going about this in the wrong way? If so, are there examples of best practice. If not, what am I doing wrong.
Thanks
Thanks to Stephen Muecke for pointing me in the right direction.
I found this example to be very close to what I was trying to achieve
http://jarrettmeyer.com/post/2995732471/nested-collection-models-in-asp-net-mvc-3
Basically it uses to MVC to dynamic render JavaScript which in turn will add a detail view model to the form which conforms to the MVC indexing convention. Therefore when the master model and sub models are posted back the whole thing is bound to one big hierarchical model.

How should I validate different sections of MVC3 page

I am trying to understand how I should validate on the client sections of my MVC3 page independently and have come up with a simplyfied version of what I am trying to achieve.
If I use one form:
Pros: When I submit back to the "PostData" controller method I receive all data contained within the form. In this case both values "name" and "description", which means that I can instantiate "PersonHobbyModel" and assign the data I have received. I can either store in the database or I can return the same view.
Cons: I cant validate independently. So if "name" isn't completed and I complete "description" I can still submit the page. (This is a simplyfied version of what I am trying to do and I would have more fields than just "name" and "description")
With two forms:
Pros: I can validate independently.
Cons: The controller method only receives the subitted forms data which, in this case either "Persons name" or "Hobby description" which means that I can't recreate a full instance of "PersonHobbyModel".
This is the model:
public class Person {
[Display(Name = "Person name:")]
[Required(ErrorMessage = "Person name required.")]
public string Name { get; set; }
}
public class Hobby {
[Display(Name = "Hobby description:")]
[Required(ErrorMessage = "Hobby description required.")]
public string Description { get; set; }
}
public class PersonHobbyModel {
public PersonHobbyModel() {
this.Person = new Person();
this.Hobby = new Hobby();
}
public Person Person { get; set; }
public Hobby Hobby { get; set; }
}
This is the controller:
public class PersonHobbyController : Controller
{
//
// GET: /PersonHobby/
public ActionResult Index()
{
var model = new PersonHobbyModel();
return View(model);
}
public ActionResult PostData(FormCollection data) {
var model = new PersonHobbyModel();
TryUpdateModel(model.Person, "Person");
TryUpdateModel(model.Hobby,"Hobby");
return View("Index", model);
}
}
This is the view:
#model MultipleFORMStest.PersonHobbyModel
#{
ViewBag.Title = "Index";
}
<h2>
Index</h2>
#using (Html.BeginForm("PostData", "PersonHobby")) {
<div>
#Html.LabelFor(model => model.Person.Name)
#Html.TextBoxFor(model => model.Person.Name)
#Html.ValidationMessageFor(model => model.Person.Name)
<input type="submit" value="Submit person" />
</div>
}
#using (Html.BeginForm("PostData", "PersonHobby")) {
<div>
#Html.LabelFor(model => model.Hobby.Description)
#Html.TextBoxFor(model => model.Hobby.Description)
#Html.ValidationMessageFor(model => model.Hobby.Description)
<input type="submit" value="Submit hobby" />
</div>
}
UPDATE 1
I didnt mention, as I wanted to keep the question as simple as possible, but for one of the sections I am using "jquery ui dialog". I initially used a DIV to define the dialog, which I had inside my main form. This would of caused one problem as I wouldn't have been able to validate on the client the "JQuery dialog form" independently from the rest of the form.
Saying this jquery did removed the "div jquery ui dialog" from the main form which made me include the dialog in it's own form. For this reason I have ended up with two forms. The advantage is that I can now independently validate the "jquery dialog ui form".
But I am confused as to how should I handle on the server data submited from various forms on the client as there is a chance that the user has JS disabled. If I submit from one form I can't access the data in other forms.
UPDATE 2
Thanks for the replies. I believe I do need two forms and two entities as I want to validate them independently on the client, (apart from being kind of forced to by "Jquery UI Dialog"). For instance if I have, instead of one hobby I have a list of hobbies, which I could posible display in a grid in the same view. So I could not fill in the person name, but continue to add hobbies to the grid, If I do not complete the hobby description I'd get a validation error. (Sorry as I should of included both of my updates in the initial question but for the purpose of clarity I wanted to keep it as simple as posible)
From my perspective, you have a single view model that corresponds to two entity models. In your place I would use a single form and validate the view model and not really think about it as two (dependent) entities. Receive back the view model in your action, instead of a generic form collection, and use model-based validation via data annotation attributes. Once you have a valid, posted model you can then translate that into the appropriate entities and save it to the database.
Model
public class PersonHobbyViewModel {
[Display(Name = "Person name:")]
[Required(ErrorMessage = "Person name required.")]
public string Name { get; set; }
[Display(Name = "Hobby description:")]
[Required(ErrorMessage = "Hobby description required.")]
public string Description { get; set; }
}
Controller
public class PersonHobbyController : Controller
{
//
// GET: /PersonHobby/
[HttpGet] // mark as accepting only GET
public ActionResult Create() // Index should probably provide some summary of people and hobbies
{
var model = new PersonHobbyViewModel();
return View(model);
}
[HttpPost] // mark as accepting only POST
public ActionResult Create(PersonHobbyViewModel model) {
if (ModelState.IsValid) {
var person = new Person { Name = model.Name };
var hobby = new Hobby { Description = model.Description };
person.Hobbies = new List<Hobby> { hobby };
db.Persons.Add( person );
db.SaveChanges();
}
return RedirectToAction( "details", new { id = person.Id } ); // view the newly created entity
}
}
View
#model MultipleFORMStest.PersonHobbyViewModel
#{
ViewBag.Title = "Create";
}
<h2>
Create</h2>
#using (Html.BeginForm("Create", "PersonHobby")) {
<div>
#Html.LabelFor(model => model.Person.Name)
#Html.TextBoxFor(model => model.Person.Name)
#Html.ValidationMessageFor(model => model.Person.Name)
<input type="submit" value="Submit person" />
</div>
<div>
#Html.LabelFor(model => model.Hobby.Description)
#Html.TextBoxFor(model => model.Hobby.Description)
#Html.ValidationMessageFor(model => model.Hobby.Description)
<input type="submit" value="Submit hobby" />
</div>
}
I think your ViewModel should be only only specific to that view you are representing. In this case, i would use a ViewModel like this
public class AddPersonHobbyViewModel
{
[Required]
[Display (Name="Person Name")]
public string PersonName { set;get;}
[Required]
[Display (Name="Hobby Description")]
public string HobbyDescription { set;get;}
}
And in my PostData ActionMethod, I will check for Model Validation
[HttpPost]
public ActionResult PostData(AddPersonHobbyViewModel objVM)
{
if(ModelState.IsValid)
{
// Everything is fine. Lets save and redirect to another get View( for PRG pattern)
}
return View(objVm);
}
And you use only one Form in your View which is strongly typed to AddPersonHobbyViewModel
#model AddPersonHobbyViewModel
#using (Html.BeginForm("PostData","Person"))
{
#Html.TextBoxFor(m=>m.PersonName)
#Html.ValidationMessageFor(m => m.PersonName)
#Html.TextBoxFor(m=>m.HobbyDescription )
#Html.ValidationMessageFor(m => m.HobbyDescription )
<input type="submit" value="Save" />
}

Categories

Resources