MVC Html Helpers - How much control over inbuilt ones? - c#

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.

Related

.NET MVC HtmlHelper extension problems

.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?

Multiple Lambda Expressions as MVC Html Helper Parameters?

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.

How do I tell the type of a field passed into a custom Html.DisplayFor helper?

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.

Can I create a HtmlHelper specifically for IEnumerable properties?

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

Get Attributes of a Func

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

Categories