I'm trying to generate forms with Blazor programmatically, and I'm running into an issue where I need to bind an InputText value to a member of a collection such as:
#foreach (var prop in formProperties)
{
<InputText id=#prop.Name #bind-Value="form.Responses[prop.Name]" />
}
However, I get the following exception:
System.ArgumentException: The provided expression contains a InstanceMethodCallExpression1 which is not supported. FieldIdentifier only supports simple member accessors (fields, properties) of an object.
Is it possible to bind the input to a collection of some sort?
Running into a similar issue where I'm trying to build a table based on a dictionary of attributes, I came across this reply from Steve.
Basically what you can do is introduce a new type called e.g.
public class FormResponse
{
public string Value { get; set;
}
Assuming you now create a dictionary of FormResponse on your form object, you can then do this:
#foreach (var prop in formProperties)
{
<InputText id="#prop.Name" #bind-Value="form.Responses[prop.Name].Value" />
}
Which is arguably more straightforward than creating a new component with Chained Binding.
Unfortunately, you cannot use the <InputText/> component for more complex accessors presently.
However, if you don't mind handling your own validation/notification code, you can roll your own by:
Changing the <InputText/> to an <input/>
Use #bind instead of #bind-Value
Using your code:
#foreach (var prop in formProperties)
{
<input id=#prop.Name #bind="form.Responses[prop.Name]" />
}
Again, you will loose change notifications, and validation, and all connection to the EditForm and EditContext.
You can also go the route that #dennis1679 suggested, creating a sub component. I am curious what the Chained Binding integration with the top level form/model validation would be like in practice with. I haven't played with that yet.
FYI, I did a quick search, and didn't see the issue (likely a feature) in github, but when I find it, I will update this.
Related
Problem Description
My problem is similar to this question but instead of applying Data Annotations to the property Name via reflection (handled by ModelMetadata.DisplayName) I am applying them to the value (not handled by ModelMetadata).
Detailed Description
In the context of an ASP.NET MVC program.
Suppose I have a Model class
public class Model
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string NickName { get; set; }
public string Address { get; set; }
public int Phone { get; set; }
[Display(Name = "Start Date")]
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime StartDate { get; set }
[Display(Name = "End Date")]
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:dd/MM/yyyy}")]
public DateTime EndDate { get; set }
}
Then suppose this Model is used in at least 5 different views where the value of every property must be displayed in full. (Sometimes for every instance, other times for a handful, other times for one).
I can manually list each Property access within it's own
<td>#Html.DisplayFor(item.<Property>)</td>
for every view.
But that won't help me if later on the definition for Model expands to include new Properties (like Description and Relation and Reliability). Then I'd need to manually update every occurrence of the complete Model listing.
I can use reflection to iterate over a list of PropertyInfo's and save having to manually list each property by using
<td>#property.GetValue(item)</td>
But DisplayFor(x) does not support an expression as complex as x=>property.GetValue(item), and this means I lose the Data Annotations that format my DateTime as
01/01/1990
instead of
01-Jan-90 12:00:00 AM
and would likely also result in the loss of all types of annotation including validation.
Problem Solutions
So far I have considered (and in some cases attempted) the following solutions:
[Failed] Manually craft an Expression which emulates the functionality of #property.GetValue(item)
[Edit]
[Failed] Pass DisplayFor a MethodInfo object representing the Property Accessor DisplayFor(x => property.GetGetMethod()) as well as .Invokeing it on x.
[/Edit]
Retrieve the value manually as normal, and
execute a method on it to manually retrieve and implement Annotation Data on it prior to insertion in the view element as this question suggests, or
Re-implement the DisplayFor handling of Data Annotations on an as-needed basis in a Display Template View and apply that directly to the value via DisplayFor as this question suggested
Refactor the Model class to contain only a list(SortedList?) of 'Prop' instances, where 'Prop' is a class representing a Property with a Name and Value element.
This last solution would turn the broken
#Html.DisplayFor(m=>property.GetValue(item)
into a theoretically working
#Html.DisplayFor(m=>item.Properties[i].Value)
Which aside from the slightly unintuitive need for getting a property called Name (Properties["Name"]) by (.Value), seems the most workable solution, at the cost of Model clarity.
[Edit]
Most recently I have created a Utility method which retrieves the DisplayFormatAttribute from the PropertyInfo and returns either the DisplayFormatString or the default of "{0}" if a format string was not annotated. I have then used it to create a collection of preformatted property values within a ViewModel.
This seems for now, to be the most elegant way I know of to decouple the View from the Model as much as possible while still retrieving the necessary data from it.
[/Edit]
The Question
This is at the moment, purely a learning exercise, but I would like to know...
Is it possible to succeed where I have failed and both have my Reflection cake and eat the Data Annotations too? Or must I seek an alternative solution?
If I must seek an alternative solution, are there routes I have missed, or am I at least on the right track?
Maybe something similar to:
#foreach (var property in Model.GetType().GetProperties())
{
<li>#property.GetValue(Model, null)</li>
}
Great Success
Revisiting my original attempt to manually craft the expression dynamically, I discovered this article which did precisely what I wanted to do, and using mostly Microsoft provided code as well!
Though the Microsoft code was difficult to find (the link in the article is broken and the example slightly outdated for the code I did find), I was able to use it to good effect to implement my own DisplayFor extension method.
Unfortunately, due to my model being a list rather than a single instance, I still needed to create a partial view to pass an instance to, so that I could access the properties via Model from within the generated expression.
My View code now looks like this:
#foreach (var thing in Model.CollectionOfThings)
{
<tr>
#foreach (var prop in typeof(Thing).GetProperties())
{
<td>
#{
Html.RenderPartial("~/Views/Shared/_DisplayForReflectedProperty.cshtml",
new Tuple<Thing, PropertyInfo>(thing, prop));
}
</td>
}
}
With _DisplayForReflectedProperty as simple as
#using WebApplication1.Models
#using System.Reflection
#using WebApplication1.Extensions
#model Tuple<Thing, PropertyInfo>
#Html.DisplayFor("Item1."+Model.Item2.Name)
And the only difference between my DisplayFor extension and the one in the article is the null object parameter in this function call (plus the obvious conversions from EditorFor to DisplayFor):
var lambda = System.Linq.Dynamic.DynamicExpression.ParseLambda(typeof(TModel),
null, expression);
Using this template I can now generate arbitrary (from code perspective, specific from business rule perspective) subsets of a model's properties to display in any manner I wish, without having to custom tailor my views to each particular subset, while retaining all the benefits of using the 'For' Helpers!
I have a completely configurable Entity Framework Model. By configurable I mean the user can choose which fields are visible or not, required or not and read-only or not. I applied the configuration by overriding the DataAnnotationsModelMetadataProvider class, now the problem is using the Model with Html.EditorFor Helper.
I wan't to loop through each property and add Html.EditorFor Like the following
#foreach(var field in fieldConfig)
{
Html.EditorFor(...Dynamicaly Created Expression...)
}
EDIT:
fieldConfig is list from ConfigTable and it contains fields like FieldName, IsRequred, IsReadOnly, OrderNo, Type
Thank you in advance.
If You want your 'dynamic expression' to be a string, you may use
Html.Editor("MyProperty")
or even
Html.Editor("MyProperty.AnotherProperty.Name")
instead.
It takes a string expression and works just the same way as EditorFor, but not typesafe.
Or You wan't to be typesafe and learn how to create expression trees: http://weblogs.asp.net/dixin/archive/2009/11/29/understanding-csharp-3-0-features-6-lambda-expression.aspx
Try
#foreach(var field in fieldConfig)
{
Html.EditorFor(model=> field)
}
Last time I've used it it worked
Maybe you can try something like that, by using reflection you can get your class' properties. I didn't try so your can check on your side:
var properties = Model.GetType().GetProperties();
foreach (var field in properties)
{
Html.EditorFor(model => field.GetValue(Model, null));
}
I have the following code and I get an error saying:
has no applicable method named 'TextBoxFor' but appears to have an extension method by that name.
My Code:
#Html.TextBoxFor(ViewBag.taglist)
Why don't you use strongly typed model in your view instead of ViewBag. This will make your life easier.
In fact, you must use a model to with TextBoxFor, otherwise it just won't work. See the definition of TextBoxFor - as a second parameter it takes a lambda expression that takes a property form a model.
If you want just a text box, two options:
#Html.TextBox("NameOfTheTextbox", (String)ViewBag.SomeValue)
or just go
<input type="text" value="#ViewBag.SomeValue" />
No complex solutions required.
I agree with other suggestions of using a strongly-typed model, because the compile-time error support is so much better than debugging exceptions. Having said that, in order to do what you want, you can use this:
#Html.TextBox("NameOfTextBox", (string)ViewBag.taglist)
Update: A Simple Example
Now that you've provided some details in your comments, I've taken a guess at what you might be doing, in order to provide a simple example.
I'm assuming you have a list of tags (like SO has per question) that you'd like to display neatly in a textbox, with each tag separated by a space. I'm going to assume your Tag domain model looks something like this:
public class Tag
{
public int Id { get; set; }
public string Description { get; set; }
}
Now, your view will need a list of the tags but will likely need some other information to be displayed as well. However, let's just focus on the tags. Below is a view model to represent all the tags, taking into account that you want to display them as a string inside a textbox:
public class SomeViewModel
{
public string Tags { get; set; }
// Other properties
}
In order to get the data you want you could grab all of the tags like this:
public ActionResult Index()
{
using (YourContext db = new YourContext())
{
var model = new SomeViewModel();
model.Tags = string.Join(" ", db.Tags.Select(t => t.Description).ToList());
return View(model);
}
}
Notice how I'm directly passing model to the view.
The view is now very simple:
#model SomeViewModel
#Html.EditorFor(m => m.Tags)
The model directive is what signifies that a view is strongly-typed. That means this view will expect to receive an instance of SomeViewModel. As you can see from my action code above, we will be providing this view the type that it wants. This now allows us to make use of the strongly-typed HtmlHelper (i.e. Html.XxxFor) methods.
In this particular case, I've used Html.EditorFor, as it will choose an appropriate input element to render the data with. (In this case, because Description is a string, it will render a textbox.)
You cannot use Html.TextBoxFor without explicitly setting a type for your model within the view. If you don't specify a type it defaults to dynamic. If you want to do model binding then you must use an explicit type rather than a dynamic type like ViewBag. To use Html.TextBoxFor you must define a model type that defines the property that you wish to bind. Otherwise you have to use Html.TextBox and set the value manually from ViewBag. As others have said, you will make your life much easier if you use a statically typed model and take advantage of the inbuilt MVC model binding.
You have to use a lambda expression to select the property, plus you will have to cast the ViewBag member to the correct type.
#Html.TextBoxFor(model => (string)ViewBag.taglist)
One of the key features of a project I'm working on is the ability for the user to configure Forms (as in "Forms" to fill-up) based on a pool of pre-existing field types (well known types, for instance "user name", "date of birth" etc. but also "generic types" like "string", "DateTime" etc.).
We used to have a static ViewModel that worked fine for the "well known" types and looked like this:
public class UserInputModel
{
[StringLength(200)]
public string Name { get; set; }
[Required(ErrorMessageResourceName = "BirthDateEmptyError", ErrorMessageResourceType = typeof(Resources.ErrorMessages))]
public DateTime BirthDate { get; set; }
//Here comes a lot of other properties
}
All the known properties were listed and we were showing or hiding them given the context.
But the last requirement came and changed all that. The user shall now be able to add as many generic type fields as he wants. In order to do this, we decided to make this InputModel entirely dynamic. It now looks like this:
public class UserInputModel
{
// Each ModelProperty has an "Id" and a "Value" property
public ICollection<ModelProperty> Properties { get; set; }
}
This works like a charm. The razor view only has to iterates over the collection, create the corresponding controls for each property of the collection in a more than standard way:
#Html.TextBoxFor(m => m.Properties[index].Value);
... and we nicely get the data back as a filled form.
=> This works fine, but we don't have any client-side validation. For this, we would need some Metadata... which we don't have via annotations anymore since we're dynamically creating the model.
In order to provide those MetaData, I created a CustomModelMetadataProvider that inherits from DataAnnotationsModelMetadataProvider and registered it as the new ModelMetadataProvider in the Global.asax. The CreateMetadata() function gets called upon creation of the ViewModel, and that for each of the properties of my ViewModel... sofar so good.
Where the problem starts: in order to add some metadata to the current property, I first need to identify which property I am currently looking at ("Name" has a maxlength of 200, "date of birth" hasn't so I cannot assign a maxlength to every property per default). And somewhow I didn't manage to do that yet since all the properties have the same name Value and the same container type ModelProperty.
I tried accessing the container of the property via reflection, but since the ModelAccessor's target is the ViewModel itself (because of the lambda expression m => m.Properties), the following construct gives me the ViewModel as a whole, not just the ModelProperty:
var container = modelAccessor.Target.GetType().GetField("container");
var containerObject = (UserInputModel)container.GetValue(modelAccessor.Target);
I've been flipping this over and over but cannot find a way to identify which ModelProperty I have in hand. Is there a way to do this?
Update: after flipping this in every possible direction for a while, we finally went another way. We are basically using unobstrusive javascript to use MVC's validation capabilities without touching attributes nor metadata. In short, we add HTML attributes like value-data="true" (and all other required attributes) to the #Html.TextBoxFor() statements. This works wonderfully for all the atomic validations (required, stringlength etc.).
Tim, you can leverage what appears to be client-side validation through Ajax with the Remote attribute on your properties.
Basically, you'll need to set up a validation controller and then write some smarts into that controller. But at least you'd be able to write some helper methods and keep it all in one place. You would have a series of validators, based on the meta data that you are presenting to the end users, and each validator method would work for a particular type with good re-use.
The one pitfall to this approach would be that you would need to write a validation method for each type and condition that you want to support. Sounds like you're having to go down that road anyways, though.
Hope this helps.
See if this article help you: Technique for carrying metadata to View Models with AutoMapper.
Also use this one for ideas (custom model metadata provider): changing viewmodel's MetadataType attribute at runtime
Fluent validation is probably the best option for you in my mind, but its obviously up to you to select the best match among those above.
Update
Try use ModelMetadata and override ModelMetadataProvider: Dive Deep Into MVC: ModelMetadata and ModelMetadataProvider. This way you completely customize your model metadata (this replaces data annotations) and you have complete control on what is happening, rather than relying on ASP.NET MVC.
Another good place to look at it is Creating your own ModelMetadataProvider to handle custom attributes.
Hope this all is of help to you.
I'm having trouble understanding how to render a collection as a drop down list.
If I have a model like:
public class AccountViewModel {
public string[] Country { get; set; }
}
I would like the string collection to render as a drop down list.
Using the html page helper InputFor doesn't seem to work. It simply render's a text box.
I've noticed that InputFor can reflect on the property type and render html accordingly. (Like a checkbox for a boolean field).
I also notice that FubuPageExtensions has methods for CheckBoxFor and TextBoxFor, but nothing equivalent to DropDownListFor.
I'm probably missing something quite fundamental in understanding html conventions in fubu.
Do I need to build the select tag myself? If so, what is the recommended approach to do it?
You are correct that (at the time I last looked) there is no FubuMVC.Core HTML extension method for generating select tags although you could use the HtmlTags library to generate a select tag via code.
As you touch upon in your question the correct way to attack this is likely with an HTML convention together with the HtmlTags library such as that demonstrated in the FubuMVC.Recipes example 'src/UI/HtmlConventionsWithPageExtensions'.
For example an enum generation example might be:
this.Editors
.If(e => e.Accessor.PropertyType.IsEnum)
.BuildBy(er =>
{
var tag = new HtmlTag("select");
var enumValues = Enum.GetValues(er.Accessor.PropertyType);
foreach (var enumValue in enumValues)
{
tag.Children.Add(new HtmlTag("option").Text(enumValue.ToString()));
}
return tag;
});
The FubuMVC.Recipes repository is quite new and still growing so there may be some better examples around but hope this gives you some ideas.