Create multiple parts of complex model in the main view - asp.net

I'm a little new to ASP.Net MVC, I have a complex model.
public class BuildingPermit
{
public int ApplicationID { get; set; }
public virtual Person Applicant { get; set; }
public virtual Area ApplicantArea { get; set; }
public virtual ICollection<Owner> Owners { get; set; }
/...
}
Using scaffolding, I created the controller and all the views. However, I want to register all the details in the same page, meaning in the BuildingPermit's Create view, creating the details for Applicant of type Person, the ApplicationArea of type Area and so on. Is there any way I can accomplish this?
If it's not possible, I think it's possible to add a link to create the object. When the user clicks on it, the page goes to that view, creates it, get its information back and shows it in the BuildingPermit's view.
I'd appreciate your help.

You could achieve this by creating an editor template for Person, Area, Owner etc in:
~/Views/Shared/EditorTemplates/Person.cshtml
~/Views/Shared/EditorTemplates/Area.cshtml
~/Views/Shared/EditorTemplates/Owner.cshtml
The editor template will want to be strongly typed and should give the editor layout for the type:
#model Models.Person
<h2>Person</h2>
<p>
#Html.LabelFor(model => model.Name)
#Html.EditorFor(model => model.Name)
</p>
<p>
#Html.LabelFor(model => model.Address)
#Html.EditorFor(model => model.Address)
</p>
// And so on
Once you've done this calling #Html.EditorFor(model => model.Applicant) will pick up your template and display within your Edit view.
If you are wanting to display all of this information together then you will probably want to also create display templates for these types. These work just like the editor templates except you keep your templates in a DisplayTemplates folder.
~/Views/Shared/DisplayTemplates/Person.cshtml
~/Views/Shared/DisplayTemplates/Area.cshtml
~/Views/Shared/DisplayTemplates/Owner.cshtml

That's no problem, just make sure you initialise your complex object somehow to avoid null reference exceptions:
public BuildingPermit()
{
this.Applicant = new Person();
this.ApplicantArea = new Area();
...
}
Then in your controller action method create an instance of the model and pass it to your view:
public ActionResult Create()
{
BuildingPermit model = new BuildingPermit();
View(model);
}
For the view:
#model MyNamespace.BuildingPermit
#Html.LabelFor(m => m.Applicant.FirstName)<br />
#Html.TextBoxFor(m => m.Applicant.FirstName)<br />
...
<input type="submit" value="Create new building permit" />
Then look into examples online on how to handle a HttpPost in your MVC controller.
If you want to create specific UI partials for each object type, then you can looking into EditorFor and DisplayFor templates. From what you mention in your original post, this might be what you're looking for also.
Hope this helps.

Related

Instantiating an object inside of view

I am trying to instantiate an object from inside of my view:
In my model I have an entity object called Listing that has a Property object inside of it:
public class Listing
{
...
public Property ListingProperty { get; set; }
...
}
Inside of my view I am referencing the listing model:
#model Realintory.Models.Listing
...
Inside of a form in this view I am trying to set up the properties of the "Property" object like so:
<div class="form-group">
#Html.LabelFor(model => model.ListingProperty.Address, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.ListingProperty.Address)
#Html.ValidationMessageFor(model => model.ListingProperty.Address)
</div>
</div>
This throws a null value exception because it hasn't been instantiated. My question is how do I set this up, I know this must be easy but no matter what I try it's not working on the view side.
Things like this fail badly:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
Property toCreate = new Property();
#Model.ListingProperty = toCreate;
...
}
I realize this is probably a newb question but I cant seem to find the answer anywhere so I didn't want to waste anymore time and figured I would ask.
Your controller is supposed to provide a model that represents the data required by the view. If the view needs that property populated in the model then it's the responsibility of the controller to do it. The view should be using the model, not building it.
To add an arbitrary block of code in a Razor view you can do this:
#{
// ...
}
In this case, maybe something like this is appropriate:
#if (Model.ListingProperty == null)
{
Model.ListingProperty = new Property();
}

MVC Model not updating

The Model:
class Address
{
public string City { get; set; }
public string Zip { get; set; }
}
The Controller:
[HttpPost]
public ActionResult GetAddress(Address model)
{
if (!String.IsNullOrEmpty(model.Zip))
{
model.City = GetCityByZip(model.Zip);
}
return View(model);
}
The View:
<div class="formrow">
#Html.LabelFor(model => model.City)
#Html.TextBoxFor(model => model.City)
#Html.ValidationMessageFor(model => model.City)
</div>
<div class="formrow">
#Html.LabelFor(model => model.Zip)
#Html.TextBoxFor(model => model.Zip)
#Html.ValidationMessageFor(model => model.Zip)
</div>
The problem is whenever the city is being modified, it never gets reflected on the view. During debugging, the model.City contains the correct value but it doesn't show up on view. Even something as simple as #Html.TextBoxFor(model => model.City) doesn't display the correct model.City value.
HtmlHelpers get the model values from the model state and not the model when you update and return the model. In order to update and return the model, add this line of code in your post method:
ModelState.Clear();
or you could set the value of city in the ModelState itself:
ModelState["City"].Value = GetCityByZip(model.Zip);
As Tommy noted, this is, somewhat counterintuitively, the correct behavior since form data submitted on post gets first priority when binding the data to the returned view. This makes some sense as the user is likely to have made a validation error when re-returning the same view and gets to resume their form entry as is without the problems of losing form input when restoring a page
One other option is to manually insert the value for the input
So instead of this:
#Html.TextBoxFor(model => model.City)
Do this instead:
<input type="text" name="City" value="#Model.City" />
* which will grab the value directly off the model
Or even better:
<input type="text" value="#Model.City"
name="#Html.NameFor(model => model.City)"
id="#Html.IdFor(model => model.City)" />
*Note: this won't bring in data-val attributes. If you're using them on this property for client side validation, you'll need to build list of data validation attributes for a given element
Additional Resources
HiddenFor not getting correct value from view model
HTML.HiddenFor is not updating on a postback
ASP.NET MVC Postbacks and HtmlHelper Controls ignoring Model Changes

Rendering fields in ASP.NET Razor view with configurable order

I need to allow customers to specify which fields will be drawn in which order, from among a fixed set of fields (address, home phone, SSN, first name, etc. etc.)
What is the best practice for this? I feel like an HTML helper method like "DrawField" is appropriate, but can I use helpers like Html.EditorFor in the body of an HTML helper method? When modelstate has errors and I redisplay the form, will the submitted values and errors be populated?
The "safest" approach seems to be an ugly big loop:
foreach( Field f in FieldList)
{
if(f.Key == FieldKey.FirstName)
{
#Html.LabelFor(model => model.FirstName, StringResource("firstNameLabel"))
#Html.EditorFor(model => model.FirstName)
#Html.ValidationMessageFor(model => model.FirstName)
<br />
}
if(f.Key == FieldKey.LastName)
{
......
}
}
There's gotta be a better way!
Since the list of fields is fixed I would just order it in the controller and just have n regions in the view that will render the fields in the order they arrive in the model. I would structure the model so that it contains a list of the same object, but the content will be different for each one (First name, last name etc) .You can add whatever meta data you need to your model
#Html.LabelFor(model => model.Items[0].Prop, StringResource(model.Items[0].PropName))
#Html.EditorFor(model => model.Items[0].Prop)
......
That way you don't need any conditionals or loops. It's instead n generic regions driven by the data
Although I haven't tried you can try by creating a custom editor / display templates for the Model that takes care of ordering the fields.
To get a basic idea about model templates check this link http://bradwilson.typepad.com/blog/2009/10/aspnet-mvc-2-templates-part-1-introduction.html
UPDATE:
I've tried a simple example. We have a Person model and I want to control the order in which the FirstName and LastName are displayed.
public class Person
{
[Required(ErrorMessage = "The field is required")]
[Display(Name = "First Name:")]
public string FirstName { get; set; }
[Required(ErrorMessage = "The field is required")]
[Display(Name = "Last Name:")]
public string LastName { get; set; }
}
Create a custom editor template Person.cshtml and place it in the Views/Shared folder. So whenever you call the EditorFor method for
Person this template will be used for rendering. For simplicity I'm passing the order in which they have to displayed as an array of fields through ViewBag.
#model RazorAndJson.Models.Person
#{
var fields = ViewBag.FieldsOrder != null
? ViewBag.FieldsOrder
: new[] { "FirstName", "LastName" };
}
#foreach(string field in fields)
{
<p>
#Html.Label(field)
#Html.Editor(field)
#Html.ValidationMessage(field)
</p>
}

Html.EditorFor shows the correct number of items but not the data

I'm working on my first ASP.NET MVC 3 application and I'm trying to show ingredients of a particular ice cream on a create page.
I've got a viewmodel for the page which has a structure something like this:
public class IceCreamViewModel
{
...
public IEnumerable<IngredientViewModel> Ingredients { get; set; }
}
(there are other properties but they aren't germane to the discussion)
Ingredients gets populated by the Create action on the controller and I've verified that it contains the data I want.
The IngredientViewModel has the following structure:
public class IngredientViewModel
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsChecked { get; set; }
}
In the Create view I have tried to display the collection of ingredients to allow the user to check which are in the recipe (e.g., peanuts, egg, etc.) and I'm doing something like this:
#Html.EditorFor(m => m.Ingredients)
I've written and editor template for this that looks like so:
#model IceCream.ViewModels.Ingredients.IngredientViewModel
<div>
#Html.HiddenFor(m => m.Id)
#Html.LabelFor(m => m.Name)
#Html.CheckBoxFor(m => m.IsChecked)
</div>
What I'd expect to show up is a bunch of labels and checkboxes for each of my ingredients, but what shows up is the correct number of label/checkbox entries but they all say "Name" rather than the ingredient name that is in the IngredientViewModel. So I'm certainly doing something wrong here. It obviously knows that it has N items to iterate through but it isn't picking up the properties of those items. Guidance?
Update
So, all I ended up doing was switching my LabelFor to a TextBoxFor and my values showed up... as they would, of course. (tired, long day) - #LabelFor uses the name of the property, or the annotated DisplayName for the property. Things work fine now... move along, nothing to see here...
You're trying to create a label for the Name property (as if you wanted the user to edit the Ingredient Name), instead of actually showing the name as the label for the checkbox.
How about changing:
#Html.LabelFor(m => m.Name)
... to:
#m.Name
Or, better yet:
#model IceCream.ViewModels.Ingredients.IngredientViewModel
<div>
#Html.HiddenFor(m => m.Id)
<label for="#m.Id">#m.Name</label>
#Html.CheckBoxFor(m => m.IsChecked)
</div>

Accessing models from view in MVC 2 Timesheet application?

I am trying to create a timesheet application in MVC 2, but I feel like I am still struggling to grasp the model/view relationships and all that.
The problem I have is, I want to let the user report a new time segment in a create view. But I want to have dropdownlists populated with Projects, Tasks, and Consultants from the model.
Basically the database structure looks like this:
(table) TimeSegments
TimeSegmentID
Hours
Date
ConsultantID (FK)
TaskID (FK)
ProjectID (FK)
(table) Projects
ProjectID
ProjectName
(table) Tasks
TaskID
TaskName
(table) Consultants
ConsultantID
ConsultantName
This design may be extended in future, right now I want to get basic functionality working before I complicate it further.
Now, I am passing the entire model to the create view (actually a viewmodel based on it, just to simplify some coding, but it might as well have been the entire model).
The problem is, normally when I have done similar things with a create view, I would have created a new object in the controller and passed that to the view. In this case it would have been the TimeSegment object, since it is a new time segment that should be created in the database. Then I could just submit it and update the database in the controller. However, if I only pass a new TimeSegment object to the view, I can't populate the dropdownlists with Projects, Tasks and Consultants.
And oppositely, if I only pass the entire model, how would I bind a textbox to a new TimeSegment to be updated in the database?
I feel like I need to send both a new TimeSegment object and the entire model to do this, but then I have no idea how I would access it (there's only that one "Model" to access from the view). Also, back in the controller after a submit, how would the controller know what to update?
I'm sure I'm just thoroughly confused still by the MVC way of thinking, but I would really appreciate it if someone could clarify this for me and tell me (as pedagogically as possible) what to do to solve this.
Okay, I will give it a shot.
MVC is not hard, but you do have to alter your way of thinking a bit. In MVC you have the Models (your data layer[s]), the Views and the Controllers.
Before we continue, I make the assumptions with my examples below that you are using LINQ to SQL for you data access layer (Model), and I have labeled it as dc.
The Controllers fetch and format the data out of the Models and hand it off to the Views to display. So lets start with your first view which would be the view to create a TimeSegment.
[HttpGet]
public ActionResult CreateTimeSegment() {
return View(new TimeSegmentView {
Consultants = dc.Consultants.ToList(),
Projects = dc.Projects.ToList(),
Tasks = dc.Tasks.ToList()
});
}
This action will create a TimeSegmentView object and pass that to the View as its Model. Keep in mind that this action is decorated with [HttpGet]. TimeSegmentView` is a container class for the objects you need to pass to the view to create your UI and it looks like this:
public class TimeSegmentView {
public IList<Consultant> Consultants { get; set; }
public IList<Project> Projects { get; set; }
public IList<Task> Tasks { get; set; }
public TimeSegment TimeSegment { get; set; }
}
NOTE: I'm not using the TimeSegment property yet, it's further down...
In the view make sure you have it inherit from TimeSegmentView. Assuming that you're following the default MVC project structure and with me taking the liberty to add a Views folder into the Models folder your full reference would look like this:
<%# Page Inherits="System.Web.Mvc.ViewPage<PROJECTNAME.Models.Views.TimeSegmentView>" %>
Now you've typed the view to that object and you can now interact with its properties. So, you can build a form such as:
<form action="/" method="post">
<p>
<label>Hours</label>
<input name="TimeSegment.Hours" />
</p>
<p>
<label>Date</label>
<input name="TimeSegment.Date" />
</p>
<p>
<label>Consultant</label>
<select name="TimeSegment.ConsultantID">
<% foreach (Consultant C in Model.Consultants) { %>
<option value="<%=C.ConsultantID%>"><%=C.ConsultantName%></option>
<% }; %>
</select>
</p>
<p>
<label>Project</label>
<select name="TimeSegment.ProjectID">
<% foreach (Project P in Model.Projects) { %>
<option value="<%=P.ProjectID%>"><%=P.ProjectName%></option>
<% }; %>
</select>
</p>
<p>
<label>Task</label>
<select name="TimeSegment.TaskID">
<% foreach (Task T in Model.Tasks) { %>
<option value="<%=T.TaskID%>"><%=T.TaskName%></option>
<% }; %>
</select>
</p>
</form>
As you can see it created 3 select fields and just performed loops in each of them to build up their values based off of the model.
Now, taking a submission of this form, we'll need to get the data and add it to our database with:
[HttpPost]
public RedirectToRouteResult CreateTimeSegment(
[Bind(Prefix = "TimeSegment", Include = "Hours,Date,ConsultantID,ProjectID,TaskID")] TimeSegment TimeSegment) {
dc.TimeSegments.InsertOnSubmit(TimeSegment);
dc.SubmitChanges();
return RedirectToAction("EditTimeSegment", new {
TimeSegmentID = TimeSegment.TimeSegmentID
});
}
Okay, first notice that I've named the action the same, but this one has an [HttpPost] decoration. I'm telling the action that I'm sending it a TimeSegment object and that I want it to bind the properties in the Include clause (this is mostly for security and validation). I then take the TimeSegment object I've passed in, add it to the data context, submit the changes and redirect. In this case I'm redirecting to another action to edit the object I just created passing in the new TimeSegmentID. You can redirect to what ever, this just felt appropriate to me...
[HttpGet]
public ActionResult EditTimeSegment(
int TimeSegmentID) {
return View(new TimeSegmentView {
Consultants = dc.Consultants.ToList(),
Projects = dc.Projects.ToList(),
Tasks = dc.Tasks.ToList(),
TimeSegment = dc.TimeSegments.Single(t => t.TimeSegmentID == TimeSegmentID)
});
}
In the edit action your doing the same thing as in the create action by building a new TimeSegmentView object and passing it to the view. The key difference here is that you're now populating the TimeSegment property. Your form would look something like this (shortened from above):
<form action="/<%=Model.TimeSegment.TimeSegmentID%>" method="post">
<p>
<label>Hours</label>
<input name="TimeSegment.Hours" value="<%=Model.TimeSegment.Hours%>" />
</p>
</form>
And your receiving action on the controller would look like this:
[HttpPost]
public RedirectToRouteResult EditTimeSegment(
int TimeSegmentID) {
TimeSegment TS = dc.TimeSegments.Single(t => t.TimeSegmentID == TimeSegmentID);
TryUpdateModel<TimeSegment>(TS, "TimeSegment", new string[5] {
"Hours", "Date", "ConsultantID", "ProjectID", "TaskID"
});
dc.SubmitChanges();
return RedirectToAction("EditTimeSegment", new {
TimeSegmentID = TimeSegment.TimeSegmentID
});
}
Lastly, if you want to display a list of TimeSegments you can do something like this:
[HttpGet]
public ActionResult ListTimeSegments() {
return View(new TimeSegmentsView {
TimeSegments = dc.TimeSegments.ToList()
});
}
And TimeSegmentsView looks like this:
public class TimeSegmentsView {
public IList<TimeSegment> TimeSegments { get; set; }
}
And in the View you'd want to do this:
<table>
<% foreach (TimeSegment TS in Model.TimeSegments) { %>
<tr>
<td><%=TS.Hours%></td>
<td><%=TS.Date%></td>
<td><%=TS.Project.ProjectName%></td>
<td><%=TS.Consultant.ConsultantName%></td>
<td><%=TS.Task.TaskName%></td>
</tr>
<% }; %>
</table>
I hope this is enough to give you a start. It's by no means complete, but its 5 AM and I haven't slept yet, so this will have to do for now (from me). Feel free to name your actions what you want, you don't have to stick to my naming conventions.
I would suggest however that you change the naming of the properties of your tables. For example when your writing the expressions like in the table above you'll have to do TS.Project.ProjectName and that's redundant. You're already accessing the Project property of TS through their relationship so you know you're only going to work with a Project. This then makes ProjectName a pointless blob of text re-describing the object your working with. Instead just use Name, and turn your expression to TS.Project.Name.
Anyway, just a suggestion, do what you like better. I'm passing out, so good night and happy Thanksgiving!
UPDATE
The process with collections is essentially the same as far as the controller side is conserned. It's the client side and the JavaScript that's more difficult to get going, so I'll assume that you have something established on that end.
So, here's how the controller would work. You pass in an array of TimeSegment and the model binder is smart enough to figure it out through the Prefix of your form elements.
<form action="/<%=Model.TimeSegment.TimeSegmentID%>" method="post">
<p>
<label>Hours</label>
<input name="TimeSegment[0].Hours" />
<!-- Notice the array in the prefix -->
</p>
<p>
<label>Hours</label>
<input name="TimeSegment[1].Hours" />
<!-- Notice the array in the prefix -->
</p>
</form>
And the controller:
[HttpPost]
public RedirectToRouteResult CreateTimeSegments(
[Bind(Prefix = "TimeSegment", Include = "Hours,Date,ConsultantID,ProjectID,TaskID")] TimeSegment[] TimeSegments) {
dc.TimeSegments.InsertAllOnSubmit(TimeSegments);
dc.SubmitChanges();
return RedirectToAction("ListTimeSegments");
}
And that's it. Of course you'll want to validate or do other stuff before sending to the database, but that's roughly all there is to it.
UPDATE 2
I believe you can do an IList<TimeSegment> instead of TimeSegment[] without issues, but as far as if it's better, that's up for debate. The way I look at it the browser still sends a virtual array to the server so having the action receive an array feels natural, but its up to you what you want to use.
So, a generic list action would look like this:
[HttpPost]
public RedirectToRouteResult CreateTimeSegments(
[Bind(Prefix = "TimeSegment", Include = "Hours,Date,ConsultantID,ProjectID,TaskID")] IList<TimeSegment> TimeSegments) {
dc.TimeSegments.InsertAllOnSubmit(TimeSegments);
dc.SubmitChanges();
return RedirectToAction("ListTimeSegments");
}
Keep in mind that I haven't used this (meaning the IList) before so I can't guarantee it will work, just speculating...
UPDATE 3
About what you want to do with the Consultant, it sound a lot like what I do with Cookies. I have a BaseView class which is the type used by the Site.Master and then all other views extend from it. In the BaseView I have a Cookie property which is always populated by each controller action. I then use that property to get the id of the currently authorized user.
So, in code it looks like this (using examples from one of my apps):
public class BaseView {
// Don't confuse with an HttpCookie, this is an object in my database...
public Cookie Cookie { get; set;}
}
public class EmployeeView : BaseView {
public Employee Employee { get; set; }
}
And with this, say I want to add a note to an employee, my form would look like this where I pass in a hidden field which is where your ConsultantID comes into play.
<form>
<input type="hidden" name="Note.AuthorId" value="<%=Model.Cookie.EmployeeId%>" />
<!-- other fields -->
</form>
Hope this helps.

Resources