I think my understanding of lambda notation is lacking because I do not understand why it is even needed in Razor. For example:
instead of this:
#Html.DisplayFor(modelItem => item.FirstName)
why can't we just have this:
#Html.DisplayFor(item.FirstName)
What is the purpose of the lambda syntax and why do we need to add in all of the extra work of typing out the lambda notation?
Can someone help me to understand why this is needed and what benefit it offers?
The purpose of lambda expression on Razor view is returning value given by model from anonymous function (i.e. nameless function). Take a look on your first example:
#Html.DisplayFor(modelItem => item.FirstName)
would be translated to something like:
#Html.DisplayFor(String Function(Model modelItem)
{
return item.FirstName;
})
Here modelItem given as function parameter declared as Model, and return statement as function body to return property value depending on get/set operation.
If we look further to the DisplayFor helper declaration:
public static MvcHtmlString DisplayFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression,
string templateName
)
As stated by #SLaks before, expression tree can be parsed as a parameter to generate proper HTML tag into view based from type of your model defined by #model directive instead of executing it.
The second argument, Expression<Func<TModel, TValue>> is a declaration that ensures any given function parameter will always have same type as your model. This way eliminates reflection code that using GetProperty and GetValue required by HTML helper to retrieve given property value at appropriate time with strongly typed model as a benefit.
Here is an example of reflection code inside HTML helper declaration which can be eliminated by lambda syntax:
var model = html.ViewData.Model;
var value = String.Empty;
if (model != null)
{
var type = typeof(TModel);
var propertyInfo = type.GetProperty(templateName);
var propertyValue = propertyInfo.GetValue(model);
value = propertyValue.ToString();
}
Afterwards, let's examine the second example:
#Html.DisplayFor(item.FirstName)
Here DisplayFor will use Object as parameter type, consider we can't determine exactly what type should be pre-assigned thus it sets to System.Object. Since the method doesn't provide model definition type as TModel in generic code, the method probably requires reflection when dealing with property value.
Any improvements and suggestions welcome.
References:
https://msdn.microsoft.com/en-us/library/hh833706%28v=vs.118%29.aspx
http://odetocode.com/blogs/scott/archive/2012/11/26/why-all-the-lambdas.aspx
I want to understand the lambda expression in #Html.DisplayFor(modelItem => item.FirstName)
The HtmlHelper methods take an expression tree as a parameter.
This lets them see the actual property that you pass so that they can observe its attributes.
Related
I want to create a custom HTML Helper where I can pass a LINQ expression as a parameter, like this:
#Html.GetBackgroundColor(model => model.RiskAssessment)
I want to use it to display some custom css in an MVC view, depending on what the RiskAssessment property is.
So I created a helper method like this:
public static string GetBackgroundColor<T, TResult>(this HtmlHelper<T> htmlHelper, Expression<Func<T, TResult>> expression)
{
...
}
However, that won't compile, the error is IEnumerable does not contain a definition for 'RiskAssessment'
So I changed the method to
public static string GetBackgroundColor<T, TResult>(this HtmlHelper<IEnumerable<T>> htmlHelper, Expression<Func<T, TResult>> expression)
{
...
}
which compiles, so now I presumably have all the objects in the collection but I have no idea how to get the object I want as I can't use use the expression on the IEnumerable, there is no Where() method available. I would have thought I could do something like this:
IEnumerable<T> collection = htmlHelper.ViewData.Model;
T obj = collection.Where(expression)
but I just don't know what I am doing wrong here.
Figured it out, simple mistake. The table header row is set up with #Html.DisplayNameFor(model => modelType), and I was trying to call my custom HTML helper with these parameters. I should have been calling the method on each table row, using #Html.GetBackgroundColor(modelItem => item.RiskAssessment), and this works because I can use htmlHelper.ValueFor(expression) within the method to get the property value.
That said, I have no idea how the header row is generated as Html.DisplayNameFor uses the same method signature as my custom method but Intellisense reports that one of the Expression types is unknown. But that is not an issue for me.
Thanks.
I am converting some existing HTML to work with ASP.NET MVC, and have some forms containing input fields that have additional attributes (that, for now, I need to preserve) that contain a namespace prefix:
<input type="text" foo:required="true" foo:message="Please enter a username" />
I would like to use the TextBoxFor helper method overload that allows htmlAttributes to be specified using collection initializer syntax:
#Html.TextBoxFor(
model => model.Username,
new { foo:required="true", foo:message="Please enter a username" })
However, this syntax is not valid, due to the colon in foo:required (etc).
Instead, I am having to use this more verbose dictionary initializer syntax:
#Html.TextBoxFor(
model => model.Username,
new Dictionary<string, object>
{
{ "foo:required", "true" },
{ "foo:message", "Please enter a username" }
})
Is it possible to use a variation on the first syntax, somehow escaping (for want of a better word) the colons?
Or
Is this a scenario where the TextBoxFor helper method does not really help, and would it be simpler to just keep the existing raw HTML, and add value="#Model.UserName" to the input element?
The first syntax is using an anonymous object, for which the same rules regarding how to create identifiers in C# apply:
You can use any unicode character of the classes Lu, Ll, Lt, Lm, Lo, or Nl
You can scape C# keywords using "#" as in new { #class = "foo" };
Since the colon belongs to the Po unicode class it cannot be used in an identifier. (In C# you can use the static method char.GetUnicodeCategory in order to check the class of any character)
Additionally, and only in MVC, when using an anonymous object for the html attributes in the helper, any attribute name with underscores will be replaced by hyphens. This is due to the method HtmlHelper.AnonymousObjectToHtmlAttributes
Back to your case and regarding your options, if those are not too widely used (like in a couple of views) I would consider staying with the Dictionary syntax of the TextBoxFor helper. You will still get the automatic generation of the id\name properties in sync with the model binding, and you will get any other attributes from the model metadata like the unobtrusive validation attributes. (although looking at the attributes you want to preserve, it seems you won´t need the unobtrusive validation ones :) )
However if the id\name will be as simple as the name of the property and you don´t need any other html attributes that would be generated automatically using the helper, then going for the raw HTML makes sense. (as the dictionary syntax is quite ugly)
In case you use it widely across the application, then in my opinion the most sensible approach may be creating your own helper, like #Html.LegacyTextBoxFor(...). That helper will render those legacy attributes you want to kepp, and you can incorporate additionaly logic similar to the standard helpers for id\name attribute creation.
Something like this:
public class FooAttributes
{
public bool Required { get; set; }
public string Message { get; set; }
}
public static class FooHelper
{
public static MvcHtmlString LegacyTextboxFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, FooAttributes fooAttributes)
{
var fieldName = ExpressionHelper.GetExpressionText(expression);
var fullBindingName = html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(fieldName);
var fieldId = TagBuilder.CreateSanitizedId(fullBindingName);
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var value = metadata.Model;
TagBuilder tag = new TagBuilder("input");
tag.Attributes.Add("name", fullBindingName);
tag.Attributes.Add("id", fieldId);
tag.Attributes.Add("type", "text");
tag.Attributes.Add("value", value == null ? "" : value.ToString());
if (fooAttributes != null)
{
if (fooAttributes.Required) tag.Attributes.Add("foo:required", "true");
if (!String.IsNullOrEmpty(fooAttributes.Message)) tag.Attributes.Add("foo:message", fooAttributes.Message);
}
return new MvcHtmlString(tag.ToString(TagRenderMode.SelfClosing));
}
}
That can be used as:
#Html.LegacyTextboxFor(model => model.UserName, new FooAttributes {Required=true, Message="Please enter a value." })
And will generate this html:
<input foo:message="Please enter a value." foo:required="true" id="UserName" name="UserName" type="text" value="">
And once you have your own helper, you could add additionaly logic, for example logic that will generate those attributes from the model metadata and its data annotation attributes...
I have extended my answer more than intended, but I hope it helps!
I'm dealing with this problem.
I'm using EditorFor for a model which is an abstract class.
#model Contoso.Core.Base.Question
#Html.HiddenFor(model => Model.Id)
#Html.Hidden("ModelType", Model.GetType().AssemblyQualifiedName)
#Html.EditorFor(model => Model, Contoso.Core.QuestionRepositoryManager.GetQuestionView(Model))
As you can above, later invoke a concrete editor for the model and what does MVC is only render the first one (I mean, print hidden input fields "Id and ModelType", but not for the inputs in the concrete EditorFor).
How can I do to print these two EditorFor? I was reading about using PartialView, but I don't like this idea because I don't know how to bind these properties in the POST Method.
UPDATE:
I'm not sure if do I have to modify the prefixHtml to fix this?
ViewData.TemplateInfo.HtmlFieldPrefix = ...
It renders the second template because if I change to Editor instead of EditorFor
#Html.Editor("abc", Contoso.Core.QuestionRepositoryManager.GetQuestionView(Model))
I believe the problem here is that the compiler is choosing the wrong overload. If you look at the overloads of EditorFor, you see there are two that take two parameters:
public static MvcHtmlString EditorFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression,
Object additionalViewData
)
and
public static MvcHtmlString EditorFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression,
string templateName
)
You could try this:
#Html.EditorFor(model => Model,
Contoso.Core.QuestionRepositoryManager.GetQuestionView(Model) as string)
or you could do this:
#Html.EditorFor(model => Model,
Contoso.Core.QuestionRepositoryManager.GetQuestionView(Model), null)
There are also two constructors that take 3 arguments, but either of them, the second argument is always the template name, so by passing null it really doesn't matter which is chosen.
The problem may also be that GetQuestionView() returns an object instead of a string, and that is why it's choosing the wrong constructor, making sure the return type of GetQuestionView() is string may also fix it. Although I'm not sure why it works with Editor, since the same problem would exist there as the constructors are pretty analogous.
I have this example code within Razor:
#Html.TextBoxFor(x => ((VisitGozo.Modules.Data.Events.EventSpecificFieldsData)x.Event.DataObject.SpecificFields).LinkedTourismProduct)
As you can see, the expression includes casting. The TextBoxFor makes use of the ExpressionHelper.GetExpressionText() method which converts an expression into text for Model Binding.
In the above example, due to casting, the returned field name is just LinkedTourismProduct and not Event.DataObject.SpecificFields.LinkedTourismProduct. If there wasn't any casting, this would have returned the full name correctly.
Hence during model binding, it does not bind to the correct property because the property of the Model is Event.DataObject.SpecificFields.LinkedTourismProduct and not LinkedTourismProduct only.
Any idea why this is happening and is there any other solution/workaround?
That's normal behavior. Only simple expressions (property access and indexer access) are supported by the strongly typed helpers. Casting is not.
The correct way to do this is to have a property on your view model of the correct type:
public EventSpecificFieldsData EventSpecificFields { get; set; }
and then bind your textbox to it without any casts:
#Html.TextBoxFor(x => x.Event.DataObject.EventSpecificFields.LinkedTourismProduct)
I trying to use a Razor Helper that does some if statements for me. I'm passing it a list with certain rules and based on that rule a label and input text field are created.
The problem I have is I can't get it to work with a Lambda expression as parameter. It won't recognize the TModel part.
The helper method is as follows:
#helper CreateCheckbox(Expression<Func<TModel, bool>> expression, object htmlAttributes, List<Rule> ruleList)
{
}
The error I get is: The type of namespace 'TModel' can not be found.
You have to pick a type for TModel. Because your view doesn't have any generic type parameters, there is no way for it to figure out what type it should substitute in for TModel. You have to give it an actual type to work with.
If that's not an option, you might just make an extension method for the HtmlHelper class, and make it a generic method. See this question for an example of how to do that.