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
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 need to check an incoming Func<TIn, TOut> for a specific attribute.
So far I have:
var methodInfo = cachedMethod.GetMethodInfo();
var isCachable = methodInfo.CustomAttributes.
Any(x => x.AttributeType == typeof(CachedAttribute));
However I'm finding that the CustomAttributes property is empty.
What's the proper way to find the attributes applied to an incoming func? In my case the func is a static method in another class.
Update
Here's a small sample showing how I'm trying to use this:
The HTML helper:
public static IHtmlString CachedPartial<TModel>(this HtmlHelper helper,
string partialName,
TModel model,
Func<string, string> cachedMethod)
{
var methodInfo = cachedMethod.GetMethodInfo();
var isCachable = methodInfo.IsDefined(typeof (CachedAttribute));
if (!isCachable)
{
throw new Exception("...");
}
return new MvcHtmlString(cachedMethod("foo"));
}
The cached method
[Cached]
public static string GenrateSiteMapGraphHtml(string siteCode)
{
return "Foo";
}
This is being called from a Razor view:
#Html.CachedPartial("Foo",
Model,
HtmlHelperExtensions.GenrateSiteMapGraphHtml)
I may not quite understand what is your problem, but I'll suggest two things:
You need to be sure that CustomAttributes isn't empty, use
if (methodInfo.CustomAttributes!=NULL) //or whatever is analogue of NULL for that type of property
{/some action/}
You can't figure out why is this property empty. The first thing to check is visibility.
In my case the func is a static method in another class
So go and check out if that class is visible for the area of code where you are trying to invoke that static method
I have some questions about html helpers in ASP.NET MVC Framework. So...
Can I override a inbuilt helper like #Html.HiddenFor?
Should I override, or create a CustomHiddenFor one?
While creating a Custom one, how can I change the value from ModelMetaData and then call the inbuilt helper? Eg:
public static MvcHtmlString HiddenSecuredFor<TModel, TProperty>
(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object htmlAttributes,bool secured)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var name = ExpressionHelper.GetExpressionText(expression);
if (!secured)
{
return htmlHelper.HiddenFor(expression,htmlAttributes);
}
// Here I want to change the value
// from the TModel lets say the property is x => x.Name
// And I want to make the Name = "Mr. " + Name
// Call the inbuild helper with the expression value changes.
return htmlHelper.HiddenFor(expression, htmlAttributes);
}
Yes - How can I override the #Html.LabelFor template?
It depends - If you are adding functionality you always need and you want other developers to use without having to know about it, overriding is a good option. Otherwise, creating a custom one that calls the original is a good approach.