Update Label Text in ASP.NET MVC Html Helper - c#

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)

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.

Adding HtmlHelper to translate field name into column header using resource file

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.

Show list of all Enums of type<T> except the one passed via lambda

In my MVC application, I've created a helper which should take an enum from the model and then display radio buttons for all of the other available enums of that type.
For example, you have the enum Satus which has Active, Inactive, Closed and the model for the page has Status = Status.Active so you want to display radio buttons for Inactive and Closed.
Going forward with this example, the MVC view calls helper RadioButtonForEnum:
#Html.RadioButtonForEnum(model => model.Status)
RadioButtonForEnum then gets the list of all Enums of that type and prints them out as radio buttons; however, I'm not sure how to get to the enum that was passed to exclude it from names
public static MvcHtmlString RadioButtonForEnum<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var names = Enum.GetNames(metaData.ModelType);
var sb = new StringBuilder();
foreach (var name in names)
{
var id = string.Format(
"{0}_{1}_{2}",
htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix,
metaData.PropertyName,
name
);
var radio = htmlHelper.RadioButtonFor(expression, name, new { id = id }).ToHtmlString();
sb.AppendFormat("<label for=\"{1}\">{0}{2}</label>", radio, id, HttpUtility.HtmlEncode(StringHelpers.PascalCaseToSpaces(name)));
}
return MvcHtmlString.Create(sb.ToString());
}
Something like
var modelValue = expression.Compile()(htmlHelper.ViewData.Model);
...
foreach (var name in names.Where(s => s != modelValue.ToString())
...
You need the model instance to get the current enum value to avoid, you could get to it inside the HtmlHelper by using HtmlHelper.ViewData.Model as above.
I don't think you have the model object, as-is. You could add it as another parameter:
#Html.RadioButtonForEnum(Model, model => model.Status)
public static MvcHtmlString RadioButtonForEnum<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, TModel model, Expression<Func<TModel, TProperty>> expression)
{
string currentStatusName = expression.Compile()(model).ToString();
...
Quick and dirty way:
foreach (var name in names)
{
if (name == metaData.Model.ToString())
continue;
...
}
Completely untested here as I don't have my own computer, but what about changing your signature to:
public static MvcHtmlString RadioButtonForEnum<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Enum Status)
This will allow you to pass in the Status enum and interrogate its value:
public static MvcHtmlString RadioButtonForEnum<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Enum Status){
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var names = Enum.GetNames(metaData.ModelType);
var sb = new StringBuilder();
foreach (var name in names)
{
if (!name.Equals(Status.ToString()){
var id = string.Format(
"{0}_{1}_{2}",
htmlHelper.ViewData.TemplateInfo.HtmlFieldPrefix,
metaData.PropertyName,
name
);
var radio = htmlHelper.RadioButtonFor(expression, name, new { id = id }).ToHtmlString();
sb.AppendFormat("<label for=\"{1}\">{0}{2}</label>", radio, id, HttpUtility.HtmlEncode(StringHelpers.PascalCaseToSpaces(name)));
}
}
And you would call it from your view like this:
#Html.RadioButtonForEnum(model => model.Status, Model.Status)

How do I get the value from an anonymous expression?

For sake of simplicity, imagine the following code:
I want to create a Foo:
public class Foo
{
public string Bar { get; set; }
}
And pass it to a special Html Helper method:
Html.SomeFunction(f => f.Bar);
Which is defined as:
public string SomeFunction<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
I want to get the value of Bar inside of this function, but have absolutely no idea how to get it.
Simply compile the expression and get the value.
Func<TModel, TValue> method = expression.Compile();
TValue value = method(html.ViewData.Model);
// might be a slightly different property, but you can get the ViewModel
// from the HtmlHelper object.
You will need to call Compile() on the expression to get the Func and then execute that.
public string SomeFunction<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
TValue valueOfBar = expression.Compile()(html.Model); // Assumes Model is accessible from html.
// Do stuff
}
Side note: If there isn't any need for the dynamic expressions or expression analysis you might as well pass the Func directly in instead.
For those that are using expression without MVT Model, one would obtain name and value of property in a following way.
public static string Meth<T>(Expression<Func<T>> expression)
{
var name = ((MemberExpression)expression.Body).Member.Name;
var value = expression.Compile()();
return string.Format("{0} - {1}", name, value);
}
use:
Meth(() => YourObject.Property);
in Microsoft.AspNetCore.Mvc.Rendering there is helpfull valuefor method;
public static string ValueFor<TModel, TResult>(this IHtmlHelper htmlHelper, Expression<Func<TModel, TResult>> expression);
public string SomeFunction<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression){
var valueOfExpression = html.ValueFor(expression);
//do your stuff
}
Using Compile() will use the Roslyn compiler-framework and will emit MSIL-code that will be dynamically loaded into your application. This executable code takes up memory, and in contrast to "normal" memory it is not subject to garbage collection nor can you free it yourself. If you do this too frequently (like regularly during SQL generation) you will run out of memory eventually. I ran into this issue and open-sourced my solutions as an open-source library:
https://www.nuget.org/packages/MiaPlaza.ExpressionUtils

Categories