I am trying to create a Html Helper which takes two model properties. In my example below my model has two fields Height and HeightUnit. The code in the helper will render a Bootstrap textbox with a drop down list of units in an input group. The first model property is bound to the textbox and the second is bound to the drop down list. The code does not error on compile but when it gets the display name of the 2nd expression it fails with the following error:
"Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions."
Here is the Html Helper declaration:
public static MvcHtmlString MaterialTextBoxFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> textBoxExpression, Expression<Func<TModel, TValue>> dropDownListExpression, object htmlAttributes = null)
{
string Id = htmlHelper.IdFor(textBoxExpression).ToString();
string DisplayName = htmlHelper.DisplayNameFor(textBoxExpression).ToString();
// this is coming out as blank
string DDId = htmlHelper.IdFor(dropDownListExpression).ToString();
// this is causing the error message displayed
string DDDisplayName = htmlHelper.DisplayNameFor(dropDownListExpression).ToString();
}
Here is the razor code I am trying to use to call the helper:
#Html.MaterialTextBoxFor(m => m.Height, m => m.HeightUnit)
Does anyone know how to make this work?
I eventually did find a solution. The key was to not use a second expression for the data source. Instead to create the source as an item in the model but pass it directly to the helper.
public static MvcHtmlString CustomDropDownListFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression, List<SelectListItem> DataSource, object htmlAttributes = null)
Inside the model you need to have two fields, one to store the selected value and one for holding the source data:
public string MyField{ get; set; }
public List<SelectListItem> MyFieldSource { get; set; }
You then call the helper as follows:
#Html.CustomDropDownListFor(m => m.MyField, Model.MyFieldSource)
I populate the "source" fields in the model constructor.
Related
.NET MVC HtmlHelper extension
I have this custom HtmlHelper extension, it gets a lambda expression that must be a child collection of the model, (for example model.childs), and a string that contains the name of a property of the collection items (for example "child_name")
And it creates a <span> whose content is the concatenation of the value of that property on every item of the collection. (for example "-- Mary -- Paul -- Rick")
public static MvcHtmlString SpanCollectionFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable<TValue>>> expression, string nomPropToUse, IDictionary<string, object> htmlAttributes)
{
var collection = expression.Compile()(html.ViewData.Model).ToList();
string text = "";
if (collection != null) {
foreach (var ent in collection) {
var val = ent.GetType().GetProperty(nomPropToUse).GetValue(ent, null).ToString();
text += " -- " + val;
}
}
//creates span using tagbuilder
var span = new TagBuilder("span");
span.SetInnerText(text);
span.MergeAttributes(new RouteValueDictionary(htmlAttributes));
return MvcHtmlString.Create(span.ToString());
}
That I can use in a Razor view like:
Html.SpanCollectionFor(modelItem => item.childs, "child_name")
So this creates a <span> whose content is the concatenation of the name of every child in the collection.
This is working, but I have 2 problems:
First problem: This works for simple cases, where the nomPropToUse is a property of the collection items. but wont work if I try to pass a property of a granchild. so for example, imagine that every child contains a child Type, with a property type_name.
I would use it with:
Html.SpanCollectionFor(modelItem => item.childs, "type.type_name")
but this does not work.
Second problem: I would like to have also an overload that receives the second parameter as a lambda expression too, instead of a string.
something like
public static MvcHtmlString SpanCollectionFor<TModel, TValue, KValue>(this HtmlHelper<TModel> html,
Expression<Func<TModel, IEnumerable<TValue>>> expression,
Expression<Func<TModel, KValue>> expression2,
IDictionary<string, object> htmlAttributes)
{
Any help?
I am trying to extend the HtmlHelper so it can translate field names from the Linq entities using a simple resource file.
The problem is that i can't get the extended method signature right. Here the code:
public static class HtmlHelperExtension
{
public static MvcHtmlString HeaderFromResource<TModel,
TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
...
return (MvcHtmlString)html.Raw(something)
}
}
When i try to use it inside the view, like this:
#model IEnumerable<WebApp.Models.TransferInConfig>
...
<th>
#Html.HeaderFromResource(model => model.RemotePath)
</th>
i get the following error:
'IEnumerable' does not contain a definition for
'RemotePath' and no extension method 'RemotePath'
Well to start your model is an IEnumerable, it indeed does not have RemotePath definition, but the items in it does (probably). So you will have to loop through your model list first and use each items. Something like this
foreach (var item in model){
#Html.HeaderFromResource(item => item.RemotePath)
}
public static MvcHtmlString HeaderFromResource<TModel, TValue>( this HtmlHelper<IEnumerable<TModel>> html, Expression<Func<TModel, TValue>> expression)
{
var headerName = expression.Body.ToString() //return model.Connection.RemotePath
.Split('.')
.Last();
if (Properties.Resources.ResourceManager.GetString(headerName) != null)
headerName = Properties.Resources.ResourceManager.GetString(headerName);
return MvcHtmlString.Create(headerName);
The only thing i needed to do was to look up on the manual for the signature of DisplayForName, but i am not use it anymore. Should be a better way to get the name of the field from the expression, anyway this one works for me.
If I write a custom DisplayFor helper, for example:
public static HtmlString MyDisplayFieldFor<TModel, TValue>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression, object additionalViewData = null)
{
//...
}
How do I determine the type of the field being passed into it such that different types can have custom display logic?
For example, I can all this method with three types:
#Html.MyDisplayFieldFor(e=>e.Name) //string
#Html.MyDisplayFieldFor(e=>e.DepartmentSelectList) //SelectList
#Html.MyDisplayFieldFor(e=>e.CupsOfTeaPerDay) // int
What's the best way to access this type information inside the Helper method?
Get the value type from the expression
public static HtmlString MyDisplayFieldFor<TModel, TValue>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression, object additionalViewData = null) {
var valueType = typeof(TValue);
//...other code
}
You don't need to check the type of the passed value. ASP.NET MVC lets you create display templates that are automatically selected based on the type of the field.
You could for example create this template for the SelectList in your example and place it in the Views/Shared/DisplayTemplates/SelectList.cshtml project folder.
#model SelectList
<!-- Replace this with the display code you prefer -->
<div class="select-list">#Model.ToString()</div>
This lets you keep the HTML code within the views.
I would like to create a HtmlHelper that can be used on IEnumerable properties.
The aim is to use it like this:
#Html.DisplayForEnumerable(m => m.EnumerableItemsProperty, "ViewTemplateName");
If possible I would like to use the m => m.Items lambda syntax (as opposed to passing through Model.Items).
This is my best effort so far. But I'm not sure how to get the items variable from the expression parameter.
I suspect I may have to use something like IEnumerable<TValue> as the return type of the expression, but I'm quite new to generics and I've no idea how to implement this.
public static MvcHtmlString DisplayForEnumerable<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, IEnumerable>> expression, string templateName, object additonalViewData = null)
{
var sb = new StringBuilder();
// how to get items variable?
foreach (var item in items)
{
var item1 = item;
sb.Append(html.DisplayFor(m => item1, templateName, additonalViewData));
}
return MvcHtmlString.Create(sb.ToString());
}
Update
To clarify - I am taking this approach because I would like to be able so specify differnt templates for the same model. And the normal DisplayFor() enumeration does not occur if you specify a particular template.
I know I could just enumerate through manually, but I'd rather use this method unless someone more knowledgable advises otherwise.
You helper will need to be
public static MvcHtmlString DisplayForEnumerable<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression, string templateName, object additionalViewData = null)
{
ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, helper.ViewData);
IEnumerable collection = metaData.Model as IEnumerable;
if (collection == null)
{
return helper.DisplayFor(expression, templateName, additionalViewData );
}
StringBuilder html = new StringBuilder();
foreach (var item in collection)
{
html.Append(helper.DisplayFor(m => item, templateName, additionalViewData).ToString());
}
return MvcHtmlString.Create(html.ToString());
}
Note the code allows you to pass either a single T or IEnumerable<T> (although the method name now does not really make sense). If you wanted to limit it to only IEnumerable<T> you could throw an InvalidCastException if collection == null
Note that this approach will not work if you wanted to generate form controls for a collection (for example a EditorForEnumerable() method) because the required collection indexers will not be added to the generate name attributes. A better approach is to use the built-in DisplayFor() and EditorFor() methods which will generate the correct html for both T and IEnemerable<T>
Assuming you have a Person.cs class, create a partial view in /Views/Shared/DisplayTemplates/Person.cshtml (note the name of the file must match the name of the class) and in the view simply use
#Html.DisplayFor(m => m.yourCollectionProperty)
You can also create specific display and editor templates for each controller, for example /Views/yourControllerName/DisplayTemplates/Person.cshtml. This allows you to use one template in /Persons/Index and another template in /Organisation/Details/1 which might display a list of Person associated with an Organisation
And finally, if you do need 2 different templates for Person in the same controller, you can use view models, for example class PersonVM and class AssociatedPersonVM and create an EditorTemplate for each
I have an ASP.NET MVC app. I am rendering the view with the help of Html Helpers. For example, my .cshtml file looks like the following:
<div>
#Html.Label(model => model.Price)
#Html.TextPrice(model => model.Price)
</div>
The helpers are defined like the following in Extensions.cs:
public static MvcHtmlString Label<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string classes = "control-label")
{
var attr = new Dictionary<string, object>();
attr .Add("class", classes);
return System.Web.Mvc.Html.LabelExtensions.LabelFor(html, expression, attr);
}
public static MvcHtmlString TextPrice<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, byte decimalPlaces = 2, string classes = "form-control")
{
return Text(html, expression, classes + " decimal-" + decimalPlaces.ToString());
}
Eventually, I want to translate the labels into other languages. For now, I need to do an intermediary translation. My question is, when I print a label, how do I grab the text, alter it, then use the new text for the label? I do NOT want to add a bunch of Display attributes on my model at this time. I just need to do a quick-and-dirty search and replace in my Label extension method. However, I'm just not sure how to grab the text and update it.
Thank you!
You can get the property name from the expression yourself and then pass the translation, as an argument for the labelText parameter, to LabelExtensions.LabelFor():
public static MvcHtmlString Label<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string classes = "control-label")
{
var attr = new Dictionary<string, object>();
attr .Add("class", classes);
string propertyName = ((MemberExpression)expression.Body).Member.Name;
string labelText = translate(propertyName);
return System.Web.Mvc.Html.LabelExtensions.LabelFor(html, expression, labelText, attr);
}
You may use the DisplayName attribute at the Model. E.g.:
[Display(Name="Character_FirstName", ResourceType=typeof(ClassLib1.Resources))]
public string FirstName {get; set;}
See for example: http://haacked.com/archive/2011/07/14/model-metadata-and-validation-localization-using-conventions.aspx/ or https://stackoverflow.com/a/3877154/2298807 (related to Localization of DisplayNameAttribute)