Rendering fields in ASP.NET Razor view with configurable order - asp.net

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>
}

Related

Is using Viewbag for remembering past form inputs a bad idea?

I have a small asp.net core mvc application that basically consists of a form that a user can input some constraints into, and then get a filtered list of data depending on those constraints.
The controller action for filtering data basically looks like this:
[HttpPost]
public async Task<IActionResult> Query(QueryModel query)
{
var customers = await _context.Customers.AsQueryable().FilterCustomerList(query);
return View("Index", customers);
}
Now, my issue is that I would like the inputs in the fields to persist after entering them and being redirected to the view again. Right now they are currently just reset.
One way of doing this that I found was using viewBag. An example for a single query attribute is this:
public async Task<IActionResult> Query(QueryModel query)
{
var customers = await _context.Customers.AsQueryable().FilterCustomerList(query);
ViewBag.Name = query.Name;
return View("Index", customers);
}
and then the inpuit html elelment would look like:
<div class="col-md-4">
<input name="Name" type="text" placeholder="First name" value="#ViewBag.Name"class="form-control">
</div>
And this makes sure that if something has been entered into a field, it will now be entered into the field when after the query has been submitted.
But when I read up on ViewBag, I understand that a lot of .net developers have an aversion to it. It's not safe, the compiler can't catch errors in it easily etc.
Also, If I were to add all the input fields in my form to the viewbag, I would need a lot of lines of ViewBag.Attribute = query.SomeAttribute (20-30). Which seems like a code-smell too.
Is there any nicer way to do what I am trying to here?
You haven't included your QueryModel class and that class could be a key point to a cleaner approach.
You see, usually the user data, POSTed to your action is bound to the model, from there it's rendered on the form and is POSTed again. The model binding is where an input of a specific name is bound to a model member of the same name.
Thus, there's no need for viewbags.
More formally:
The Model
public class QueryModel
{
[your-validators-in-attributes, e.g. Required or MaxLength
there can be multiple validators]
public string Name { get; set; }
}
The controller:
[HttpPost]
async Task<IActionResult> Query(QueryModel query)
{
// query.Name is there, bound from the view
}
The View:
#model .....QueryModel
<div>
#Html.TextBoxFor( m => m.Name, new { placeholder = "a placeholder" } )
</div>
The html helper does two things
renders an input of the given name (Name in this case)
sets its value depending on the actual value from the model
In newer ASP.NETs you can achieve similar result by using tag helpers, where instead of Html.TextBoxFor(...) you write
<input asp-for="Name" />
These two approaches, using html helpers or using tag helpers are equivalent. In both cases there's no need for view bags.

Create multiple parts of complex model in the main view

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.

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>

How to get data of EditorFor with nested viewmodels

Here is my situation -
I have two nested view models:
<%=Html.EditorFor(x => x.DisplayEntitiesWithRadioboxesViewModel)%><br />
Which sit within their parent (StructureViewModel), I can populate the nested ViewModels easily and pass it through to the main View:
Within the Controller - Example
var moveDepartment = new StructureViewModel();
moveDepartment.DisplayEntitiesWithRadioboxesViewModel = fullDepartmentList.Select(x => new DisplayEntityViewModel
{
Id = x.Id,
Path = x.Path,
PathLevel = x.PathLevel,
Description = x.Description,
});
return View(moveDepartment);
EditorTemplete - Example
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IEnumerable<Site.Areas.Administration.ViewModel.DisplayEntityViewModel>>" %>
<table class="aligncenter"><%
if (Model != null)
{
foreach (var entity in Model)
{%>
<tr class="tRow">
<td style="text-align:left; text-indent:<%=Html.Encode(entity.PathLevel)%>em">
<%=Html.Encode(entity.Description)%>
<%=Html.RadioButton("radiobutton",entity.Id)%>
</td>
</tr><%
}
}%>
</table>
namespace Site.Areas.Administration.ViewModel
{
public class DisplayEntityViewModel
{
public int Id { get; set; }
public string Path { get; set; }
public string PathLevel { get; set; }
public string Description { get; set; }
}
}
However when I try to pull back this information the nested ViewModels are null:
[HttpPost]
public ActionResult Move(StructureViewModel StructureViewModel)
When I hover over StructureViewModel it only contains data set at the parent ViewModel. For example: a hidden value can been seen but DisplayEntitiesWithRadioboxesViewModel = null.
The only way I know how to access the DisplayEntitiesWithRadioboxesViewModel is to use FormCollection and iterate throught the FormCollection and pull out the information I need from the nested ViewModels.
This however just doesn't seem right, as I have found at I then have to re-populate the DisplayEntitiesWithRadioboxesViewModel with the values from the FormCollection, if for example an error has occured and the user needs to be sent back to the same View.
I have tried searching the web/books but cannot find a solution.
Is there a better way?
Thanks in advance for any help.
And why did you use an EditorFor for a
simple dropdown, which is easily to
use with DropDownFor
This has now been altered to use the DropDownFor.
what is the Key of the
DisplayEntitiesWithRadioboxesViewModel
value in FormCollection
{string[3]}
[0] = "DisplayEntitiesWithRadioboxesViewModel.radiobutton"
[1] = "Action"
[2] = "OldParentId"
Clare :-)
Your problem is pretty common and somewhat easy to fix once you understand how it works.
Right now you have a view model that has a property which is an IEnumerable<T> (doesn't matter what the generic parameter is).  You are trying to pass the items to the view and populate the IEnumerable<T> with the same values when the response comes back, using the values originally written to the page, and augmented with the selected item (at least from the code you have posted anyway, it would help for you to state your exact intention in the question).  The problem you have here is that you must send those values to the page in a way in which they can be returned.
Let me just say now that you probably should NOT be using this technique.  It is typically a much better idea to return the selection only and generate the list again if you need to server side.
From the looks of things, you want to return the whole list and then look for the item that is selected, which is after all the point of a drop down or radio button group.  In order to get the selection back, the parameter to your controller action must have properties which match the variables passed back in.  In this case, it looks like you are using the parameter name radiobutton for all of your radio buttons (the same hold true for drop down list, only it uses the name of the list).  Which ever one is selected, the value associated with it is returned with that name.  The MVC framework takes care of trying to find the appropriate action which has as many names specified as possible.  
What you need to use for your action parameter is a new class that contains a property for all of the field names being submitted back to the server!  Or of course you could simply add the radiobutton property to your StructureViewModel too.  In fact, you'll notice that it is trying to set that value already, only it doesn't currently exist on your view model.  You still will not receive the original list back however, but thats okay, because even if you did receive the original list back, you have no identifier on it to let you know which item was selected!
Hopefully this helps you understand what is going on, if you have more questions, please ask.
I would recommend you using strongly typed helpers everywhere so that you don't have to worry about naming your controls. Here's how to proceed:
Models:
public class DisplayEntityViewModel
{
public int Id { get; set; }
public string Path { get; set; }
public string PathLevel { get; set; }
public string Description { get; set; }
}
public class StructureViewModel
{
public IEnumerable<DisplayEntityViewModel> DisplayEntitiesWithRadioboxesViewModel { get; set; }
}
Controller:
public class HomeController : Controller
{
public ActionResult Index()
{
var moveDepartment = new StructureViewModel();
moveDepartment.DisplayEntitiesWithRadioboxesViewModel = new[]
{
new DisplayEntityViewModel
{
Id = 1,
Path = "some path 1",
PathLevel = "some path level 1",
Description = "some description 1"
},
new DisplayEntityViewModel
{
Id = 2,
Path = "some path 2",
PathLevel = "some path level 2",
Description = "some description 2"
},
};
return View(moveDepartment);
}
[HttpPost]
public ActionResult Index(StructureViewModel StructureViewModel)
{
return View(StructureViewModel);
}
}
Main View (~/Views/Home/Index.aspx):
<%# Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<SomeNs.Models.StructureViewModel>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<% using (Html.BeginForm()) { %>
<table class="aligncenter">
<%= Html.EditorFor(x => x.DisplayEntitiesWithRadioboxesViewModel) %>
</table>
<input type="submit" value="Go" />
<% } %>
</asp:Content>
Editor Template (~/Views/Home/EditorTemplates/DisplayEntityViewModel.ascx)
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ToDD.Models.DisplayEntityViewModel>" %>
<tr class="tRow">
<td style="text-align:left; text-indent:<%=Html.Encode(Model.PathLevel)%>em">
<%= Html.Encode(Model.Description) %>
<!-- Remember that you need to place input fields for each property
that you expect to get back in the submit action
-->
<%= Html.HiddenFor(x => x.Description) %>
<%= Html.TextBoxFor(x => x.Path) %>
</td>
</tr>
Now submit the form and everything should be bound correctly. An important thing to note is that the editor template is strongly typed to DisplayEntityViewModel and not IEnumerable<DisplayEntityViewModel> as in your case. When in your main view you write:
<%= Html.EditorFor(x => x.DisplayEntitiesWithRadioboxesViewModel) %>
the framework automatically detects that the property is a collection and will call the editor template for each item of this collection so you no longer need to loop through the elements which makes your code more elegant.
UPDATE:
Using dropdown lists is also very easy: checkout this answer.
Can you tell me how the EditorFor looks exactly? And why did you use an EditorFor for a simple dropdown, which is easily to use with DropDownFor.
what is the Key of the DisplayEntitiesWithRadioboxesViewModel value in FormCollection
If I understand it correctly, you have a View, with some parent-info, and at the same time multiple iterations of these 2 fields in the same view. Is that right?
Then I know how to fix this.

How to bind Lists of a custom view model to a dropDownList an get the selected value after POST in ASP.NET MVC?

I have following problem. In my view model I defined some list properties as follows:
public class BasketAndOrderSearchCriteriaViewModel
{
List<KeyValuePair> currencies;
public ICollection<KeyValuePair> Currencies
{
get
{
if (this.currencies == null)
this.currencies = new List<KeyValuePair>();
return this.currencies;
}
}
List<KeyValuePair> deliverMethods;
public ICollection<KeyValuePair> DeliveryMethods
{
get
{
if (this.deliverMethods == null)
this.deliverMethods = new List<KeyValuePair>();
return this.deliverMethods;
}
}
}
This view model is embedded in another view model:
public class BasketAndOrderSearchViewModel
{
public BasketAndOrderSearchCriteriaViewModel Criteria
{
[System.Diagnostics.DebuggerStepThrough]
get { return this.criteria; }
}
}
I use 2 action methods; one is for the GET and the other for POST:
[HttpGet]
public ActionResult Search(BasketAndOrderSearchViewModel model){...}
[HttpPost]
public ActionResult SubmitSearch(BasketAndOrderSearchViewModel model){...}
In the view I implement the whole view model by using the EditorFor-Html Helper which does not want to automatically display DropDownLists for List properties!
1. Question: How can you let EditorFor display DropDownLists?
Since I could not figure out how to display DropDownLists by using EditorFor, I used the DropDownList Html helper and filled it through the view model as follows:
public IEnumerable<SelectListItem> DeliveryMethodAsSelectListItem()
{
List<SelectListItem> list = new List<SelectListItem>();
list.Add(new SelectListItem()
{
Selected = true,
Text = "<Choose Delivery method>",
Value = "0"
});
foreach (var item in this.DeliveryMethods)
{
list.Add(new SelectListItem()
{
Selected = false,
Text = item.Value,
Value = item.Key
});
}
return list;
}
My 2. question: As you can see I pass my view model to the action metho with POST attribute! Is there a way to get the selected value of a DropDownList get binded to the passed view model? At the moment all the DropDownList are empty and the selected value can only be fetched by the Request.Form which I definitely want to avoid!
I would greatly appreciate some ideas or tips on this!
For those like me that got to this post these days I'd recommend you to fully download the tutorial from http://www.asp.net/mvc/tutorials/mvc-music-store-part-1 which covers this and most of the common techniques related with .NET MVC applications.
Anyway Really usefull your post and answers man (If I could vote you I would :)
Let's try to take on this one:
Answer to Question 1: How can you let EditorFor display DropDownLists?
When you call Html.EditorFor() you can pass extra ViewData values to the EdiorTemplate View:
<%: Html.EditorFor(model => Model.Criteria, new { DeliveryMethods = Model.DeliveryMethods, Currencies = Model.Currencies}) %>
Now you have ViewData["DeliveryMethods"] and ViewData["Currencies"] initialized and available inside your EditorTemplate.
In your EditorTemplate you somehow need to call and convert those entries into DropDowns / SelectLists.
Assuming you've got an ascx file of type System.Web.Mvc.ViewUserControl<BasketAndOrderSearchCriteriaViewModel> you could do the following:
<%: Html.LabelFor(model => model.DeliveryMethods) %>
<%: Html.DropDownList("SelectedDeliveryMethod", new SelectList(ViewData["DeliveryMethods"] as IEnumerable, "SelectedDeliveryMethod", "Key", "value", Model.SelectedDeliveryMethod)) %>
Same goes for the Currencies.
<%: Html.LabelFor(model => model.Currencies) %>
<%: Html.DropDownList("SelectedCurrency", new SelectList(ViewData["Currencies"] as IEnumerable, "SelectedDeliveryMethod", "Key", "value", Model.SelectedCurrency)) %>
This setup will make your DeliveryMethodAsSelectListItem() obsolete and you can use any kind of list. Means you are not bound to KeyValuePairs. You'll just need to adjust your call on Html.DropDownList() from now on.
As you can see, I have introduced some new properties to your BasketAndOrderSearchCriteriaViewModel:
Model.SelectedDeliveryMethod
Model.SelectedCurrency
They are used to store the currently selected value.
Answer to Question 2: Is there a way to get the selected value of a DropDownList get binded to the passed view model?
In the EditorFor template we are passing the newly created Model.SelectedDeliveryMethod and Model.SelectedCurrency properties as the SelectedValue Parameter (See 4th Overload of the DropDownList Extension Method).
Now that we have the View doing it's job: How can we get the currently selected value inside the POST Action?
This is really easy now:
[HttpPost]
public ActionResult SubmitSearch(BasketAndOrderSearchViewModel model)
{
...
var selectedDeliveryMethod = model.Criteria.SelectedDeliveryMethod;
var selectedCurrency model.Criteria.SelectedDeliveryMethod;
...
}
Note: I don't have an IDE to test it right now, but it should do the trick or at least show you in which direction to go.

Resources