Editor method of HtmlHelper class doesn't work with collections - asp.net-core

In my view I have a model with a property Details of type List. The collection has 3 elements. Now I need to edit this list in a view.
If I use Html.EditorFor method passing the expression everything works correctly, But if I use Html.Editor method, the binding fails. By "fails" I mean that MVC uses the string editor for all fields (even if they are numbers) passing null as a model.
// this works correctly
#for (var i = 0; i < Model.Details.Count; i++)
{
<li>
#Html.EditorFor(m => m.Details[i].Name)
#Html.EditorFor(m => m.Details[i].Age)
</li>
}
// this doesn't work
#for (var i = 0; i < Model.Details.Count; i++)
{
<li>
#Html.Editor("Details[" + i +"].Name")
#Html.Editor("Details[" + i +"].Age")
</li>
}
I'm using ASP.NET Core 3.0 and didn't test this code against previous versions. For several reasons, I cannot use the EditorFor method so I'm stuck with this problem.
Any ideas?

Editor() HTML Helper method is for simple type view and EditorFor() HTML Helper method is for strongly type view to generate HTML elements based on the data type of the model object’s property.
The definition of Html.Editor:
// Summary:
// Returns HTML markup for the expression, using an editor template. The template
// is found using the expression's Microsoft.AspNetCore.Mvc.ModelBinding.ModelMetadata.
//
// Parameters:
// htmlHelper:
// The Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper instance this method extends.
//
// expression:
// Expression name, relative to the current model. May identify a single property
// or an System.Object that contains the properties to edit.
//
// Returns:
// A new Microsoft.AspNetCore.Html.IHtmlContent containing the <input> element(s).
//
// Remarks:
// For example the default System.Object editor template includes <label> and <input>
// elements for each property in the expression's value.
// Example expressions include string.Empty which identifies the current model and
// "prop" which identifies the current model's "prop" property.
// Custom templates are found under a EditorTemplates folder. The folder name is
// case-sensitive on case-sensitive file systems.
public static IHtmlContent Editor(this IHtmlHelper htmlHelper, string expression);
You could identify a single property for the expression of Editor Tag Helper like below :
#model MVC3_0.Models.Detail
<table>
<tr>
<td>Id</td>
<td>#Html.Editor("Id")</td>
</tr>
<tr>
<td>Name</td>
<td>#Html.Editor("Name")</td>
</tr>
<tr>
<td>Age</td>
<td>#Html.Editor("Age")</td>
</tr>
</table>
public IActionResult Index()
{
var model = new Detail { Id = 1, Name = "jack", Age = 12 };
return View(model);
}
Result:
There is a workaround that you could use TextBox or input instead
#for (var i = 0; i < Model.Details.Count; i++)
{
<li>
#Html.TextBox("Details[" + i + "].Name", Model.Details[i].Name, new { htmlAttributes = new { #class = "text-field" } })
#Html.TextBox("Details[" + i + "].Age", Model.Details[i].Age, new { htmlAttributes = new { #class = "text-field" } })
</li>
}
// input tag helper
#for (var i = 0; i < Model.Details.Count; i++)
{
<li>
<input asp-for="#Model.Details[i].Name" />
<input asp-for="#Model.Details[i].Age" />
</li>
}

Related

Property values of the same type are updated incorrectly in Blazor

Background:
I wanted to achieve the following:
Keep a copy of the data context and use the copy for editing
So that I can reset the data context back to its unchanged state using an onclick event by doing copyValue = unchangedValue
Here is my attempt (it's been trimmed down in size to reduce noises but it has the same issue):
**index.razor**
#page "/"
#using SolutionName.Data
#using System.Reflection
<EditForm Model="Items2">
<table class="table">
<thead>
<tr>
<th>Summary</th>
</tr>
</thead>
<tbody>
#foreach (var i in Items2)
{
<tr #key="#i.GetHashCode()">
<InputText #bind-Value="i.Summary"></InputText>
</tr>
}
</tbody>
</table>
</EditForm>
//
//reflections for debuggings
//
#if (Items != null)
{
<p>
#foreach (var item in Items)
{
<span>#($"Items.{typeof(WeatherForecast).GetProperty(nameof(WeatherForecast.Summary)).Name}={typeof(WeatherForecast).GetProperty(nameof(WeatherForecast.Summary)).GetValue(item)}")</span>
}
</p>
}
#if (Items2 != null)
{
<p>
#foreach (var item in Items2)
{
<span>#($"Items2.{typeof(WeatherForecast).GetProperty(nameof(WeatherForecast.Summary)).Name}={typeof(WeatherForecast).GetProperty(nameof(WeatherForecast.Summary)).GetValue(item)}")</span>
}
</p>
}
#code{
List<WeatherForecast> Items = new List<WeatherForecast>();
List<WeatherForecast> Items2 = new List<WeatherForecast>();
protected override void OnInitialized()
{
Items = new List<WeatherForecast>()
{
new WeatherForecast()
{
Date = DateTime.Now,
Summary = "123",
TemperatureC = 1
}
};
Items2 = Items;
}
private void ResetItems2()
{
Items2 = Items;
}
}
As you can see, I am binding Items2, and not Items, to the <EditForm>.
However, updating the summary seems to update both Items2 and Items. I also noticed that this will not happen if Items and Items2 are of two different types (say that they have exactly the same properties, and I cast one to another...)
Two questions:
Why is Item updated in this case?
Is there a way to only update Items2 and not Items, while allowing Items and Items2 to be the same type?
Detailed steps to reproduce the issue:
Step 1. Initialized and render for the first time
Step 2. Change the value to 456 and then tab away
The expected result should be
Items.Summary=123 (not 456)
Items2.Summary=456
The issue is that you're using reference type assignment. When you assign Items to Items2, you actually assign a pointer to Itemss values. Both variable point to the same list of objects.
If it's applicable create a value type instead. Saving data in the local storage and then retrieving it is a viable solution.
This:
List<WeatherForecast> Items = new List<WeatherForecast>();
List<WeatherForecast> Items2 = new List<WeatherForecast>();
is superfluous. Code like this:
List<WeatherForecast> Items;
List<WeatherForecast> Items2;

Error when converting List to IEnumerable [duplicate]

This question already has answers here:
The model item passed into the dictionary is of type .. but this dictionary requires a model item of type
(7 answers)
Closed 5 years ago.
I am trying to display the default index page for a model. But I get the below error.
The model item passed into the dictionary is of type
'System.Collections.Generic.List 1[System.Boolean]', but this
dictionary requires a model item of type
'System.Collections.Generic.IEnumerable`1[EDIWebsite.Models.Error_Details]'.
Controller
public ActionResult FindRelatedBols(string bolnumber)
{
if (bolnumber == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
var error_Summaries = db.Error_Details.Select(r => r.BOL == bolnumber);
if (error_Summaries == null)
{
return HttpNotFound();
}
return PartialView("~/Views/Error_Details/Index.cshtml",error_Summaries.ToList());
}
View
#model IEnumerable<EDIWebsite.Models.Error_Details>
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<p>
#Html.ActionLink("Create New", "Create")
</p>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.Source_System)
</th>
.
.
.
#Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
#Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
#Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
</td>
</tr>
}
</table>
The error is self explanatory. Your view is strongly typed to a collection of Error_Details objects.Your current code is generating IQueryable<bool> as the type of error_Summaries variable and you are later calling the ToList() on that, which will generate a list of boolean values(List<bool>).
Your view is expecting something(IEnumerable<Error_Details>) and your action method is passing something else(List<bool>), hence getting that type mismatch exception!
You need to pass a collection of Error_Details objects to the view. I assume you wanted to pass a filtered list of items which is has the same BOL value as your bolnumber parameter. You can use the LINQ Where method to do the filtering.
var items = db.Error_Details.Where(a=>a.BOL == bolnumber).ToList();
return PartialView("~/Views/Error_Details/Index.cshtml",items);
Assuming the BOL property on Error_Details class is of string type.

MVC: Access data attributes in controller

Might be a asked and answered...sorry but, I've been searching for a while.
RAZOR VIEW
#foreach (var item in Model.LanguageList)
{
<li>
<a href="#Url.Action("ChangeLanguage", "UserHeader")" data_languageID="#item.LanguageID" data_someval="hello" data_somevalb="world">
<div class="flag #item.LanguageFlag">
</div>
<div class="flag-title"> #item.LanguageName</div>
</a>
</li>
}
How do I access the data attributes in my controller?
CONTROLLER
public ActionResult ChangeLanguage()
{
var x = ControllerContext; //// ??? get the collection of [data-xx] from where?
return RedirectToAction("Buttons", "Designer");
}
data-* attributes are client side values and are not sent in the request.
In order to send those values, add them as route values
#foreach (var item in Model.LanguageList)
{
<li>
<a href="#Url.Action("ChangeLanguage", "UserHeader", new { languageID=item.LanguageID, someval="hello", somevalb="world" })">
<div class="flag #item.LanguageFlag"></div>
<div class="flag-title"> #item.LanguageName</div>
</a>
</li>
}
and include parameters in your GET method for the values
public ActionResult ChangeLanguage(int LanguageID, string someval, string somevalb)
Alternatively you could handle this using javascript/jquery (the ChangeLanguage() method also needs to be modified as shown above)
$('a').click(function() {
// get the url
var url = $(this).attr('href');
// get the data attributes
var languageID = $(this).data('languageID');
var someVal= $(this).data('someval');
var someValB= $(this).data('somevalb');
location.href = url + '?languageID=' + languageID + '&someVal =' + someVal + '&someValB=' + someValB;
return false; // cancel the default redirect
});

MVC4 ViewData.TemplateInfo.HtmlFieldPrefix generates an extra dot

I'm trying to get name of input correct so a collection of objects on my view model can get bound.
#{ViewData.TemplateInfo.HtmlFieldPrefix = listName;}
#Html.EditorFor(m => m, "DoubleTemplate", new {
Name = listName,
Index = i,
Switcher = (YearOfProgram >= i +1)
})
As you can see here, I pass in the "listName" as the prefix for my template, the value of listName = "MyItems".
And here is my template:
#model Web.Models.ListElement
#if (ViewData["Switcher"] != null)
{
var IsVisible = (bool)ViewData["Switcher"];
var index = (int)ViewData["Index"];
var thisName = (string)ViewData["Name"] + "[" + index + "].Value";
var thisId = (string)ViewData["Name"] + "_" + index + "__Value";
if (IsVisible)
{
#*<input type="text" value="#Model.Value" name="#thisName" id ="#thisId" class="cell#(index + 1)"/>*#
#Html.TextBoxFor(m => m.Value, new { #class ="cell" + (index + 1)})
#Html.ValidationMessageFor(m => m.Value)
}
}
but I found that the generated name becomes this: MyItems.[0].Value
It has one extra dot. How can I get rid of it?
Incidentally, I tried to manually specify the name inside the template and found the name gets overridden by the Html helper.
Update
The reason why I have to manually set the HtmlFieldPrefix is the property name (MyItems which is a list of objects) will get lost when MyItems is passed from main view to the partial view. By the time, the partial view called my template and passed in one object in MyItems, the template itself has no way to figure out the name of MyItems as it has been lost since the last "pass-in".
So that's why I have to manually set the html field prefix name. And I even tried to use something similar to reflection(but not reelection, I forgot the name) to check the name of passed in object and found it returned "Model".
Update 2
I tried Stephen's approach, and cannot find the html helper PartialFor().
I even tried to use this in my main view:
Html.Partial(Model, "_MyPartialView");
In Partial View:
#model MvcApplication1.Models.MyModel
<h2>My Partial View</h2>
#Html.EditorFor(m => m.MyProperty)
Here is my templat:
#model MvcApplication1.Models.ListElement
#Html.TextBoxFor(m => m.Value)
Here is my Model:
public class MyModel
{
private List<ListElement> myProperty;
public List<ListElement> MyProperty
{
get
{
if (myProperty == null)
{
this.myProperty = new List<ListElement>() { new ListElement() { Value = 12 }, new ListElement() { Value = 13 }, new ListElement() { Value = 14 }, new ListElement() { Value = 15 }, };
}
return this.myProperty;
}
set
{
this.myProperty = value;
}
}
}
public class ListElement
{
[Range(0, 999)]
public double Value { get; set; }
}
And here is my controller:
public ActionResult MyAction()
{
return View(new MyModel());
}
It only generates raw text("12131415") for me, instead of the wanted text box filled in with 12 13 14 15.
But if I specified the template name, it then throws an exception saying:
The template view expecting ListElement, and cannot convert
List<ListElement> into ListElement.
There is no need set the HtmlFieldPrefix value. MVC will correctly name the elements if you use an EditorTemplate based on the property type (and without the template name).
Assumed models
public class ListElement
{
public string Value { get; set; }
....
}
public class MyViewModel
{
public IEnumerable<ListElement> MyItems { get; set; }
....
}
Editor template (ListElement.cshtml)
#model YourAssembly.ListElement
#Html.TextBoxFor(m => m.Value)
Main view
#model YourAssembly.MyViewModel
...
#Html.EditorFor(m => m.MyItems) // note do not specify the template name
This will render
<input type="text" name="MyItems[0].Value" ...>
<input type="text" name="MyItems[1].Value" ...>
....
If you want to do this using a partial, you just can pass the whole model to the partial
MyPartial.cshtml
#model #model YourAssembly.MyViewModel
#Html.EditorFor(m => m.MyItems)
and in the main view
#Html.Partial("MyPartial")
or create an extension method
public static MvcHtmlString PartialFor<TModel, TProperty>(this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression, string partialViewName)
{
string name = ExpressionHelper.GetExpressionText(expression);
object model = ModelMetadata.FromLambdaExpression(expression, helper.ViewData).Model;
var viewData = new ViewDataDictionary(helper.ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo { HtmlFieldPrefix = name }
};
return helper.Partial(partialViewName, model, viewData);
}
}
and use as
#Html.PartialFor(m => m.MyItems, "MyPartial")
and in the partial
#model IEnumerable<YourAssembly.ListElement>
#Html.EditorFor(m => m)
Call your partial this way:
#Html.Partial("_SeatTypePrices", Model.SeatTypePrices, new ViewDataDictionary
{
TemplateInfo = new TemplateInfo() {HtmlFieldPrefix = nameof(Model.SeatTypePrices)}
})
Partial view:
#model List
#Html.EditorForModel()
Editor template implementation:
#using Cinema.Web.Helpers
#model Cinema.DataAccess.SectorTypePrice
#Html.TextBoxFor(x => Model.Price)
This way your partial view will contain list of items with prefixes.
And then call EditorForModel() from your EditorTemplates folder.
I found that I can change the value of HtmlFeildPrefix in my template.
So what I did to solve my problem was just to assign the correct value to HtmlFeildPrefix in the template directly rather than in the page which calls the template.
I hope it helps.
If I want to pass the HtmlFieldPrefix I use the following construct:
<div id="_indexmeetpunttoewijzingen">
#Html.EditorFor(model => model.MyItems, new ViewDataDictionary()
{
TemplateInfo = new TemplateInfo()
{
HtmlFieldPrefix = "MyItems"
}
})
</div>

Url.Action is how to reformat URL

I am creating an MVC4 application.
In my contract controller overview page i have an Url.Action
int teller = 0;
foreach (var item in Model)
{
<a href="#Url.Action("Details", "Contract",new { id = teller })">
<tr>
<td>#Html.DisplayFor(modelItem => item.ContractMSFNo)</td>
<td>#Html.DisplayFor(modelItem => item.StageCode)</td>
<td>#Html.DisplayFor(modelItem => item.ValidFromView)</td>
<td>#Html.DisplayFor(modelItem => item.ValidToView)</td>
</tr>
</a>
teller++;
}
I need to pass the id. I am using id in the ActionLink details in Contract Controller
my controller is
public ActionResult Details(int id)
{
//code
return View(contract);
}
When i click on the link Url generated is
http://localhost:4826/Contract/Details/0
/0 is the id
i want my Url to be http://localhost:4826/Contract/Details
i know this can be acheived thru Html.Actionlink but it is my compulsion to use Url.Action. Can it be acheived with Url.Action
It can't be done by routing or ActionLink. But you may try to use session.
1) Add to your controller new method to save your id to session:
public JsonResult Save(int id)
{
Session["ID"] = id;
return Json("Success");
}
2) Add jQuery method to save data in session from View and delete parameter from Url.Action:
<a class="mylink" href="#Url.Action("Details", "Contract")"></a>
<script>
$(".mylink").click(function(){
var data = { id : teller}; //**teller is from your example
$.get("#Url.Action("Details", "Contract")", data)
});
</script>
3) Change your Details ActionResult to get id from session:
public ActionResult Details()
{
var id = (int)Session["ID"];
//code
return View(contract);
}
P.S: Ask your client, how he expects to give sombody external links. It will be impossible if url doesn't have a parameter. And it is very bad for SEO.
If you want your URL without the id parameter, simply don't pass it to the Url.Action() method, as follows:
#Url.Action("Details", "Contract")
If you add like {id=teller} then route automatically add id parameters end of the link. If you don't need id parameters for this url you need to remove
new { id = teller }
Final version like this
#Url.Action("Details", "Contract")
OK, reading this comment: "no actually there are many ids ... code is foreach (var item in Model) { ", I am not sure I understand what you really want to achieve. You are passing a parameter to the view, which can have only one value. Are you sure that you are not looking for something like:
foreach (var item in Model)
{
<a href="#Url.Action("Details", "Contract",#item.ID>
...
}
instead? The fact the ID is visible or not in the URL seems to be another problem, no ?