How to bind an MVC3 view to a SelectList that is created from an IEnumerable<MyCustomType> - asp.net-mvc-3

When I try to bind in an MVC 3 view (using an #Html.DropDownList helper) to a select list based on an IEnumerable< X >, where X is a custom class I created rather than a framework class, I get the error “DataBinding: 'MyCustomNamespace.MyCustomClass' does not contain a property with the name 'MyProperty'.”. I do not get an error if I use a SelectListItem or a KeyValuePair in place of my custom class in the IEnumerable - in that case it works fine. I am guessing that the issue may be that my custom class is not known in the Html.DropDownList helper and hence can’t be accessed there? But I thought this was supposed to operate using reflection and the property names I specified during SelectList definition, so that would not be necessary… ?
Here is a simplified version of my code:
// In .cshtml file:
#Html.DropDownList("cmbSection", (SelectList)ViewBag.Section)
// In Controller:
List<MyCustomClass> filters = new List<MyCustomClass>();
MyCustomClass testItem1 = new MyCustomClass { MyProperty = "AAA"};
MyCustomClass testItem2 = new MyCustomClass { MyProperty = "BBB"};
filters.Add(testItem1);
filters.Add(testItem2);
return new SelectList(filters, "AAA", "MyPropertyName", "MyPropertyName");
// Elsewhere:
public class MyCustomClass
{
public string MyProperty
}
Thanks!

controller
//your code starts
List<MyCustomClass> filters = new List<MyCustomClass>();
MyCustomClass testItem1 = new MyCustomClass { MyProperty = "AAA"};
MyCustomClass testItem2 = new MyCustomClass { MyProperty = "BBB"};
filters.Add(testItem1);
filters.Add(testItem2);
//your code ends here
var items= (from item in filters
select new SelectListItem
{
Value= item.MyProperty
Text= item.MyProperty
}).toList();
ViewBag.items= items;
View
#Html.DropDownList("MyDropDownList", items)

Check out Phil Haacked blog Model Binding To A List. He posts almost everything about how the default model binder works with Lists.

Related

Is there a way to invalidate the ASP.NET MVC 3 ActionCache

I have an ASP.NET MVC 3 application that uses custom attributes to create select controls for model properties that can be populated from external data sources at runtime. The issue is that my EditorTemplate output appear to be cached at the application level, so my drop down lists are not updated when their data source changes until the Application Pool is recycled.
I also have output the contents of the MVC 3 ActionCache that is bound to the ViewContext.HttpContext object as shown in the MVC 3 source code in System.Web.Mvc.Html.TemplateHelpers.cs:95.
Action Cache GUID: adf284af-01f1-46c8-ba15-ca2387aaa8c4:
Action Cache Collection Type: System.Collections.Generic.Dictionary``2[System.String,System.Web.Mvc.Html.TemplateHelpers+ActionCacheItem]
Action Cache Dictionary Keys: EditorTemplates/Select
So it appears that the Select editor template is definitely being cached, which would result in the TemplateHelper.ExecuteTemplate method to always return the cached value instead of calling ViewEngineResult.View.Render a second time.
Is there any way to clear the MVC ActionCache or otherwise force the Razor view engine to always re-render certain templates?
For reference, Here are the relevant framework components:
public interface ISelectProvider
{
IEnumerable<SelectListItem> GetSelectList();
}
public class SelectAttribute : Attribute, IMetadataAware
{
private readonly ISelectProvider _provider;
public SelectAttribute(Type type)
{
_provider = DependencyResolver.Current.GetService(type) as ISelectProvider;
}
public void OnMetadataCreated(ModelMetadata modelMetadata)
{
modelMetadata.TemplateHint = "Select";
modelMetadata.AdditionalValues.Add("SelectListItems", SelectList);
}
public IEnumerable<SelectListItem> SelectList
{
get
{
return _provider.GetSelectList();
}
}
}
Next, there is a custom editor template in ~\Views\Shared\EditorTemplates\Select.cshtml.
#model object
#{
var selectList = (IEnumerable<SelectListItem>)ViewData.ModelMetadata.AdditionalValues["SelectListItems"];
foreach (var item in selectList)
{
item.Selected = (item != null && Model != null && item.Value.ToString() == Model.ToString());
}
}
#Html.DropDownListFor(s => s, selectList)
Finally, I have a view model, select provider class and a simple view.
/** Providers/MySelectProvider.cs **/
public class MySelectProvider : ISelectProvider
{
public IEnumerable<SelectListItem> GetSelectList()
{
foreach (var item in System.IO.File.ReadAllLines(#"C:\Test.txt"))
{
yield return new SelectListItem() { Text = item, Value = item };
}
}
}
/** Models/ViewModel.cs **/
public class ViewModel
{
[Select(typeof(MySelectProvider))]
public string MyProperty { get; set; }
}
/** Views/Controller/MyView.cshtml **/
#model ViewModel
#using (Html.BeginForm())
{
#Html.EditorForModel()
<input type="submit" value="Submit" />
}
** EDIT **
Based on suggestions in the comment, I started to look more closely at the ObjectContext lifecycle. While there were some minor issues, the issue appears to be isolated to an odd behavior involving a callback within a LINQ expression in the SelectProvider implementation.
Here is the relevant code.
public abstract class SelectProvider<R, T> : ISelectProvider
where R : class, IQueryableRepository<T>
{
protected readonly R repository;
public SelectProvider(R repository)
{
this.repository = repository;
}
public virtual IEnumerable<SelectListItem> GetSelectList(Func<T, SelectListItem> func, Func<T, bool> predicate)
{
var ret = new List<SelectListItem>();
foreach (T entity in repository.Table.Where(predicate).ToList())
{
ret.Add(func(entity));
}
return ret;
}
public abstract IEnumerable<SelectListItem> GetSelectList();
}
public class PrinterSelectProvider : SelectProvider<IMyRepository, MyEntityItem>
{
public PrinterSelectProvider()
: base(DependencyResolver.Current.GetService<IMyRepository>())
{
}
public override IEnumerable<SelectListItem> GetSelectList()
{
// Create a sorted list of items (this returns stale data)
var allItems = GetSelectList(
x => new SelectListItem()
{
Text = x.DisplayName,
Value = x.Id.ToString()
},
x => x.Enabled
).OrderBy(x => x.Text);
// Do the same query, but without the callback
var otherItems = repository.Table.Where(x => x.Enabled).ToList().Select(x => new SelectListItem()
{
Text = x.DisplayName,
Value = x.Id.ToString()
}).OrderBy(x => x.Text);
System.Diagnostics.Trace.WriteLine(string.Format("Query 1: {0} items", allItems.Count()));
System.Diagnostics.Trace.WriteLine(string.Format("Query 2: {0} items", otherItems.Count()));
return allItems;
}
}
And, the captured output from the System.Diagnostics.Trace is
Query 1: 2 items
Query 2: 3 items
I'm not sure what could be going wrong here. I considered that the Select may need an Expressions, but I just double-checked and the LINQ Select method only takes Func objects.
Any additional suggetions?
Problem Solved!
I finally had a chance to re-visit this issue. The root cause had nothing to do with LINQ, the ActionCache, or the ObjectContext, rather it was related to when attribute constructors are called.
As shown, my custom SelectAttribute class calls DependencyResolver.Current.GetService in its constructor to create an instance of the ISelectProvider class. However, the ASP.NET MVC framework scans the assemblies for custom metadata attributes once and keeps a reference to them in the application scope. As explained in the linked question, accessing a Attribute triggers its constructor.
So, the constructor was run only once, rather than on each request, as I had assumed. This meant that there was actually only one, cached instance of the PrinterSelectProvider class instantiated that was shared across all requests.
I solved the problem by changing the SelectAttribute class like this:
public class SelectAttribute : Attribute, IMetadataAware
{
private readonly Type type;
public SelectAttribute(Type type)
{
this.type = type;
}
public void OnMetadataCreated(ModelMetadata metadata)
{
// Instantiate the select provider on-demand
ISelectProvider provider = DependencyResolver.Current.GetService(type) as ISelectProvider;
modelMetadata.TemplateHint = "Select";
modelMetadata.AdditionalValues.Add("SelectListItems", provider.GetSelectList());
}
}
Tricky problem indeed!

Razor doesn't recognize List()?

My View begins like this:
#using MyNamespace.Models.Mapping
#{
PlacemarkKmlModel pmodel = new PlacemarkKmlModel();
pmodel.Center.Latitude = 8.52115M;
pmodel.Center.Longitude = -80.35981667M;
pmodel.KmlObjectTokens kmlIDs = new List<string>();
pmodel.KmlObjectTokens.Add("Sample1.kml");
pmodel.KmlObjectTokens.Add("Sample2.kml");
}
#Html.Partial("_Mapping", #pmodel, #ViewData)
And Intellisense is giving me a red underline in the 3rd pmodel statement "The type or namespace 'pmodel' could not be found":
pmodel.KmlObjectTokens kmlIDs = new List<string>();
That is strange because it doesn't give the error in the PlacemarkKmlModel pmodel declaration nor in the use of the same in the statements following that where it even does autocompletion of KmlObjectTokens but it craps out on the 3rd?!.
When I execute the view I get the same error:
CS0246: The type or namespace name 'pmodel' could not be found (are you missing a using directive or an assembly reference?)
The error is misleading in the sense that it complains about pmodel but what it doesn't seem to like is the List().
My model looks like this:
public class PlacemarkKmlModel
{
public ViewportCenterModel Center { get; set; }
public List<string> KmlObjectTokens { get; set; }
}
Try like this:
#using MyNamespace.Models.Mapping
#{
PlacemarkKmlModel pmodel = new PlacemarkKmlModel();
pmodel.Center.Latitude = 8.52115M;
pmodel.Center.Longitude = -80.35981667M;
pmodel.KmlObjectTokens = new List<string>();
pmodel.KmlObjectTokens.Add("Sample1.kml");
pmodel.KmlObjectTokens.Add("Sample2.kml");
}
#Html.Partial("_Mapping", pmodel, ViewData)
Notice that in your code you had #( instead of #{ to open the code snippet.
Also notice that the following is invalid C#:
pmodel.KmlObjectTokens kmlIDs = new List<string>();
I guess you wanted to simply assign the KmlObjectTokens property on the pmodel instance in which case the correct syntax is:
pmodel.KmlObjectTokens = new List<string>();
And a final remark : views are not intended to contain C# code and initialize models. That's the responsibility of the controller. I am afraid that you have mixed up the responsibilities in the MVC pattern.
You can work around the fact that razor treats < string > as an HTML tag like this:
Create a helper class where you have a factory of string lists:
public class Helpers
{
public static List<string> GetNewStringList()
{
return new List<string>();
}
}
Use that factory in the razor view to create a new list of strings:
#pmodel.KmlObjectTokens = Helpers.GetNewStringList();
But, as Darin said, you should not mix responsibilities in the MVC pattern, so the model should already contain your KmlObjectTokens list populated with the data.

How can I create a RadioButtonList in a MVC View via HTML class ( Razor syntax )

I need to show my list in a RadioButtonList , some thing like this:
#Html.RadioButtonList("FeatureList", new SelectList(ViewBag.Features))
But as you know there is no RadioButtonList class in HTML Helper class and when I use :
#Html.RadioButton("FeatureList", new SelectList(ViewBag.Features))
it shows me a blank list!
// Controller codes :
public ActionResult Rules()
{
ViewBag.Features = (from m in Db.Features where m.ParentID == 3 select m.Name);
return View();
}
Html.RadioButton does not take (string, SelectList) arguments, so I suppose the blank list is expected ;)
You could 1)
Use a foreach over your radio button values in your model and use the Html.RadioButton(string, Object) overload to iterate your values
// Options could be a List<string> or other appropriate
// data type for your Feature.Name
#foreach(var myValue in Model.Options) {
#Html.RadioButton("nameOfList", myValue)
}
or 2)
Write your own helper method for the list--might look something like this (I've never written one like this, so your mileage may vary)
public static MvcHtmlString RadioButtonList(this HtmlHelper helper,
string NameOfList, List<string> RadioOptions) {
StringBuilder sb = new StringBuilder();
// put a similar foreach here
foreach(var myOption in RadioOptions) {
sb.Append(helper.RadioButton(NameOfList, myOption));
}
return new MvcHtmlString(sb.ToString());
}
And then call your new helper in your view like (assuming Model.Options is still List or other appropriate data type)
#Html.RadioButtonList("nameOfList", Model.Options)

ViewBag property value in DropDownListFor instead of Model property value

We found strange behaviour in DropDownListFor (ASP.NET MVC3 release). It selects ViewBag property value instead of Model property value in dropdown.
Model:
public class Country {
public string Name { get; set; }
}
public class User {
public Country Country { get; set; }
}
Controller Index action:
ViewBag.CountryList = new List<Country> { /* Dropdown collection */
new Country() { Name = "Danmark" },
new Country() { Name = "Russia" } };
var user = new User();
user.Country = new Country(){Name = "Russia"}; /* User value */
ViewBag.Country = new Country() { Name = "Danmark" }; /* It affects user */
return View(user);
View:
#Html.EditorFor(user => user.Country.Name)
#Html.DropDownListFor(user => user.Country.Name,
new SelectList(ViewBag.CountryList, "Name", "Name", Model.Country), "...")
It will show text box with "Russia" value and dropdown with "Danmark" value selected instead of "Russia".
I didn't find any documentation about this behaviour. Is this behaviour normal? And why is it normal? Because it is very hard to control ViewBag and Model properties names.
This sample MVC3 project sources
I'm not so sure why this decision was made, but it was happened because MVC framework tried to use the ViewData-supplied value before using the parameter-supplied value. That's why ViewBag.Country override parameter-supplied value Model.Country.
That was how it was written in MVC framework in the private method SelectInternal.
object defaultValue = (allowMultiple) ? htmlHelper.GetModelStateValue(fullName, typeof(string[])) : htmlHelper.GetModelStateValue(fullName, typeof(string));
// If we haven't already used ViewData to get the entire list of items then we need to
// use the ViewData-supplied value before using the parameter-supplied value.
if (!usedViewData) {
if (defaultValue == null) {
defaultValue = htmlHelper.ViewData.Eval(fullName);
}
}
if (defaultValue != null) {
IEnumerable defaultValues = (allowMultiple) ? defaultValue as IEnumerable : new[] { defaultValue };
IEnumerable<string> values = from object value in defaultValues select Convert.ToString(value, CultureInfo.CurrentCulture);
HashSet<string> selectedValues = new HashSet<string>(values, StringComparer.OrdinalIgnoreCase);
List<SelectListItem> newSelectList = new List<SelectListItem>();
foreach (SelectListItem item in selectList) {
item.Selected = (item.Value != null) ? selectedValues.Contains(item.Value) : selectedValues.Contains(item.Text);
newSelectList.Add(item);
}
selectList = newSelectList;
}
This code defaultValue = htmlHelper.ViewData.Eval(fullName); tried to get the value from ViewData and if it can get the value, it will override the supplied parameter selectList with new list.
Hope it can help. Thanks.
side-node: ViewBag is just a dynamic wrapper class of ViewData.
The following line from your action method is what is confusing the code:
ViewBag.Country = new Country() { Name = "Danmark" }; /* It affects user */
That's because the html helpers look into a few different places to pick up values for the generated controls. In this case ViewData["Country"] is clashing with ModelState["Country"] Rename that property to something else and everything should work.

trouble in converting Generic List coming from a WCF Service to a DataTable

I am confused on how can I use generic methods to parse generic list into datatable/dataset. My setup:
1. I have a class Customers defined in WCF Service Library.
namespace Wcf.Sample.ServiceLibrary
{
public class Customers
{
public string ID = string.Empty;
public string CompanyName = string.Empty;
public string ContactName = string.Empty;
}
}
2. I use this class to return a generic list from my OperationContract.
namespace Wcf.Sample.ServiceLibrary
{
[ServiceContract]
public interface ICustomerService
{
[OperationContract]
List<Customers> GetAllCustomers();
}
}
3. Consume WCF Service in web client page. On button click I populate the GridView with the list returned from GetAllCustomers(). This works perfectly fine.
GridView1.DataSource = client.GetAllCustomers();
GridView1.DataBind();
4. Now the issue is, for some reason (sort/paging function) I want to actually convert the returned generic list into a datatable. To do so, I have a method that returns me a datatable which I want to bind to a GridView. Here are the methods:
public static DataTable ConvertTo<T>(System.Collections.Generic.List<T> genericList)
{
//create DataTable Structure
DataTable dataTable = CreateTable<T>();
Type entType = typeof(T);
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entType);
//get the list item and add into the list
foreach (T item in genericList)
{
DataRow row = dataTable.NewRow();
foreach (PropertyDescriptor prop in properties)
{
row[prop.Name] = prop.GetValue(item);
}
dataTable.Rows.Add(row);
}
return dataTable;
}
public static DataTable CreateTable<T>()
{
//T –> ClassName
Type entType = typeof(T);
//set the datatable name as class name
DataTable dataTable = new DataTable(entType.Name);
//get the property list
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entType);
foreach (PropertyDescriptor prop in properties)
{
//add property as column
dataTable.Columns.Add(prop.Name, prop.PropertyType);
}
return dataTable;
}
I am not sure how to call this function? How can I specify the as Customers class which is actually in a webservice? Totally lost. I would appreciate if someone can guide me on the following code, how to make it work.
GridView1.DataSource = ConvertTo<???>(client.GetAllCustomers());
I was able to resolve this issue by modifing the WCF Service itself (although I was reluctant to do so). I modified the GetAllCustomers method to return a datatable instead of generic type. In the service itself, I am converting the generic type into datatable using the same methods:
public static DataTable ConvertTo<T>(System.Collections.Generic.List<T> genericList)
{
//create DataTable Structure
DataTable dataTable = CreateTable<T>();
Type entType = typeof(T);
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entType);
//get the list item and add into the list
foreach (T item in genericList)
{
DataRow row = dataTable.NewRow();
foreach (PropertyDescriptor prop in properties)
{
row[prop.Name] = prop.GetValue(item);
}
dataTable.Rows.Add(row);
}
return dataTable;
}
public static DataTable CreateTable<T>()
{
//T –> ClassName
Type entType = typeof(T);
//set the datatable name as class name
DataTable dataTable = new DataTable(entType.Name);
//get the property list
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entType);
foreach (PropertyDescriptor prop in properties)
{
//add property as column
dataTable.Columns.Add(prop.Name, prop.PropertyType);
}
return dataTable;
}
Another thing that I noticed is that the following line
PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(entType);
would always returned null for my type. This was due to the fact that I didn't have any get/set methods in Customers class. I created get/set methods in Customer class and everything worked like a charm.
Thanks to everyone who helped and those who tried to help :)

Resources