IEnumerable in my ViewModel is not displayed with EditorForModel - razor

ViewModel
[Validator(typeof(ProdutoCategoriaValidator))]
public class ProdutoCategoriaViewModel
{
[HiddenInput(DisplayValue = false)]
public Guid ID { get; set; }
public IEnumerable<SelectListItem> Tipos { get; set; } // <<<<------- Is not showing in my view
[AdditionalMetadata("data-bind", "event: { change: function(data) { Link(data.Nome()); }}")]
public string Nome { get; set; }
[DataType(DataType.Url)]
[AdditionalMetadata("Prefixo", "Produtos/{tipo-de-produto}#")]
public string Link { get; set; }
public int? Ordem { get; set; }
public ProdutoCategoriaViewModel()
{
ID = Guid.NewGuid();
}
}
Solution
View (_Formulario.cshtml)
#model ProdutoCategoriaViewModel
#using (Html.BeginForm(null, null, FormMethod.Post, new { id="form-produtocategoria", data_bind = "submit: salvar" }))
{
#Html.AntiForgeryToken()
<legend>#Html.MvcSiteMap().SiteMapTitle()</legend>
<fieldset>
#Html.ValidationSummary(false, "Verifique os erros abaixo:")
#Html.EditorForModel()
</fieldset>
<div class="buttons">
#Html.ActionLink("Cancelar", "Index")
<input type="submit" value="SALVAR" />
</div>
}
SelectListItem.cshtml
#model IEnumerable<SelectListItem>
#Html.DropDownListFor(m => m, Model)
<p>Test</p>
Result
Full image: http://i.imgur.com/I7HxA.png
Notes
I've tried to put the attribute "UIHint" but still nothing is displayed!
Questions
What am I doing wrong?

By default when you use Html.EditorForModel don't expect this to recurse down to complex properties such as your Tipos property which is of type IEnumerable<SelectListItem>. Brad Wilson explained this in his blog post (more specifically read the Shallow Dive vs. Deep Dive section towards the end of the post). You will need to write a custom editor template for the Object type if you want this to happen.
Another possibility is to specify the template name:
#Html.EditorFor(x => x.Tipos, "SelectListItem")
Also bear in mind that your editor template for the SelectListItem is wrong because you are binding the DropDownListFor to the model as first argument. Don't forget that the first argument of this helper must be a scalar property that will be used to hold the selected value. You need a string or integer property on your view model for this. The second argument represents the collection.
Another important aspect about editor templates is that when you have a property of type IEnumerable<T> and an editor template called T.cshtml this editor template must be strongly typed to the T class and not IEnumerable<T> as you did with your SelectListItem.cshtml template. This doesn't apply if you use UIHint or specify the template name as second argument to the EditorFor helper. n this case the template will be typed to the collection.
So to recap, you could either implement a custom object editor template as Brad Wilson suggested that will recurse down to complex properties or you could modify your _Formulario.cshtml view to specify EditorFor each individual elements.

A #foreach loop renders something that looks right, but the resulting markup will have the same id for each row's controls. It also will not post the enumerable collection back with the model instance.
There are two ways to make this work such that you have a unique id for each item in the collection, and so that the collection is hydrated on postbacks:
1. Use the default editor template rather than a named one
// editor name parameter must be omitted; default editor template's model type
// should be a single instance of the child object, not an IEnumerable. This
// convention looks wrong, but it fully works:
#Html.EditorFor(parentObject => parentObject.Items)
2. Use a #for loop, not a #foreach:
#for (int i = 0; i < parentObject.Items.Count ; i++) {
// the model binder uses the indexer to disambiguate the collection items' controls:
#Html.EditorFor(c => Model.Items[i], "MyEditorTemplate")
}
This will not work, however:
// this will error out; the model type will not match the view's at runtime:
#Html.EditorFor(parentObject => parentObject.Items, "MyEditorTemplate")
Nor will this:
#foreach(var item in parentObject.Items) {
// this will render, but won't post the collection items back with the model instance:
#Html.EditorFor(c => item, "MyEditorTemplate")
}
For a detailed answer why this is, look at this question: MVC can't override EditorTemplate name when used in EditorFor for child object.

Related

How to define templated component in RenderFragment with its #context available

I have a templated component (a tooltip) that has a parameter and passes that parameter in context to child content. That parameter is a wrapper for ElementReference. The purpose of this is to get back to the tooltip the child's reference, once it is set.
What I want to do is to store a particular instance of that tooltip component in a reusable RenderFragment in several places.
But I get the error The name 'context' does not exists in the current context.
This is original question, but it proved to be oversimplified. Please go to second separation line, with sample more resembling my situation.
Here is a sample (not that tooltip, but simplified code that has exactly the same problem).
Templated component RenderFragTempl:
#inherits ComponentBase
<span id="#(Ref)">
#IntChildContent(Ref)
</span>
#code {
[Parameter] public RenderFragment<int> IntChildContent { get; set; }
[Parameter] public int Ref { get; set; }
}
and the call in Index.razor:
#page "/"
#using BlazorServer.Pages.StackOverflow
<RenderFragTempl Ref="10">
<IntChildContent>
<p>#context</p>
</IntChildContent>
</RenderFragTempl>
<br />
#test
#code {
//compiler error CS0103: The name 'context' does not exist in the current context
private readonly RenderFragment test = #<RenderFragTempl Ref="10001">
<IntChildContent>
<p>#context</p>
</IntChildContent>
</RenderFragTempl>;
}
EDIT: 1
I have a tooltip component that has a child content. Tooltip will be shown whenever the mouse hovers over that child content. But tooltip does not wrap the child content with anything. So if you check out the source, you will only see the child content without any indication of the tooltip. The moment mouse cursor hovers over the child content, the tooltip container is added to the bottom of the page and is position right over that child content. This is quite problematic to achieve in blazor because the tooltip needs to have the reference to the child content. But references are established after parameters are filled. So a special wrapper is used to achieve that and Tooltip is built as a templated component.
So the wrapper for ElementReference I use (curtesy of MatBlazor):
public class TargetForwardRef
{
private ElementReference _current;
public ElementReference Current
{
get => _current;
set
{
Set(value);
//this is just for debugging purpose
Console.WriteLine($"Ref: {value.Id ?? "null"}");
}
}
public void Set(ElementReference value) => _current = value;
}
My simplified Tooltip as RenderFragTempl (just the important bits)
#inherits ComponentBase
<span>
#UnboundChildContent(RefBack)
</span>
#code {
[Parameter] public RenderFragment<TargetForwardRef> UnboundChildContent { get; set; }
[Parameter] public TargetForwardRef RefBack { get; set; } = new TargetForwardRef();
}
And my index.razor
#page "/"
#*
//this is working, will printout to console `<span>` reference id, you will be
//able to find it once you go to source; I added this here for reference
*#
<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Span content</span>
</UnboundChildContent>
</RenderFragTempl>
<br/>
#test
#code {
//compiler error CS0103: The name 'context' does not exist in the current context
private readonly RenderFragment test =
#<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Hover to show tooltip</span >
</UnboundChildContent>
</RenderFragTempl>;
}
To answer the question - why I am trying to use RenderFragment - imagine I have a collection of cards - let's say 150. The card component accepts as a parameter IList<RenderFragment> for rendering buttons on a card. I want to pass my icon with tooltip to that card. I need to have access to Tooltip's context.
I tried renaming context to something else, but I get the same error (except in the error there is new context name).
Why define a templated component and use it like that ?
However, here's code sample that demonstrates how to get the same required result without defining a templated component:
#test(12)
#code {
private RenderFragment<int> test => (value) => (__builder) =>
{
<span id="#(value)">
#value
</span>
};
}
But if you insist, here's the code that demonstrate how to render your component:
#test((10001, 15))
#code
{
private RenderFragment<(int, int)> test => (value) => (__builder) =>
{
<RenderFragTempl Ref="#value.Item1">
<IntChildContent>
<p>#value.Item2</p>
</IntChildContent>
</RenderFragTempl>;
};
}
Update:
The following code describes how to render the templated component with the internal variable context. You can improve on it as you like.
Note: I did not read your update... I just do what you requested on the original question, but this time using context.
#test(121)
#code {
private RenderFragment<int> test => (value) => (__builder) =>
{
__builder.OpenComponent<TemplatedComponent>(0);
__builder.AddAttribute(1, "Ref", value);
__builder.AddAttribute(2, "IntChildContent",
(RenderFragment<int>)((context) => (__builder2) =>
{
__builder2.OpenElement(3, "p");
__builder2.AddContent(4, context);
__builder2.CloseElement();
}
));
__builder.CloseComponent();
};
}
Note that when you invoke the RenderFragment delegate you pass it a value which is assigned to the Ref parameter, and is passed in the form the the internal context variable
I asked this question in https://github.com/dotnet/aspnetcore/issues/29671 and I got an answer. This is not exactly what I wanted, but that seems to be official:
private readonly RenderFragment<TargetForwardRef> test => context =>
#<RenderFragTempl>
<UnboundChildContent>
<span #ref="#context.Current">Hover to show tooltip</span >
</UnboundChildContent>
</RenderFragTempl>;

Model binding with a child object

I have a class:
public class Application
{
....
public Deployment NewDeployment { get; set; }
....
}
I have an editor template for Deployment within the Application View folder.
The ApplicationViewModel has a SelectedApplication (of type Application), in my Index.cshtml where I use ApplicationViewModel as my Model, I have this call:
#using (Html.BeginForm("Create", "Deployment", new { #id = Model.SelectedId,
q = Model.Query }, FormMethod.Post, new { id = "form", role = "form" }))
{
#Html.EditorFor(m => m.SelectedApplication.NewDeployment)
}
Which then correctly renders out the control in my DisplayTemplates\Deployment.cshtml (though, it may just be pulling the display code and nothing in relation to the NewDeployment object's contents). All is well in the world until I go to submit. At this stage everything seems good. Controller looks like:
public class DeploymentController : Controller
{
[HttpPost]
public ActionResult Create(Deployment NewDeployment)
{
Deployment.CreateDeployment(NewDeployment);
return Redirect("/Application" + Request.Url.Query);
}
}
However, when it goes to DeploymentController -> Create, the object has nulls for values. If I move the NewDeployment object to ApplicationViewModel, it works fine with:
#Html.EditorFor(m => m.NewDeployment)
I looked at the output name/id which was basically SelectedApplication_NewDeployment, but unfortunately changing the Create signature to similar didn't improve the results. Is it possible to model bind to a child object and if so, how?
Your POST action should accept the same model your form is working with, i.e.:
[HttpPost]
public ActionResult Create(ApplicationViewModel model)
Then, you'll be able to get at the deployment the same way as you did in the view:
model.SelectedApplication.NewDeployment
It was technically an accident that using #Html.EditorFor(m => m.NewDeployment) worked. The only reason it did is because the action accepted a parameter named NewDeployment. If the parameter had been named anything else, like just deployment. It would have also failed.
Per Stephen Muecke's comment and with slight modifications, I was able to find how to correct it:
[HttpPost]
public ActionResult Create ([Bind(Prefix="SelectedApplication.NewDeployment")] Deployment deployment)
{
// do things
}

StringLength in ViewModel not setting maxlength on textbox

Here the property in my ViewModel:
[Display(Name = "Ext.")]
[MaxLength(6, ErrorMessage = "Must be a maximum of 6 characters")]
[StringLength(6)]
public string Extension { get; set; }
And in my View:
#Html.EditorFor(model => model.Extension)
And it renders:
<input class="text-box single-line" data-val="true" data-val-length="The field Ext. must be a string with a maximum length of 6." data-val-length-max="6" id="Extension" name="Extension" type="text" value="" />
Should this be setting the maxlength attribute on my textbox? If not, how can I do that with DataAttributes?
I'd like the attribute that I set in the ViewModel to control this if
possible.
ASP.NET MVC provides an extensible system for doing exactly this. Here is what you need to do:
Implement a custom ModelMetadataProvider.
Look for the StringLengthAttribute or the MaxLengthAttribute, extract the information and add it to the ModelMetadata.
Provide a custom Editor template that makes use of the information.
Step 1: Implement a custom ModelMetadataProvider.
Create a class that derives from ModelMetadataProvider. Typically you would derive from the DataAnnotationsModelMetadataProvider as this provides some default functionality which means you only have to override a single method called CreateMetadata.
Step 2: Extract the information:
To get the information, you need to look for the attribute, extract the maximum length information and add it to the AdditionalValues dictionary of the ModelMetadata. The implementation would look something like this (this is the entire implementation):
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
// Call the base class implementation to create the default metadata...
var metadata = base.CreateMetadata(
attributes,
containerType,
modelAccessor,
modelType,
propertyName);
// Extract the stringLengthAttribute (you can do the same for the
// MaxLengthAttribute if you want).
var attr = attributes
.OfType<StringLengthAttribute>()
.First();
// This could be getting called on a property that doesn't have the
// attribute so make sure you check for null!
if (attr != null)
{
metadata.AdditionalValues["maxLength"] = attr.MaximumLength;
}
return metadata;
}
}
In order for ASP.NET MVC to use this you need to register it in the Application_Start method in Global.asax.
ModelMetadataProviders.Current = new CustomModelMetadataProvider();
Step 3: Create a custom editor template.
You now need to create a view that uses the information. Create a new view called String in the Views\Shared\ folder.
String.cshtml
#{
object maxLength;
if (!ViewData.ModelMetadata.AdditionalValues
.TryGetValue("maxLength", out maxLength))
{
maxLength = 0;
}
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{ "maxlength", (int)maxLength },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)
When you run your application you will get the following HTML output by calling #Html.EditorFor.
<input class="text-box single-line" id="Extension" maxlength="6" name="Extension" type="text" value="" />
If you want to know more about the model metadata provider system, Brad Wilson has a series of blog posts that detail how it works (these were written prior to the Razor view engine so some of the view Syntax is a bit funky but otherwise the information is sound).
Essentially based on Brad's answer, wrapped in an extension on the Html helper using lambda syntax so you don't pollute your Razor views with reflection stuff:
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Mvc;
public static class HtmlHelper
{
public static int? MaxLength<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
MemberExpression memberExpression = (MemberExpression)expression.Body;
PropertyInfo property = typeof(TModel)
.GetProperty(memberExpression.Member.Name);
StringLengthAttribute attribute = (StringLengthAttribute)property
.GetCustomAttributes(typeof(StringLengthAttribute), true)
.FirstOrDefault();
if (attribute != null)
{
return attribute.MaximumLength;
}
return null;
}
}
Use it like such:
#Html.TextBoxFor(x => x.Name, new { maxlength = Html.MaxLength(x => x.Name) })
where x refers to your model.
If the StringLengthAttribute is not declared for the property, null will be returned and the maxlength attribute will be empty on the textbox element.
Remember to include using in your razor page so you can access the method.
#using HtmlHelper
You also need to use none null-able result for the method to overcome compile error.
I ran into something similar, here was my quick and dirty solution:
at the top of your .cshtml file add the line:
#{
var max = ((System.ComponentModel.DataAnnotations.StringLengthAttribute)(typeof(MyType))
.GetProperty("MyProp")
.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.StringLengthAttribute), true)[0]).MaximumLength;
}
below that in your html replace the EditorFor with:
#Html.TextBoxFor(model => model.Extension, htmlAttributes: new {maxlength=max })
I eventually decided I'd rather just do it in script:
<script>
$(function ()
{
var max = $("#myinput").attr("data-val-length-max");
$("#myinput").attr("maxlength", max);
});
</script>
but if you don't want to to add script the first example should work.

My editor templates for an empty list is not rendering

I have a custom editor in my EditorTemplates folder for a IList<PersonRelations>. The Editor has this model:
#model IList<PersonRelation>
and in my entity is as this:
public IList<PersonRelation> Relations { get; set; }
this is how I called it in my view:
<div class="editor-field">
#Html.EditorFor(model => model.Relations)
</div>
and it's rendering the model if Relations is null.
But.. I want to declare my property in this way
private IList<PersonRelation> _relations;
public IList<PersonRelation> Relations
{
get { return _relations ?? (_relations = new List<PersonRelation>()); }
set { _relations = value; }
}
To avoid null references exceptions.
The thing is when the List is not null and has no elements, the editor is not being displayed at all.
In my editor I iterate through the elements but also I render another controls outside the loop, and I can't see any elements.
I'm missing something?
Solved.
When I changed the property, I forgot to decorate it with [UIHint("PersonRelations")]
which was in the original form of the property (my custom editor's file name is "PersonRelations.cshtml")
This is needed due it seems that the engine is not able to infer the editor for a collection, even when you have one, so you explicitly have to tell which one you want to use.

How to have a conditional checked, disabled, ... with the html helper?

For a view, I've to generate some checkbox.
I've one collection of items:
public class ItemSelection
{
public int Id { get; set; }
public String Name { get; set; }
public Boolean IsSelected { get; set; }
public Boolean IsActive { get; set; }
}
and in the view, I'm iterating on this
#foreach(ItemSelection item in Model.Items){
Html.CheckBoxFor(m=>item.IsSelected)//HERE I WOULD LIKE TO HAVE DISABLED properties if I've a IsActive=falsel
Html.HiddenFor(m=>item.Id)
}
Now I see that I can do a "if" in which I create a different HtmlAttribute array, depending of this property, but is there a way to create only one array
new {disabled=item.IsActive?"ONE_SPECIAL_VALUE_HERE":"disabled"}
I tried to put false, or some other things, nothing worked.
You can't avoid the if:
The problem is with the special nature of the disabled attribute because there is no "special" value which would make your sample work, because:
"disabled" is only possible value for this attribute. If the input
should be enabled, simply omit the attribute entirely.
So you need to omit the attribute to enable the control but all HTML helpers will serialize all the properties of the anonymous objects passed in as the htmlattributes. And there is no way to conditionally add properties to anonymous types.
However if you have multiple common attributes for the enable/disable case, and you don't want to create two anonymoues types, you can put the attributes in a dictionary with the optional disabled attribute and use the dictionary as the htmlAttributes:
var checkboxHtmlAttributes = new Dictionary<string, object>
{{"attibute1", "value1"},
{"attribute2", "value2"}};
if (!item.IsActive)
{
checkboxHtmlAttributes.Add("disabled", "disabled");
}
#Html.CheckBoxFor(m=>item.IsSelected, checkboxHtmlAttributes)