Is there a way to modify the generated name attribute for TextboxFor, HiddenFor etc through ModelBinder? - asp.net-mvc

I've looked into this question and found questions such as:
SO-Link1
SO-Link2
So the problem seems rather common - but i really don't like the solution.
Question:
I've started digging into sources but couldn't find a convenient solution yet. Is there a built in solution to this by now using attributes?
Having to use dynamic parameters in place instead of using attributes on the model is very inconvenient.
Guess i'll see what i can achieve by modifying the ModelBinder in the meantime - there has to be some way i guess.
Update:
Why do i want to do this?
I want to reduce network traffic because properties in code may be long + nested. Imagine 200 Checkboxes x 40 bytes generated names.
I can already make my modelbinder work with aliases - however in order to fully automate it, i need the TextBoxFor etc methods to use alias names instead of the actual property names.

Personally I don't think you should be totally concerned with the size of the the name attribute's values. As long as you're using compression through something like IIS then you aren't going to be saving that much.
You can however, achieve what you're after by creating custom HTML helpers which will create the HTML markup you desire.
Example Model
public class UserModel
{
public string FullName { get; set; }
}
Helper
public static MvcHtmlString CustomTextBoxFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string name)
{
var fieldName = ExpressionHelper.GetExpressionText(expression);
//
// Pass in alias or call method to get alias here
//
var fullBindingName = String.IsNullOrWhiteSpace(name) ? html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(fieldName) : name;
var fieldId = TagBuilder.CreateSanitizedId(fullBindingName);
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var value = metadata.Model;
var 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());
var validationAttributes = html.GetUnobtrusiveValidationAttributes(fullBindingName, metadata);
foreach (var key in validationAttributes.Keys)
{
tag.Attributes.Add(key, validationAttributes[key].ToString());
}
return new MvcHtmlString(tag.ToString(TagRenderMode.SelfClosing));
}
Call the Method in your View
#Html.TextBoxFor(x => x.FullName)
#Html.CustomTextBoxFor(x => x.FullName, "FName")
Output
<input id="FullName" type="text" value="Heymega" name="FullName">
<input id="FName" type="text" value="Heymega" name="FName">

Related

Can I use a LambdaExpression type for MVC's HTML helpers?

I am trying to use reflection to auto-generate a view. Html.DisplayFor and some of the other helpers take Expression<Func<,>> which derives from LambdaExpression. Seemed like I'd be able to manually generate my own lambda and then pass that in, but it's throwing this error:
The type arguments for method 'DisplayExtensions.DisplayFor<TModel, TValue>(HtmlHelper<TModel>, Expression<Func<TModel, TValue>>)' cannot be inferred from the usage. Try specifying the type arguments explicitly.`
Here's my markup:
<tr>
#foreach (var pi in Model.GetType().GetProperties())
{
<td>
#Html.DisplayFor(ExpressionHelpers.GetPropertyGetterLambda(pi))
</td>
}
</tr>
I am pretty sure what's happening is that .DisplayFor requires generic type arguments to infer the types for Func<TModel, TValue>, but I am using LambdaExpression which is hiding the types.
It seems like the only way to do what I want is to build/compile an expression that actually calls .DisplayFor using type-safe arguments, but that seems overly complicated.
Is there another way to achieve my goal or would I be better off just outputting the results directly to the HTML rather than calling the helpers?
Edit: Per request, here's the code for GetPropertyGetterLambda:
public static LambdaExpression GetPropertyGetterLambda(PropertyInfo pi, BindingTypeSafety TypeSafety)
{
if (pi.CanRead)
{
ParameterExpression entityParameter = Expression.Parameter(TypeSafety.HasFlag(BindingTypeSafety.TypeSafeEntity) ?
pi.ReflectedType : typeof(object));
LambdaExpression lambda = Expression.Lambda(GetPropertyReadExpression(entityParameter, pi, TypeSafety), entityParameter);
return lambda;
}
else
{
return null;
}
}
There is another solution for this that I recommend first. Change your for loop to an DisplayForModel call:
#Html.DisplayForModel()
That will render out all the properties, using DisplayFor internally. You can modify this by modifying the Object.cshtml in the DisplayTemplates folder (either in the Views folder your are working in, or in the Shared folder to apply globally).
If that is insufficient, or you really really want to use LambdaExpression, here is an alternative. It will require you adding a DisplayFor<TModel>(LambdaExpression expression) extension method. It would be something like (note that I haven't actually tested this, but this is close to what is needed):
public static IHtmlString DisplayFor<TModel>(this HtmlHelper<TModel> helper, LambdaExpression expression)
{
var wrapperClass = typeof(DisplayExtensions);
///find matching DisplayFor<TModel, TProperty> method
var matchingMethod = wrapperClass.GetMethods()
.Single( c => c.IsStatic
&& c.Name == "DisplayFor"
&& c.GetGenericArguments( ).Count() == 2
&& c.GetParameters( ).Count() == 2 //overloads don't have same # of parameters. This should be sufficient.
).MakeGenericMethod( typeof(TModel), expression.ReturnType ); //Make generic type from the TModel and the Return Type
//invoke the method. The result is a IHtmlString already, so just cast.
return (IHtmlString) matchingMethod.Invoke( null, new Object[] { helper, expression } );
}

How can I persist a check box list in MVC

I'm trying to build an html helper for creating a list of checkboxes, which will have the check state persisted using sessions. It works for the most part, remembering check box states when you check and uncheck various boxes and click submit. However, if you have boxes checked and submitted, and you go back and clear the checkboxes and resubmit (when they are ALL cleared) - it seems to want to remember the last selections. Here is what I've written...
[HomeController]
public ActionResult Index()
{
TestViewModel tvm = new TestViewModel();
return View(tvm);
}
[HttpPost]
public ActionResult Index(TestViewModel viewModel)
{
viewModel.SessionCommit();
return View(viewModel);
}
[Index View]
#model TestApp.Models.TestViewModel
#{
ViewBag.Title = "Index";
}
#using (Html.BeginForm())
{
<p>Checkboxes:</p>
#Html.CheckedListFor(x => x.SelectedItems, Model.CheckItems, Model.SelectedItems)
<input type="submit" name="Submit form" />
}
[TestViewModel]
// Simulate the checklist data source
public Dictionary<int, string> CheckItems
{
get
{
return new Dictionary<int, string>()
{
{1, "Item 1"},
{2, "Item 2"},
{3, "Item 3"},
{4, "Item 4"}
};
}
}
// Holds the checked list selections
public int[] SelectedItems { get; set; }
// Contructor
public TestViewModel()
{
SelectedItems = GetSessionIntArray("seld", new int[0] );
}
// Save selections to session
public void SessionCommit()
{
System.Web.HttpContext.Current.Session["seld"] = SelectedItems;
}
// Helper to get an int array from session
int[] GetSessionIntArray(string sessionVar, int[] defaultValue)
{
if (System.Web.HttpContext.Current.Session == null || System.Web.HttpContext.Current.Session[sessionVar] == null)
return defaultValue;
return (int[])System.Web.HttpContext.Current.Session[sessionVar];
}
[The HTML helper]
public static MvcHtmlString CheckedList(this HtmlHelper htmlHelper, string PropertyName, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
StringBuilder result = new StringBuilder();
foreach(var item in ListItems)
{
result.Append(#"<label>");
var builder = new TagBuilder("input");
builder.Attributes["type"] = "checkbox";
builder.Attributes["name"] = PropertyName;
builder.Attributes["id"] = PropertyName;
builder.Attributes["value"] = item.Key.ToString();
builder.Attributes["data-val"] = item.Key.ToString();
if (SelectedItemArray.Contains(item.Key))
builder.Attributes["checked"] = "checked";
result.Append(builder.ToString(TagRenderMode.SelfClosing));
result.AppendLine(string.Format(" {0}</label>", item.Value));
}
return MvcHtmlString.Create(result.ToString());
}
public static MvcHtmlString CheckedListFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, Dictionary<int, string> ListItems, int[] SelectedItemArray)
{
var name = ExpressionHelper.GetExpressionText(expression);
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
return CheckedList(htmlHelper, name, ListItems, SelectedItemArray);
}
I've read this SO question and I think this may be to do with the model binder not knowing when there are no checkboxes checked, but even though I've gone through that and various other posts - I'm no further forward.
In one post, I saw that a hidden field is often used in combination with the checkbox to pass the 'false' state of the checkbox, but I couldn't get it working with multiple checkboxes posting back to a single property.
Can anyone shed light on this?
EDITED : to include the demonstration project I've highlighted in this post. Hopefully this will help someone to help me!
Your main issue, and the reason why the previous selections are being 'remembered' when you un-check all items is that you have a constructor in your model that calls GetSessionIntArray() which gets the values you stored last time you submitted the form. The DefaultModelBinder works by first initializing your model (including calling its default constructor) and then setting the values of its properties based the form values. In the following scenario
Step 1: Navigate to the Index() method
Assuming its the first call and no items have been added to Session, then the value of SelectedItems returned by GetSessionIntArray() is int[0], which does not match any values in CheckItems, so no checkboxes are checked.
Step 2: Check the first 2 checkboxes and submit.
The DefaultModelBinder initializes a new instance of TestViewModel and calls the constructor. The value of SelectedItems is again int[0] (nothing has been added to Session yet). The form values are then read and the value of SelectedItems is now int[1, 2] (the values of the checked checkboxes). The code inside the method is called and int[1, 2] is added to Session before returning the view.
Step 3: Un-check all checkboxes and submit again.
Your model is again initialized, but this time the constructor reads the values from Session and the value of SelectedItems is int[1,2]. The DefaultModelBinder reads the form values for SelectedItems, but there are none (un-checked checkboxes do not submit a value) so there is nothing to set and the value of SelectedItems remains int[1,2]. You then return the view and your helper checks the first 2 checkboxes based on the value of SelectedItems
You could solve this by removing the constructor from the model and modifying the code in the extension method to test for null
if (SelectedItemArray != null && SelectedItemArray.Contains(item.Key))
{
....
However there are other issues with you implementation, including
Your generating duplicate id attributes for each checkbox (your use of builder.Attributes["id"] = PropertyName;) which is invalid html.
builder.Attributes["data-val"] = item.Key.ToString(); makes no sense (it generates data-val="1", data-val="1" etc). Assuming you want attributes for unobtrusive client side validation, then the attributes would be data-val="true" data-val-required="The SelectedItems field is required.". But then you would need a associated placeholder for the error message (as generated by #Html.ValidationMessageFor() and the name attribute of each checkbox would need to be distinct (i.e. using indexers - name="[0].SelectedItems" etc).
Your using the value of the property for binding, but the correct approach (as all the built in extension method use) is to first get the value from ModelState, then from the ViewDataDictionary and finally if no values are found, then the actual model property.
You never use the value of var metadata = ModelMetadata..... although you should be (so that you can remove the last parameter (int[] SelectedItemArray) from the method, which is in effect just repeating the value of expression.
Side note: The use of a hidden field is not applicable in your case. The CheckboxFor() method generates the additional hidden input because the method binds to a bool property, and it ensures a value is always submitted.
My recommendation would be to use a package such as MvcCheckBoxList (I have not tried that one myself as I have my own extension method), at least until you spend some time studying the MVC source code to better understand how to create HtmlHelper extension methods (apologies if that sounds harsh).

Using an HtmlHelper inside a custom HtmlHelper

Is there a way to do something like the following:
public static IHtmlString TableFor(this HtmlHelper helper, IEnumerable<MaterialGroup> groups, Func<HtmlHelper, MaterialGroup, int, string> tableContentsFunc)
{
return MvcHtmlString.Create("#Html.TextBoxFor(x => this.Model.Something)");
}
Obviously this is a trivial example, but when ever I try something of the sort it renders the Helpers i.e. "#Html.TextBoxFor(x => this.Model.Something)" as text on the page instead of processing them as helpers.
Is there a way to achieve this?
public static MvcHtmlString TableFor(this HtmlHelper<IEnumerable<MaterialGroup>> helper, IEnumerable<MaterialGroup> groups, Func<HtmlHelper<MaterialGroup>, MaterialGroup, int, string> tableContentsFunc)
{
String html = "<table class='materials joists'>";
String endHtml = "</table>";
for (int i = 0; i < groups.Count(); ++i)
{
HtmlHelper<MaterialGroup> groupHelper = new HtmlHelper<MaterialGroup>(helper.ViewContext, helper.ViewDataContainer); // Crashes here with cannot convert IEnumerable<MaterialGroup> to MaterialGroup.
html += TbodyFor(groupHelper , groups.ElementAt(i), i);
html += tableContentsFunc(groupHelper , groups.ElementAt(i), i);
}
return MvcHtmlString.Create(html + endHtml);
}
public static string TbodyForJoists(this HtmlHelper<MaterialGroup> helper, MaterialGroup group, int index)
{
string html = string.Empty;
MvcHtmlString markTextbox = InputExtensions.TextBoxFor<MaterialGroup, String>(helper, x => group.Joists.ElementAt(i).Mark, new { Name = "MaterialGroups[" + index + "].Joists[" + i + "].Mark", Class = "auto-size first-column" });
html += martTextbox;
.
.
.
return html;
}
When I attempt the above I get issues with the HtmlHelper<> Types.
If I leave it with just HtmlHelper I get an error telling me to explicity state since it doesn't know what I doing with it. If I explicitly state is have conversion? issues I guess you could say.
How can I simply just use the TextBoxFor in this way?
Because that is very literally what you are telling it to do. The output of the helper itself is not processed by Razor. Whatever you return is what's going on the page.
However, you could always do:
MvcHtmlString textBox = Html.TextBoxFor(expression);
And, you'd have to feed the helper the expression to use. It perhaps would be easier in this situation to use Html.TextBox instead, but then you're going to have to do more work to try to figure out the right names and such for the fields.
It's going to be far easier and less convoluted to just use editor templates for this type of thing.

How to bind view model property with different name

Is there a way to make a reflection for a view model property as an element with different name and id values on the html side.
That is the main question of what I want to achieve. So the basic introduction for the question is like:
1- I have a view model (as an example) which created for a filter operation in view side.
public class FilterViewModel
{
public string FilterParameter { get; set; }
}
2- I have a controller action which is created for GETting form values(here it is filter)
public ActionResult Index(FilterViewModel filter)
{
return View();
}
3- I have a view that a user can filter on some data and sends parameters via querystring over form submit.
#using (Html.BeginForm("Index", "Demo", FormMethod.Get))
{
#Html.LabelFor(model => model.FilterParameter)
#Html.EditorFor(model => model.FilterParameter)
<input type="submit" value="Do Filter" />
}
4- And what I want to see in rendered view output is
<form action="/Demo" method="get">
<label for="fp">FilterParameter</label>
<input id="fp" name="fp" type="text" />
<input type="submit" value="Do Filter" />
</form>
5- And as a solution I want to modify my view model like this:
public class FilterViewModel
{
[BindParameter("fp")]
[BindParameter("filter")] // this one extra alias
[BindParameter("param")] //this one extra alias
public string FilterParameter { get; set; }
}
So the basic question is about BindAttribute but the usage of complex type properties. But also if there is a built in way of doing this is much better.
Built-in pros:
1- Usage with TextBoxFor, EditorFor, LabelFor and other strongly typed view model helpers can understand and communicate better with each other.
2- Url routing support
3- No framework by desing problems :
In general, we recommend folks don’t write custom model binders
because they’re difficult to get right and they’re rarely needed. The
issue I’m discussing in this post might be one of those cases where
it’s warranted.
Link of quote
And also after some research I found these useful works:
Binding model property with different name
One step upgrade of first link
Here some informative guide
Result: But none of them give me my problems exact solution. I am looking for a strongly typed solution for this problem. Of course if you know any other way to go, please share.
Update
The underlying reasons why I want to do this are basically:
1- Everytime I want to change the html control name then I have to change PropertyName at compile time. (There is a difference Changing a property name between changing a string in code)
2- I want to hide (camouflage) real property names from end users. Most of times View Model property names same as mapped Entity Objects property names. (For developer readability reasons)
3- I don't want to remove the readability for developer. Think about lots of properties with like 2-3 character long and with mo meanings.
4- There are lots of view models written. So changing their names are going to take more time than this solution.
5- This is going to be better solution (in my POV) than others which are described in other questions until now.
Actually, there is a way to do it.
In ASP.NET binding metadata gathered by TypeDescriptor, not by reflection directly. To be more precious, AssociatedMetadataTypeTypeDescriptionProvider is used, which, in turn, simply calls TypeDescriptor.GetProvider with our model type as parameter:
public AssociatedMetadataTypeTypeDescriptionProvider(Type type)
: base(TypeDescriptor.GetProvider(type))
{
}
So, everything we need is to set our custom TypeDescriptionProvider for our model.
Let's implement our custom provider. First of all, let's define attribute for custom property name:
[AttributeUsage(AttributeTargets.Property)]
public class CustomBindingNameAttribute : Attribute
{
public CustomBindingNameAttribute(string propertyName)
{
this.PropertyName = propertyName;
}
public string PropertyName { get; private set; }
}
If you already have attribute with desired name, you can reuse it. Attribute defined above is just an example. I prefer to use JsonPropertyAttribute because in most cases I work with json and Newtonsoft's library and want to define custom name only once.
The next step is to define custom type descriptor. We will not implement whole type descriptor logic and use default implementation. Only property accessing will be overridden:
public class MyTypeDescription : CustomTypeDescriptor
{
public MyTypeDescription(ICustomTypeDescriptor parent)
: base(parent)
{
}
public override PropertyDescriptorCollection GetProperties()
{
return Wrap(base.GetProperties());
}
public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
{
return Wrap(base.GetProperties(attributes));
}
private static PropertyDescriptorCollection Wrap(PropertyDescriptorCollection src)
{
var wrapped = src.Cast<PropertyDescriptor>()
.Select(pd => (PropertyDescriptor)new MyPropertyDescriptor(pd))
.ToArray();
return new PropertyDescriptorCollection(wrapped);
}
}
Also custom property descriptor need to be implemented. Again, everything except property name will be handled by default descriptor. Note, NameHashCode for some reason is a separate property. As name changed, so it's hash code need to be changed too:
public class MyPropertyDescriptor : PropertyDescriptor
{
private readonly PropertyDescriptor _descr;
private readonly string _name;
public MyPropertyDescriptor(PropertyDescriptor descr)
: base(descr)
{
this._descr = descr;
var customBindingName = this._descr.Attributes[typeof(CustomBindingNameAttribute)] as CustomBindingNameAttribute;
this._name = customBindingName != null ? customBindingName.PropertyName : this._descr.Name;
}
public override string Name
{
get { return this._name; }
}
protected override int NameHashCode
{
get { return this.Name.GetHashCode(); }
}
public override bool CanResetValue(object component)
{
return this._descr.CanResetValue(component);
}
public override object GetValue(object component)
{
return this._descr.GetValue(component);
}
public override void ResetValue(object component)
{
this._descr.ResetValue(component);
}
public override void SetValue(object component, object value)
{
this._descr.SetValue(component, value);
}
public override bool ShouldSerializeValue(object component)
{
return this._descr.ShouldSerializeValue(component);
}
public override Type ComponentType
{
get { return this._descr.ComponentType; }
}
public override bool IsReadOnly
{
get { return this._descr.IsReadOnly; }
}
public override Type PropertyType
{
get { return this._descr.PropertyType; }
}
}
Finally, we need our custom TypeDescriptionProvider and way to bind it to our model type. By default, TypeDescriptionProviderAttribute is designed to perform that binding. But in this case we will not able to get default provider that we want to use internally. In most cases, default provider will be ReflectTypeDescriptionProvider. But this is not guaranteed and this provider is inaccessible due to it's protection level - it's internal. But we do still want to fallback to default provider.
TypeDescriptor also allow to explicitly add provider for our type via AddProvider method. That what we will use. But firstly, let's define our custom provider itself:
public class MyTypeDescriptionProvider : TypeDescriptionProvider
{
private readonly TypeDescriptionProvider _defaultProvider;
public MyTypeDescriptionProvider(TypeDescriptionProvider defaultProvider)
{
this._defaultProvider = defaultProvider;
}
public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
{
return new MyTypeDescription(this._defaultProvider.GetTypeDescriptor(objectType, instance));
}
}
The last step is to bind our provider to our model types. We can implement it in any way we want. For example, let's define some simple class, such as:
public static class TypeDescriptorsConfig
{
public static void InitializeCustomTypeDescriptorProvider()
{
// Assume, this code and all models are in one assembly
var types = Assembly.GetExecutingAssembly().GetTypes()
.Where(t => t.GetProperties().Any(p => p.IsDefined(typeof(CustomBindingNameAttribute))));
foreach (var type in types)
{
var defaultProvider = TypeDescriptor.GetProvider(type);
TypeDescriptor.AddProvider(new MyTypeDescriptionProvider(defaultProvider), type);
}
}
}
And either invoke that code via web activation:
[assembly: PreApplicationStartMethod(typeof(TypeDescriptorsConfig), "InitializeCustomTypeDescriptorProvider")]
Or simply call it in Application_Start method:
public class MvcApplication : HttpApplication
{
protected void Application_Start()
{
TypeDescriptorsConfig.InitializeCustomTypeDescriptorProvider();
// rest of init code ...
}
}
But this is not the end of the story. :(
Consider following model:
public class TestModel
{
[CustomBindingName("actual_name")]
[DisplayName("Yay!")]
public string TestProperty { get; set; }
}
If we try to write in .cshtml view something like:
#model Some.Namespace.TestModel
#Html.DisplayNameFor(x => x.TestProperty) #* fail *#
We will get ArgumentException:
An exception of type 'System.ArgumentException' occurred in System.Web.Mvc.dll but was not handled in user code
Additional information: The property Some.Namespace.TestModel.TestProperty could not be found.
That because all helpers soon or later invoke ModelMetadata.FromLambdaExpression method. And this method take expression we provided (x => x.TestProperty) and takes member name directly from member info and have no clue about any of our attributes, metadata (who cares, huh?):
internal static ModelMetadata FromLambdaExpression<TParameter, TValue>(/* ... */)
{
// ...
case ExpressionType.MemberAccess:
MemberExpression memberExpression = (MemberExpression) expression.Body;
propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : (string) null;
// I want to cry here - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// ...
}
For x => x.TestProperty (where x is TestModel) this method will return TestProperty, not actual_name, but model metadata contains actual_name property, have no TestProperty. That is why the property could not be found error thrown.
This is a design failure.
However despite this little inconvenience there are several workarounds, such as:
The easiest way is to access our members by theirs redefined names:
#model Some.Namespace.TestModel
#Html.DisplayName("actual_name") #* this will render "Yay!" *#
This is not good. No intellisense at all and as our model change we will have no any compilation errors. On any change anything can be broken and there is no easy way to detect that.
Another way is a bit more complex - we can create our own version of that helpers and forbid anybody from calling default helpers or ModelMetadata.FromLambdaExpression for model classes with renamed properties.
Finally, combination of previous two would be preferred: write own analogue to get property name with redefinition support, then pass that into default helper. Something like this:
#model Some.Namespace.TestModel
#Html.DisplayName(Html.For(x => x.TestProperty))
Compilation-time and intellisense support and no need to spend a lot of time for complete set of helpers. Profit!
Also everything described above work like a charm for model binding. During model binding process default binder also use metadata, gathered by TypeDescriptor.
But I guess binding json data is the best use case. You know, lots of web software and standards use lowercase_separated_by_underscores naming convention. Unfortunately this is not usual convention for C#. Having classes with members named in different convention looks ugly and can end up in troubles. Especially when you have tools that whining every time about naming violation.
ASP.NET MVC default model binder does not bind json to model the same way as it happens when you call newtonsoft's JsonConverter.DeserializeObject method. Instead, json parsed into dictionary. For example:
{
complex: {
text: "blabla",
value: 12.34
},
num: 1
}
will be translated into following dictionary:
{ "complex.text", "blabla" }
{ "complex.value", "12.34" }
{ "num", "1" }
And later these values along with others values from query string, route data and so on, collected by different implementations of IValueProvider, will be used by default binder to bind a model with help of metadata, gathered by TypeDescriptor.
So we came full circle from creating model, rendering, binding it back and use it.
The short answer is NO and long answer still NO. There is no built-in helper, attribute, model binder, whatever is it (Nothing out of box).
But what I did in before answer (I deleted it) was an awful solution that I realized yesterday. I am going to put it in github for who still wants to see (maybe it solves somebody problem) (I don't suggest it also!)
Now I searched it for again and I couldn't find anything helpful. If you are using something like AutoMapper or ValueInjecter like tool for mapping your ViewModel objects to Business objects and if you want to obfuscate that View Model parameters also, probably you are in some trouble. Of course you can do it but strongly typed html helpers are not going to help you alot. I even not talking about the if other developers taking branch and working over common view models.
Luckily my project (4 people working on it, and its commercial use for) not that big for now, so I decided to change View Model property names! (It is still lot work to do. Hundreds of view models to obfuscate their properties!!!) Thank you Asp.Net MVC !
There some ways in the links which I gave in question. But also if you still want to use the BindAlias attribute, I can only suggest you to use the following extension methods. At least you dont have to write same alias string which you write in BindAlias attribute.
Here it is:
public static string AliasNameFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
if (memberExpression == null)
throw new InvalidOperationException("Expression must be a member expression");
var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
if (aliasAttr != null)
{
return MvcHtmlString.Create(aliasAttr.Alias).ToHtmlString();
}
return htmlHelper.NameFor(expression).ToHtmlString();
}
public static string AliasIdFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
var memberExpression = ExpressionHelpers.GetMemberExpression(expression);
if (memberExpression == null)
throw new InvalidOperationException("Expression must be a member expression");
var aliasAttr = memberExpression.Member.GetAttribute<BindAliasAttribute>();
if (aliasAttr != null)
{
return MvcHtmlString.Create(TagBuilder.CreateSanitizedId(aliasAttr.Alias)).ToHtmlString();
}
return htmlHelper.IdFor(expression).ToHtmlString();
}
public static T GetAttribute<T>(this ICustomAttributeProvider provider)
where T : Attribute
{
var attributes = provider.GetCustomAttributes(typeof(T), true);
return attributes.Length > 0 ? attributes[0] as T : null;
}
public static MemberExpression GetMemberExpression<TModel, TProperty>(Expression<Func<TModel, TProperty>> expression)
{
MemberExpression memberExpression;
if (expression.Body is UnaryExpression)
{
var unaryExpression = (UnaryExpression)expression.Body;
memberExpression = (MemberExpression)unaryExpression.Operand;
}
else
{
memberExpression = (MemberExpression)expression.Body;
}
return memberExpression;
}
When you want to use it:
[ModelBinder(typeof(AliasModelBinder))]
public class FilterViewModel
{
[BindAlias("someText")]
public string FilterParameter { get; set; }
}
In html:
#* at least you dont write "someText" here again *#
#Html.Editor(Html.AliasNameFor(model => model.FilterParameter))
#Html.ValidationMessage(Html.AliasNameFor(model => model.FilterParameter))
So I am leaving this answer here like this. This is even not an answer (and there is no answer for MVC 5) but who searching in google for same problem might find useful this experience.
And here is the github repo: https://github.com/yusufuzun/so-view-model-bind-20869735

HTMLHelper, generating parameter of type "Expression<Func<TModel, TValue>> expression" out of property

I'm writing an HTML Helper for an editor. The idea is to get properties from the Model with attributes AutoGenerateField and build a table, each line of which contains a name of a field (also from the attribute) and a TextBox or a CheckBox containing actual value of the field.
I have a problem with HTMLHelper. Since I send the whole model to the helper and not one value, I cannot use methods such as TextBoxFor, as they need parameter, such as
"Expression<Func<TModel, TValue>> expression".
I'm using reflexion and I tried to send the property instead, still VisualStudio considers this as incorrect usage.
Below is simplified method for my HtmlHelper:
public static MvcHtmlString GenerateEditor<TModel>(this HtmlHelper<TModel> htmlHelper)
{
var model = htmlHelper.ViewData.Model;
var result = String.Empty;
//generating container, etc ...
foreach (var property in model.GetType().GetProperties())
{
var attr = property.GetCustomAttributes(typeof (DisplayAttribute), true).FirstOrDefault();
if (attr == null) continue;
var autoGenerate = ((DisplayAttribute)attr).AutoGenerateField;
if(autoGenerate)
{
//here I'm building the html string
//My problem is in the line below:
var r = htmlHelper.TextBoxFor(property);
}
}
return MvcHtmlString.Create(result);
}
Any ideas?
How about just using the non-lambda overloads. : InputExtensions.TextBox()
if(autoGenerate)
{
//here I'm building the html string
//My problem is in the line below:
var r = htmlHelper.TextBox(property.Name);
}
//not sure what you do with r from here...
If I'm not mistaken the name attribute of the form element is set to the property name even when you use the lambda version of the function so this should do the same thing.
I will try and verify what the lambda function does, you might be able to do the same since you have the TModel with you.
Update
From a quick look of things inside the source code of InputExtensions.cs, TextBoxFor calls eventually calls InputHelper() which eventually calls ExpressionHelper.GetExpressionText(LambdaExpression expression) inside ExpressionHelper.cs which from the cursory looks of things gets the member.Name for the name html attribute on the input element.
I can't quite verify it right now because I'm not on windows but I think the non-lambda function should suit your need. Do tell me how it goes?

Resources