Where should SelectListItem lists go? - c#

In an MVC application, I have some DropDownLists. In my controller I create the IEnumerable<SelectListItem>s and transfer them to my View. Unfortunately, if there is a validation error, I need to recreate those lists, otherwise the view rendering fails.
In the controller action method I'm doing:
var possibilities = _repo.Objects.Select(o=>new SelectListItem(...));
viewmodel.Possibilities = possibilities;
return View(viewmodel);
The view-model has the Possibilities property defined like this:
IEnumerable<SelectListItem> Possibilities { get; set; }
And in my view I access it:
#Html.DropDownListFor(vm=>vm.ThePossibility, vm.Possibilities)
The problem is that when the form post action method is called, the view model passed to it has a null for Possibilities, so when I call:
if(!ModelState.IsValid)
return View(model);
The view doesn't get rendered.
I understand why the propery is null on the post action method, but what's the best way of correcting this? I'd rather not reinitialize those lists.
Thanks.

If you don't want to re-initialize the lists, you will have to cache them somewhere, such as the session or somewhere else.
Frankly, in most cases, it's just simpler to rebuild them. You will have to re-assign them every time.

You should look into using the Post-Redirect-Get pattern; There is a nice pair of attributes described in this blog post that make this very easy to do in ASP.Net MVC.

I usually cache these somewhere or provide a static class for getting common lists. You can then provide access to these in your model with a getter.
For example:
IEnumerable<SelectListItem> _possibilities
IEnumerable<SelectListItem> Possibilities
{
get
{
if (_possibilities == null)
_possibilities = CommonLists.Possibilities();
return possibilities;
}
}

Accessors and JSON (NetwonSoft) are your friends.
In a nutshell
When you set the IEnumerable<SelectListItem> property of your model, serialize it to a public string property.
When your public string property is being set, and the IEnumerable<SelectListItem> is not defined (e.g. null), deserialize the string property to the IEnumerable<SelectListItem> property of your model.
In your view, embed the serialized string so that it is persisted between posts to the server. (Eg. #Html.HiddenFor(...))
Model
public class MyViewModel
{
public IEnumerable<SelectListItem> Selections
{
get
{
return selections;
}
set
{
selections= value;
// serialize SelectListItems to a json string
SerializedSelections = Newtonsoft.Json.JsonConvert.SerializeObject(value);
}
}
IEnumerable<SelectListItem> selections;
public string SerializedSelections
{
get
{
return serializedSelections;
}
set
{
serializedSelections = value;
if(Selections == null)
{
// SelectListItems aren't defined. Deserialize the string to the list
Selections = Newtonsoft.Json.JsonConvert.DeserializeObject<IEnumerable<SelectListItem>>(value);
}
}
}
string serializedSelections;
}
Razor View
<form>
...
#Html.HiddenFor(m => m.SerializedSelections)
...
#Html.DropDownListFor(m => m.SomeProperty, Model.Selections)
</form>

Related

How can I get the metadata of the items in a collection in razor view-engine?

I have a project written in C# on the top on ASP.NET MVC 5 framework. I am trying to decouple my views from my view model so I can make my views reusable. With the heavy use of EditorTemplates I am able to create all of my standard views (i.e create, edit and details) by evaluating the ModelMetadata and the data-annotation-attributes for each property on the model, then render the page. The only thing that I am puzzled with is how to render the Index view.
My index view typically accepts an IEnumerable<object> or IPagedList<object> collection. In my view, I want to be able to evaluate the ModelMetadata of a each object/record in the collection to determine if a property on the object should be displayed or not.
In another words my view-model will look something like this
public class DisplayPersonViewModel
{
public int Id{ get; set; }
[ShowOnIndexView]
public string FirstName { get; set; }
[ShowOnIndexView]
public string LastName { get; set; }
[ShowOnIndexView]
public int? Age { get; set; }
public string Gender { get; set; }
}
Then my Index.cshtml view will accepts IPagedList<DisplayPersonViewModel> for each record in the collection, I want to display the value of the property that is decorated with ShowOnIndexView attribute.
Typically I would be able to do that my evaluating the ModelMetadata in my view with something like this
#model IPagedList<object>
#{
var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata))
.OrderBy(x => x.Order)
.ToList();
}
<tr>
#foreach(var element in elements)
{
var onIndex = element.ContainerType.GetProperty(element.PropertyName)
.GetCustomAttributes(typeof(ShowOnIndexView), true)
.Select(x => x as ShowOnIndexView)
.FirstOrDefault(x => x != null);
if(onIndex == null)
{
continue;
}
#Html.Editor(element.PropertyName, "ReadOnly")
}
</tr>
Then my controller will look something like this
public class PersonController : Controller
{
public ActionResult Index()
{
// This would be a call to a service to give me a collection with items. but I am but showing the I would pass a collection to my view
var viewModel = new List<DisplayPersonViewModel>();
return View(viewModel);
}
}
However the problem with evaluating ModelMetadata for the IPagedList<DisplayPersonViewModel> is that it gives me information about the collection itself not about the generic type or the single model in the collection. In another words, I get info like, total-pages, items-per-page, total-records....
Question
How can I access the ModelMetadata info for each row in the collection to be able to know which property to display and which not to?
I will preface this answer by recommending you do not pollute your view with this type of code. A far better solution would be to create a custom HtmlHelper extension method to generate the html, which gives you far more flexibility, can be unit tested, and is reusable.
The first thing you will need to change is the model declaration in the view which needs to be
#model object
otherwise you will throw this exception (List<DisplayPersonViewModel> is not IEnumerable<object> or IPagedList<object>, but it is object)
Note that it is not clear if you want the ModelMetadata for the type in the collection or for each item in the collection, so I have included both, plus code that gets the type
#model object
#{
var elements = ViewData.ModelMetadata.Properties.Where(metadata => !metadata.IsComplexType && !ViewData.TemplateInfo.Visited(metadata)).OrderBy(x => x.Order).ToList();
// Get the metadata for the model
var collectionMetaData = ViewData.ModelMetadata;
// Get the collection type
Type type = collectionMetaData.Model.GetType();
// Validate its a collection and get the type in the collection
if (type.IsGenericType)
{
type = type.GetInterfaces().Where(t => t.IsGenericType)
.Where(t => t.GetGenericTypeDefinition() == typeof(IEnumerable<>))
.Single().GetGenericArguments()[0];
}
else if (type.IsArray)
{
type = type.GetElementType();
}
else
{
// its not a valid model
}
// Get the metadata for the type in the collection
ModelMetadata typeMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, type);
}
....
#foreach (var element in collectionMetaData.Model as IEnumerable))
{
// Get the metadata for the element
ModelMetadata elementMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => element, type);
....
Note that using reflection to determine if the attribute exists in each iteration of your loop is inefficient, and I suggest you do that once (based on the typeMetadata). You could then (for example) generate an array of bool values, and then use an indexer in the loop to check the value). A better alternative would be to have your ShowOnIndexView attribute implement IMetadataAware and add a value to the AdditionalValues of ModelMetadata so that var onIndex code is not required at all. For an example of implementing IMetadataAware, refer CustomAttribute reflects html attribute MVC5

C# Is it possible to model bind multiple options?

I have a dropdown on which you can select multiple options.
The code for this dropdown is:
How can i bind multiple 'devices' in C# so that when this dropdown is loaded
model binding will automatically select all options which are passed into the view?
For your case you should use another helper - #Html.ListBoxFor it should generate select element with multiple attribute.
//note that i use MaintanceDevices property
#Html.ListBoxFor(x => x.MaintanceDevices, new SelectList(Model.Devises, "ID", "Description"), new { #class = "multiselect form-control"})
Also, don't set id attribute in helper. It's better to create another property in your ViewModel:
public List<int> MaintanceDevices { get; set; }
Populate it in Controller and MVC automatically generate right markup for your select element bind in on form POST.
In this situations, i would do the following inside the viewmodel
public string Devices { get; set; }
List<int> innerList;
public List<int> List
{
get
{
if (this.innerList == null)
{
if (string.IsNullOrEmpty(this.Devices))
{
this.innerList = this.Devices.Split(',').Select(x => int.Parse(x)).ToList();
}
else
{
this.innerList = new List<int>();
}
}
return this.innerList;
}
}
Where Devices is the binded property with the dropdown, which it returns all items separated by ,.
When you try to access List it will separate the items and return it as a List<int>.
And i'm parsing it to int because normally i see int's as ID's
But i'm looking forward for a better option.
PS
I do this when working with Select2
Devices property in your model should be a list of Ids (where is a simple type like int or a string) and not a list of Device models (Since you are using new SelectList(Model.Devices, "ID", "Description") in the Helper it is i see that Model.Devices is a collection of complex object)
So your model should look like:
public List<Device> AvailableDevices { get;set; }
public List<string> Devices { get;set; }
and the Helper should be
#Html.ListBoxFor(m=>m.Devices,new SelectList(Model.AvailableDevices , "ID", "Description"))
or
#Html.DropDownListFor(m=>m.Devices,new SelectList(Model.AvailableDevices , "ID", "Description", new {multiple="multiple"})
post action should receive either a List<string> as an argument or a full model:
[HttpPost]
public ActionResult Submit(List<string> devices)
or
[HttpPost]
public ActionResult Submit(YourModel model)
//where YourModel model is the same type that you are using to render your view

Getting values for a Model property from a Web API call

Currently I'm populating dropdowns options using an API call. In my model I have something like this:
public List<SelectListItem> getOptions
{
get
{
var options = WebApiHelper.Download<IEnumerable<T>>(
"Controller");
var dropDownOptions = options.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.Value }).ToList();
return dropDownOptions;
}
}
And this is called in a couple of places in the .cshtml (e.g. see below):
#Html.DropDownListFor(m => m.someProperty, Model.getOptions)
List<SelectListItem> GetDropdownOptions()
{
var currentDropdownItems = Model.getOptions;
//some other code to modify dropdown options.
}
Will the Web API only be hit once when I call Model.getOptions? Or will it be called every time since it is inside the get for that property? If it is the latter, what is a good work around for that issue?
Edit: thought about it a little more, would it be better to have these controller populate the values for that property? I have a feeling that an api call gets placed on every call to model.getoptions.
You WebApiHelper.Download() will be called each time you access the property and, if you were editing a collection, your current implementation could seriously degrade performance.
While you could define a private field (say) private List<SelectListItem> _Options; and in the getter, check if its null and if so, populate it using your WebApiHelper.Download() method, and then return _Options; this is still not a preferred implementation since you cannot easily unit test your app.
private List<SelectListItem> _options;
public List<SelectListItem> getOptions
{
get
{
if (_options == null)
{
_options = // call your service
}
return _options;
}
}
Keep your view models as dumb as possible and make your property
public List<SelectListItem> Options { get; set; }
and in the controller, initialize your model and call WebApiHelper.Download() to populate the collection (and inject WebApiHelper into your controller)

How can I return an empty list instead of a null list from a ListBoxFor selection box in Asp.net MVC?

controller get action
IList<InputVoltage> inputVoltagesList = unitOfWorkPds.InputVoltageRepository.GetAll.ToList();
pdsEditViewModel.InputVoltageList = inputVoltagesList.Select(m => new SelectListItem { Text = m.Name, Value = m.Id.ToString() });
ViewModel
public IEnumerable<SelectListItem> InputVoltageList { get; set; }
public List<int> SelectedInputVoltages { get; set; }
View
#Html.ListBoxFor(m => m.SelectedInputVoltages, Model.InputVoltageList)
I want to receive a null list when a user makes no selections the selectedInputvoltages comes into my post controller action as null how do I get it to come in as an empty list?
I like both answers is there any benefit in using one over the other?
Either make sure it is initialized in the controller (or model/viewmodel) if null, or perhaps (ugly code though) use the coalesce operator to initialize on the fly if null:
#Html.ListBoxFor(m => m.SelectedInputVoltages, Model.InputVoltageList ?? new List<SelectListItem>())
If you initialize the list in the view model's constructor then it will always be at least an empty list. Anything which builds an instance of the view model would continue to set the list accordingly.
public class SomeViewModel
{
public List<int> SelectedInputVoltages { get; set; }
public SomeViewModel()
{
SelectedInputVoltages = new List<int>();
}
}
This way it will never be null in an instance of SomeViewModel, regardless of the view, controller, etc.
If you always want the view model's property to have a default value, then the best place to put that is in the view model. If that logic is instead placed in the controller or the view then it would need to be repeated any time you want to use it.

How do I transfer ViewModel data between POST requests in ASP.NET MVC?

I have a ViewModel like so:
public class ProductEditModel
{
public string Name { get; set; }
public int CategoryId { get; set; }
public SelectList Categories { get; set; }
public ProductEditModel()
{
var categories = Database.GetCategories(); // made-up method
Categories = new SelectList(categories, "Key", "Value");
}
}
Then I have two controller methods that uses this model:
public ActionResult Create()
{
var model = new ProductEditModel();
return View(model);
}
[HttpPost]
public ActionResult Create(ProductEditModel model)
{
if (ModelState.IsValid)
{
// convert the model to the actual entity
var product = Mapper.Map(model, new Product());
Database.Save(product);
return View("Success");
}
else
{
return View(model); // this is where it fails
}
}
The first time the user goes to the Create view, they are presented with a list of categories. However, if they fail validation, the View is sent back to them, except this time the Categories property is null. This is understandable because the ModelBinder does not persist Categories if it wasn't in the POST request. My question is, what's the best way of keeping Categories persisted? I can do something like this:
[HttpPost]
public ActionResult Create(ProductEditModel model)
{
if (ModelState.IsValid)
{
// convert the model to the actual entity
var product = Mapper.Map(model, new Product());
Database.Save(product);
return View("Success");
}
else
{
// manually populate Categories again if validation failed
model.Categories = new SelectList(categories, "Key", "Value");
return View(model); // this is where it fails
}
}
But this is an ugly solution. How else can I persist it? I can't use a hidden field because it's a collection.
I would use the repository to fetch whatever data is needed and don't think it's an ugly solution:
[HttpPost]
public ActionResult Create(ProductEditModel model)
{
if (!ModelState.IsValid)
{
// manually populate Categories again if validation failed
model.Categories = Repository.GetCategories();
return View(model);
}
// convert the model to the actual entity
var product = Mapper.Map(model, new Product());
Database.Save(product);
// I would recommend you to redirect here
return RedirectToAction("Success");
}
To further refactor this I would recommend you watching the excellent Putting Your Controllers on a Diet video presentation by Jimmy Bogard.
I typically implement my lists (for drop downs) as a readonly property. When the View gets the value the property is self contained on what it needs to return the values.
public SelectList Categories
{
get
{
var categories = Database.GetCategories(); // made-up method
return new SelectList(categories, "Key", "Value");
}
}
If necessary you can grab the currently selected item (i.e. validation failed) from the property containing the id that was posted and bound to the instance of your class.
In my case I have a BaseModel class where I keep all those property list as class attributes.
Like in the following sample:
public IEnumerable<SelectListItem> CountryList
{
get
{
return GetCountryList().Select(
t => new SelectListItem { Text = t.Name, Value = Convert.ToString(t.CountryID) });
}
}
GetCountryList() is a function that ask a Singleton for data. This would only happen once in the app lifecycle
Another way for doing this, and if those lists are pretty big, would be to have a static utility class with the lookup table that returns the SelectListItem.
If you need to access a list that change from time to time then simply dont use a Singleton class.

Categories

Resources