Using VS2013, MVC5, assuming a model where there is a master/header record, and multiple related detail, the MVC view will display the header data in text boxes, and will display the detail in a web grid. The controller has an action method that accepts the id of the header record to display. There is no action method with a empty parameter list. When I click the resulting/displayed view's web grid header hyperlink I receive an error...
The parameters dictionary contains a null entry for parameter 'Id' of
non-nullable type 'System.Int32' for method
'System.Web.Mvc.ActionResult Index(Int32)' in
'WebGridTest.Controllers.HomeController'. An optional parameter must
be a reference type, a nullable type, or be declared as an optional
parameter. Parameter name: parameters
Here is my code...
Code
Models
MyDetail
namespace WebGridTest.Models
{
public class MyDetail
{
public string Column1 { get; set; }
public string Column2 { get; set; }
public string Column3 { get; set; }
}
}
MyHeader
using System.Collections.Generic;
namespace WebGridTest.Models
{
public class MyHeader
{
public MyHeader()
{
this.MyDetails = new List<MyDetail>();
}
public int Id { get; set; }
public List<MyDetail> MyDetails { get; set; }
}
}
Controllers
HomeController
using System.Web.Mvc;
namespace WebGridTest.Controllers
{
public class HomeController : Controller
{
public ActionResult Index(int Id)
{
var model = new Models.MyHeader();
model.Id = 2; // HARD CODED VALUES FOR DEMONSTRATING THIS ISSUE
var detail1 = new Models.MyDetail();
detail1.Column1 = "1A";
detail1.Column2 = "1B";
detail1.Column3 = "1C";
model.MyDetails.Add(detail1);
var detail2 = new Models.MyDetail();
detail2.Column1 = "2A";
detail2.Column2 = "2B";
detail2.Column3 = "2C";
model.MyDetails.Add(detail2);
var detail3 = new Models.MyDetail();
detail3.Column1 = "3A";
detail3.Column2 = "3B";
detail3.Column3 = "3C";
model.MyDetails.Add(detail3);
return View(viewName: "Index", model: model);
}
}
}
Views
Index.cshtml
#model WebGridTest.Models.MyHeader
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm(actionName: "Index", controllerName: "Home", method: FormMethod.Post))
{
<p></p>
#Html.TextBoxFor(x => x.Id)
<p></p>
WebGrid grid = new WebGrid(null, canPage: false);
grid.Bind(Model.MyDetails);
#grid.GetHtml(tableStyle: "table table-bordered lookup-table", columns:
grid.Columns(
grid.Column(
"Column1"
, "Column 1"
, (item) => (item.Column1)
)
,
grid.Column(
"Column2"
, "Column 2"
, (item) => (item.Column2)
)
,
grid.Column(
"Column3"
, "Column 3"
, (item) => (item.Column3)
)
)
)
}
when I run the project I receive the error described above. If I remove the parameter from action method 'Index' the program will not generate an error.
When I hover over the headers in the web page, I see the generated URL. I can see it does not include route values in the query string parameters list. It only includes sort values. Example...
http://localhost:62802/Home/Index?sort=Column1&sortdir=ASC
My example is contrived and trivial, but suffice to say, I need the Id to identify which records to display.
How can I specify the id route value on the column headers?
Upon research on the web, many developers suggest using JavaScript to modify the DOM to include the route values. That seems like it would work, but really moves away from MVC structures in favor of client-side evaluation and manipulation. The information is available in IIS/MVC, but the ability to apply the information (specifically to the web grid header anchor) is lacking.
I have found an alternative to JavaScript that keeps the solution entirely in C#. I will answer my own question as a Q&A style question.
The solution involves creating an extension method that will inject the route values into the resulting HTML prior to responding to the original client request. This entails adding the extension method class, and adding the method call in the view.
Example...
Extension Method Class
using System.Web;
namespace WebGridTest
{
public static class ExtensionMethods
{
public static IHtmlString AddRouteValuesToWebGridHeaders(this IHtmlString grid, string routeValues)
{
var regularString = grid.ToString();
regularString = regularString.Replace("?sort=", "?" + routeValues + "&sort=");
HtmlString htmlString = new HtmlString(regularString);
return htmlString;
}
}
}
Augmented View
(Notice the call to the extension method 'AddRouteValuesToWebGridHeaders')...
#model WebGridTest.Models.MyHeader
#{
ViewBag.Title = "Home Page";
}
#using (Html.BeginForm(actionName: "Index", controllerName: "Home", method: FormMethod.Post))
{
<p></p>
#Html.TextBoxFor(x => x.Id)
<p></p>
WebGrid grid = new WebGrid(null, canPage: false);
grid.Bind(Model.MyDetails);
#grid.GetHtml(tableStyle: "table table-bordered lookup-table", columns:
grid.Columns(
grid.Column(
"Column1"
, "Column 1"
, (item) => (item.Column1)
)
,
grid.Column(
"Column2"
, "Column 2"
, (item) => (item.Column2)
)
,
grid.Column(
"Column3"
, "Column 3"
, (item) => (item.Column3)
)
)
).AddRouteValuesToWebGridHeaders("Id=" + Model.Id.ToString())
}
Conclusion
Injecting into the HTML is not a preferred solution because like any other parsing algorithms it is highly assumptive. I have found no other way than HTML parsing. Perhaps downloading the Microsoft open-source version of this class and creating an override with the desired behavior would be a more elegant solution.
** Request ** - Microsoft - please add the ability to control/augment the anchor in the web grid header rendering.
Please feel free to comment...
Related
First I thought that I'm using the wrong overload again (a very common gotcha in the API - everybody trips over that one). But wouldn't you know? That's not it. I actually had the HTML attributes parameter too and I verified with intellisense that it's the route values I'm entering.
#Html.ActionLink("Poof", "Action", "Home", 10, new { #class = "nav-link" })
Nevertheless, it seem that the receiving method below only sees null and crashes as it can't make an integer out of it.
public ActionResult Record(int count) { ... }
I've tried a few things: changed parameter type to int? and string (the program stops crashing but the value is still null). I've tested to package the passed value as an object (with/without #).
#Html.ActionLink("Poof", "Record", "Home",
new { count = "bamse" },
new { #class = "nav-link" })
I can see that the anchor produced has my value as a query string, so the changes are there. However, I still get null only in the method.
What am I missing?
The weird thing is that the following works fine.
#Html.ActionLink("Poof", "Record", "Home",
new Thing(),
new { #class = "nav-link" })
public ActionResult Record(Thing count) { ... }
Your using the overload of #Html.ActionLink() that expects the 4th parameter to be typeof object. Internally the method builds a RouteValueDictionary by using the .ToString() value of each property in the object.
In your case your 'object' (an int) has no properties, so no route values are generated and the url will be just /Home/Action(and you program crashes because your method expects a non null parameter).
If for example you changed it to
#Html.ActionLink("Poof", "Action", "Home", "10", new { #class = "nav-link" })
i.e. quoting the 4th parameter, the url would now be /Home/Action?length=2 because typeof string has a property length and there a 2 characters in the value.
In order to pass a native value you need to use the format
#Html.ActionLink("Poof", "Action", "Home", new { count = 10 }, new { #class = "nav-link" })
Which will generate /Home/Action?count=10 (or /Home/Action/10 if you create a specific route definition with Home/Action/{count})
Note also that passing a POCO in your case only works correctly because your POCO contains only value type properties. If for example, it also contained a property which was (say) public List<int> Numbers { get; set; } then the url created would include ?Numbers=System.Collections.Generic.List[int] (and binding would fail) so be careful passing complex objects in an action link
Hard to say what might be wrong with your code from the information provided in your question but assuming total defaults (a newly created ASP.NET MVC application in Visual Studio), if you add the following markup in your ~/Views/Home/Index.cshtml:
Html.ActionLink(
"Poof",
"Record",
"Home",
new { count = "bamse" },
new { #class = "nav-link" }
)
and the following action in your HomeController:
public ActionResult Record(string count)
{
return Content(count);
}
upon clicking on the generated anchor, the correct action will be invoked and the correct parameter passed to it.
The generated markup will look like this:
<a class="nav-link" href="/Home/Record?count=bamse">Poof</a>
So I guess that now the question that you should be asking yourself is: how does my setup differs with what Darin has outlined here? Answering this question might hold the key to your problem.
UPDATE:
OK, now you seem to have changed your question. You seem to be trying to pass complex objects to your controller action:
public ActionResult Record(Thing count) { ... }
Of course this doesn't work as you might expect. So make sure that you pass every single property that you want to be available when constructing your anchor:
Html.ActionLink(
"Poof",
"Record",
"Home",
new { ThingProp1 = "prop1", ThingProp2 = "prop2" },
new { #class = "nav-link" }
)
Alternatively, and of course a much better approach to handle this situation is to attribute an unique identifier to your models, so that all its needed in order to retrieve this model from your backend is this identifier:
Html.ActionLink(
"Poof",
"Record",
"Home",
new { id = "123" },
new { #class = "nav-link" }
)
and then in your controller action simply use this identifier to retrieve your Thing:
public ActionResult Record(int id)
{
Thing model = ... fetch the Thing using its identifier
}
I am reading a lot of posts since Last week but there is nothing about my problem.
I have this simple Controller:
public class HomeController : Controller
{
private AdventureWorks2014Entities db = new AdventureWorks2014Entities();
public ActionResult Index()
{
var model = db.Products.ToList();
ViewBag.Products = new SelectList(db.Products, "ProductID", "Name");
return View(model);
}
}
And this simple View :
#model IEnumerable<ADWork.Client.Models.Product>
#{
var grid = new WebGrid(Model);
}
#grid.GetHtml(
columns: grid.Columns(
grid.Column("ProductID"),
grid.Column("Name", null, format:#<span>#Html.DropDownList("Products", #item.Name, new { style = "color:red;" }) </span>),
grid.Column("ListPrice", "Price")
)
)
Now the problem is the DropDownList. I can render it very easy like this:
#Html.DropDownList("Products", null , new { style = "color:red;" })
But I want to add the #item.Name to show the default String, and THAT is the problem, it is not working the # (at sign) inside of the #HTML.DropDownList, it isn't yellow, it is like turn off because the # in the beginning of the line like I posted in the beginning.
Any Idea how to solve this without creating a new SelectList(blah blah), just converting the #item.Name to String?
FIND THE SOLUTION AFTER 6 days trying and trying!!!!!!!!!!!!
#grid.GetHtml(
columns: grid.Columns(
grid.Column("ProductID"),
grid.Column("Name", null, format:
#<span>
grid.Column("Name", null, format:
#<span>
#{
string name = (#item.Name).ToString();
#Html.DropDownList("Products", null, name , new { style = "color:red;" });
}
</span>),
</span>),
grid.Column("ListPrice", "Price")
)
)
I had to create a new string with the #item.Name and that's all!!! ufff I saw so many post where people where creating a ViewBag, and later creating a new list in the DropDOwnList that it didn't make sense...well here is the answer!!!
I am using the latest version Telerik MVC controls. I am using ASP.NET MVC 3 with razor.
I have a grid that lists all of my grant applications. I am wanting to use a grid that loads these grant applications via AJAX. I also need to create a client template column that has action links. These action links can vary depending on the state of each grant application.
I worked through the article at: http://gedgei.wordpress.com/2011/07/02/telerik-mvc-grid-actionlink-column/. I implemented the code as is and it works, I can create a client template column with a link in it. In my scenario I need to be able to pass in 2 parameters to the helper method, like:
column.ActionLink("Open", "Edit", "GrantApplication", item => new { id = item.Id, applicationStateId = item.GrantApplicationStateType.Id });
How I eventually implement this method in the end will change, but for now I am playing with these 2 input parameters to see how they are passed through and how I can retrieve them in the helper method.
The first question that I have regarding the article, why does the writer do the following:
var builder = factory.Template(x =>
{
var actionUrl = urlHelper.Action(action, controller, routeValues.Compile().Invoke(x));
return string.Format(#"{1}", actionUrl, linkText);
});
I can only assume that this is the server side template that is created? But nothing displays in the grid, so how do I skip this part and go directly to the client template (this is what I actually need).
The following part is also confusing because when the first parameter (id) check comes through then it is of type ParameterExpression so it goes into the true part of the if, but when the second parameter (grant application state id) comes in then it is of another type (not sure what) so then it goes into the false part of the if statement:
switch (argument.NodeType)
{
case ExpressionType.Constant:
value = ((ConstantExpression)argument).Value;
break;
case ExpressionType.MemberAccess:
MemberExpression memberExpression = (MemberExpression)argument;
if (memberExpression.Expression is ParameterExpression)
value = string.Format("<#= {0} #>", memberExpression.Member.Name);
else
value = GetValue(memberExpression);
break;
default:
throw new InvalidOperationException("Unknown expression type!");
}
When the second paramenter values goes into the false part of the if statement it fails here:
value = GetValue(memberExpression);
..and gives the following error message which I have no idea what it is:
variable 'item' of type MyProject.ViewModels.GrantApplicationListViewModel' referenced from scope '', but it is not defined
Here is my view model:
public class GrantApplicationListViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullNameDisplay
{
get { return FirstName + " " + LastName; }
}
public DateTime CreatedDate { get; set; }
public GrantApplicationStateType GrantApplicationStateType { get; set; }
}
Here is my partial grid declaration in my view:
#(Html.Telerik()
.Grid<GrantApplicationListViewModel>()
.Name("grdGrantApplications")
.Columns(column =>
{
column.Bound(x => x.Id)
.ClientTemplate(
"<label class=\"reference-number\">" + "<#= Id #>" + "</label>"
)
.Title("Ref #")
.Width(70);
column.Bound(x => x.FullNameDisplay)
.Title("Owner")
.Width(200);
column.Bound(x => x.GrantApplicationStateType.Name)
.Title("Status")
.Width(90);
//column.ActionLink("Edit", "Edit", "GrantApplication", item => new { id = item.Id });
column.ActionLink("Open", "Edit", "GrantApplication", item => new { id = item.Id, applicationStateId = item.GrantApplicationStateType.Id });
})
.DataBinding(dataBinding => dataBinding.Ajax().Select("AjaxGrantApplicationsBinding", "Home"))
.Pageable(paging => paging.PageSize(30))
.TableHtmlAttributes(new { #class = "telerik-grid" })
)
What I am trying to achieve with the above is code is something to the effect of:
if grant application id = 1
then return Edit link and View link
else
then return Details link
How would I do the above? Is the code in that article the only way to do it? Isn't there a more simplar way? I did Google and couldn't find much help on what I want to do. Has any one else come across something like this?
If all you want is the client template to display different content based on the application id, it would be simpler to just put a conditional in the client template.
column.Bound(x => x.Id)
.ClientTemplate("<# if (Id == 1 ) { #> Edit Link and View Link <# } else { #> Details Link <# } #>");
The Edit, View, and Details links would be put in the same way they are put in without the conditional.
I've currently got all of this mess at the top of my ViewModels which I feel violates the purpose of a DTO. For example, this is in the constructor of one of my view models -
Dictionary<int, string> chargeGroups = new Dictionary<int, string>();
chargeGroups.Add(1, "Administration");
chargeGroups.Add(2, "Annual Leave");
chargeGroups.Add(3, "Bereavement");
chargeGroups.Add(4, "Customer Installation, Setup & Training");
chargeGroups.Add(5, "Customer Support");
chargeGroups.Add(6, "Internal Training & Education");
chargeGroups.Add(7, "Sales & Marketing");
chargeGroups.Add(8, "Sick");
chargeGroups.Add(9, "Software Devel / Maint / Test");
chargeGroups.Add(10, "Software Upgrade / Patch");
chargeGroups.Add(11, "Other");
chargeGroups.Add(12, "Other Absence");
chargeGroups.Add(13, "Warranty");
chargeGroups.Add(14, "Public Holiday");
chargeGroups.Add(15, "Other Paid Leave");
ChargeGroups = new SelectList(chargeGroups, "Key", "Value");
My viewmodel:
[DisplayName("Charge group")]
public short? ChargeGroup { get; set; }
public SelectList ChargeGroups;
then in my view:
<div class="editor-label">
#Html.LabelFor(model => model.ChargeGroup)
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.ChargeGroup, Model.ChargeGroups)
#Html.ValidationMessageFor(model => model.ChargeGroup)
</div>
Where should I be putting this stuff?
When I have a list of values that wont change I usualy use an Enum and then use a custom Html Helper to render out a select list. You can customzie the display text of the enum values by using meta data description markup.
That way you can just use:
<%: Html.EnumDropDownListFor(model => model.EnuProperty) %>
or
#Html.EnumDropDownListFor(model => model.EnuProperty)
Check out this post by Simon which allows you to use the Meta Description attribute to customzie the output for Enum names:
How do you create a dropdownlist from an enum in ASP.NET MVC?
Here is another example but it lacks the meta description property:
http://blogs.msdn.com/b/stuartleeks/archive/2010/05/21/asp-net-mvc-creating-a-dropdownlist-helper-for-enums.aspx
EDIT
Your enum might look something like this
public enum ChargeGroupEnum
{
[Description("Administration")]
Administration=1,
[Description("Annual Leave")]
AnnualLeave=2,
[Description("Bereavement")]
Bereavement=3,
[Description("Customer Installation, Setup & Training")]
CustomerInstallation=4,
[Description("Customer Support")]
CustomerSupport=5,
[Description("Internal Training & Education")]
InternalTraining=6,
[Description("Sales & Marketing")]
SalesMarketing=7,
[Description("Sick")]
Sick=8,
[Description("Software Devel / Maint / Test")]
Development=9,
[Description("Software Upgrade / Patch")]
Upgrade=10,
[Description("Other")]
Other=11,
[Description("Other Absence")]
OtherAbsence=12,
[Description("Warranty")]
Warranty=13,
[Description("Public Holiday")]
PublicHoliday=14,
[Description(")ther Paid Leave")]
OtherPaidLeave=15
}
And then on your View Model you could use the following to make the field start with no value and REQUIRE a value:
[Required(ErrorMessage=" * required")]
public ChargeGroupEnum? ChargeGroup {get;set;}
And then in your view you would use the Html Helper "ChargeGroupEnum" which you'll need to get from the post I linked to.
#Html.EnumDropDownListFor(model => Model.ChargeGroup)
If your model has an Int, you can easly go from Enum => Int and Int => Enum with casting.
I think that Caching that data after loading it from datasource and rendering the control based on that data through a helper method will be a better solution.
I decided to keep the values for the select list on another class called OtherData which sits beside my main DBContext class in my models file.
Wouldn't it be more ideal to create the SelectList in the view, vs. view model, just so the view models aren't AS tightly coupled to a view object (SelectList)?
I know a view model is for a view so tightly coupling them is more ok, but theoretically if the view is swapped out with something that doesn't use a SelectList, you could reuse more of the view model if it didn't use that SelectList.
public static List<string> PaymentCurrency = new List<string> { "USD", "GBP", "EUR", "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "FJD", "HKD", "HNL", "HUF", "IDR", "ILS", "INR", "ISK", "JPY", "KRW", "LVL", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR" };
List<SelectListItem> PaymentCurrencyOptionItems = new List<SelectListItem>() { new SelectListItem { Text = "", Value = "" } };
PaymentCurrencyOptionItems.AddRange(Lolio.PaymentCurrency.Select(r => new SelectListItem { Text = r+" "+LangResources.Get("Currency_" + r), Value = r }));
IEnumerable<SelectListItem> LinkPaymentType = new SelectList(PaymentTypeOptionItems, "Value", "Text", lnk.Performance.PaymentType);
Html.DropDownListFor(m => m.PaymentType, LinkPaymentType))
In my viewData I have an IList mls.
I want to use this to show in a dropdown. Like so:
<%= Html.DropDownList("ml3Code",
new SelectList(Model.Mls, "Code", "Description", Model.Ml3.Code ?? ""),
Model.T9n.TranslateById("Labels.All"),
new { #class = "searchInput" })%>
This works fine, until there's a myObject.Code == VOC<420 g/l.
I would have expected that an HTML helper would encode its values, but it doesn't.
How should I approach this problem? The only thing I can come up with is first making a dupe list of the objects with encoded values and then feeding it to the selectlist. This would be really bothersome.
P.S. I hope Phill H. and his team will have a long and thorough look at the encoding for asp.net-mvc 2.0...
I'm puzzled. The question "Do ASP.NET MVC helper methods like Html.DropDownList() encode the output HTML?" was asked on SO before, and the answer was "Yes" - and the source-code from the MVC framework was cited to back this assertion up.
Well, you can roll your own Html helper, but if you're like me you won't want to do that.
To me, I see two options here:
Write your select element in plain view without the helper. I've never felt the helpers provide you much save for highlighting an element when an error occurs.
Patch the select box on the client when the page loads, as in:
function encodeHtml(str)
{
var encodedHtml = escape(str);
encodedHtml = encodedHtml.replace(///g,"%2F");
encodedHtml = encodedHtml.replace(/\?/g,"%3F");
encodedHtml = encodedHtml.replace(/=/g,"%3D");
encodedHtml = encodedHtml.replace(/&/g,"%26");
encodedHtml = encodedHtml.replace(/#/g,"%40");
return encodedHtml;
}
window.onload = function()
{
var ml3Code = document.getElementById("ml3Code");
for(var i = 0; i < ml3Code.options.length; ++i)
{
ml3Code.options[i].value = encodeHtml(ml3Code.options[i].value);
}
};
It's a hack, I know. I strongly prefer the first choice.
This is encoded. But dont check with firebug - It shows values decoded.
Check in ViewSource of the Browser and things are encoded.
Controller
public List<CategoryInfo> GetCategoryList()
{
List<CategoryInfo> categories = new List<CategoryInfo>();
categories.Add(new CategoryInfo { Name = "Food<äü", Key = "VOC<420 g/l", ID = 2, Uid = new Guid("C0FD4706-4D06-4A0F-BC69-1FD0FA743B07") });
}
public ActionResult Category(ProductViewModel model )
{
IEnumerable<SelectListItem> categoryList =
from category in GetCategoryList()
select new SelectListItem
{
Text = category.Name,
Value = category.Key
};
model.CategoryList = categoryList;
return View(model);
}
View
<%= Html.DropDownList("Category" , Model.CategoryList) %>
Model
public class ProductViewModel
{
public IEnumerable<SelectListItem> CategoryList { get; set; }
public List<CategoryInfo> Categories { get; set; }
}
HTML
<select id="Category" name="Category"><option value="VOC<420 g/l">Food<äü</option>
</select>