ASP.NET Core - Data values not bound when trying to use a partial view to lay out form controls - asp.net-core

I'm trying to create a partial view to save on the amount of boilerplate code I need when creating forms. A simple version of what I have is as follows...
#model FormRowViewModel
<div class="form-group row">
<label for="#Model.PropertyName" class="col-lg-2 col-form-label">#Model.Label</label>
<div class="col-lg-10 input-group">
<input type="text" asp-for="#Model.PropertyName" class="form-control">
</div>
</div>
...where FormRowViewModel looks like this...
public class FormRowViewModel {
public FormRowViewModel(string propertyName, string label) {
PropertyName = propertyName;
Label = label;
}
public string PropertyName { get; set; }
public string Label { get; set; }
}
The idea is to use it in a view like this...
#model ContactViewModel
<form method="post" asp-controller="Home" asp-action="Index">
<div asp-validation-summary="All" class="text-danger"></div>
#await Html.PartialAsync("_FormRow", new FormRowViewModel("UserName", "Your name"))
#await Html.PartialAsync("_FormRow", new FormRowViewModel("Email", "Email"))
#await Html.PartialAsync("_FormRow", new FormRowViewModel("Telephone", "Telephone"))
<div class="form-group row">
<div class="offset-sm-2 col-lg-10">
<button type="submit" class="btn btn-primarySubmit</button>
</div>
</div>
</form>
This works, in that it creates the HTML (almost) as expected, but has two problems...
1) The generated HTML includes value attributes that set the content of the textboxes to the property names...
<input type="text" class="form-control" id="PropertyName"
name="PropertyName" value="UserName">
2) Whatever I put in the textboxes, when the form is posted back to the server, the view model properties are all empty strings. Even the property names that were added don't come through.
In case it helps, here is the controller action that handles the view...
public IActionResult Index() =>
View();
[HttpPost]
public IActionResult Index(ContactViewModel vm) {
if (!ModelState.IsValid) {
return View(vm);
}
// Next line added so I can see when it worked
return RedirectToAction(nameof(Privacy));
}
Anyone know what I'm doing wrong? Thanks

Your whole approach here is incorrect. It seems what you're looking for is editor templates. Essentially, you need to create partial views in Views\Shared\EditorTemplates that correspond with types or members of the DataType enum, and add your custom HTML there. For example, you can create a String.cshtml view:
#model string
<div class="form-group row">
<label asp-for="#Model" class="col-lg-2 col-form-label"></label>
<div class="col-lg-10 input-group">
<input asp-for="#Model" class="form-control">
</div>
</div>
Then, for any string property:
#Html.EditorFor(x => x.MyStringProp)
And your custom template will be used, with the proper name binding.
Alternatively, you can create custom taghelpers, but the methodology for that is a bit more complicated, since you'll need to handle the HTML generation in code. If you're interested in that approach, though, look at the source for the built-in tag helpers and create your own based on that.

Related

Am I going about this the wrong way? What would be the ideal way to do this?

I have a .Net 5 Blazor web app, two date pickers, and a table generating results from the date pickers. I can't seem to get the server to re-crunch the numbers and re-generate the table with input from the date pickers. Is a Blazor server app appropriate to use for this? I didn't want the massive number crunching going on in the user's browser, so I opted for Server.
<div class="row">
<div class="col-xl-2">
<label for="start">Start date:</label>
<input type="date" id="start" name="range-start"
value=#startDate
min=#minDate max=#maxDate>
</div>
<div class="col-xl-2">
<label for="end">End date:</label>
<input type="date" id="end" name="range-end"
value=#endDate
min=#minDate max=#maxDate>
</div>
<div class="col-xl-2">
<button class="btn btn-primary" type="submit" id="fetch" name="fetch-data" #onclick="GetDataAsync">Fetch data</button>
</div>
.razor #code Portion
protected async Task GetDataAsync()
{
rd = null;
DateTime start, end;
start = new DateTime(Convert.ToDateTime(startDate).Year, Convert.ToDateTime(startDate).Month, Convert.ToDateTime(startDate).Day);
end = new DateTime(Convert.ToDateTime(endDate).Year, Convert.ToDateTime(endDate).Month, Convert.ToDateTime(endDate).Day);
rd = await DataService1.GetDataAsync(start, end);
}
Some things to note from these excerpts:
The service is injected
rd is the data source feeding the Table
The service is called on page load and generates a default data set
Clicking the "fetch data" button doesn't seem to pass back the values from the inputs
Repeatedly clicking the button after the first time doesn't seem to trigger the callback/event at all
Clearly I'm new at this and could use some guidance, reading suggestions, example code. Any help would be appreciated. Thank you in advance.
I figured it out. The value of the user controls needs to have an #bind- appended to it, and the variables need a public property {get; set;} with an [Attribute] decorator instead of a private variable.
It looks like this when correct:
<div class="row">
<div class="col-xl-2">
<label for="start">Start date:</label>
<input type="date" id="start" name="range-start"
#bind-value=#startDate
min=#minDate max=#maxDate>
</div>
<div class="col-xl-2">
<label for="end">End date:</label>
<input type="date" id="end" name="range-end"
#bind-value=#endDate
min=#minDate max=#maxDate>
</div>
<div class="col-xl-2">
<button class="btn btn-primary" type="submit" id="fetch" name="fetch-data" #onclick="GetDataAsync">Fetch data</button>
</div>
.razor #code Portion
[Parameter]
public DateTime startDate { get; set; }
[Parameter]
public DateTime endDate { get; set; }
protected async Task GetDataAsync()
{
rd = null;
DateTime start, end;
start = new DateTime(Convert.ToDateTime(startDate).Year, Convert.ToDateTime(startDate).Month, Convert.ToDateTime(startDate).Day);
end = new DateTime(Convert.ToDateTime(endDate).Year, Convert.ToDateTime(endDate).Month, Convert.ToDateTime(endDate).Day);
rd = await DataService1.GetDataAsync(start, end);
}

Setting properties using asp-for in MVC Core App with EF Core?

As I understand it from these docs: https://learn.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-3.1 , asp-for is used to transfer values from input elements to backend C# class properties, for example:
<input type="text" id="wsite" name="wsite" maxlength="11" asp-for="WebsiteName">
Along with '#folderName ClassName;' at the top, lets you transfer to this example property:
public string WebsiteName { get; set; }
However, testing this out with console.WriteLine show that the property is still null after the form containing the input has been submitted. Any idea what I'm missing?
Edit: Updated to show my property name and asp-for value match, and to add my controller:
[HttpPost]
public IActionResult Post()
{
DBCRUD.Initialize(_context);
return NoContent();
}
The asp-for tag should match the variable-name.
Try defining your html-form like:
#model Classname
<form asp-action="ActionName" asp-controller="ControllerName" ...>
<input type="text" asp-for="VarName">
and your controller:
public MyReturnVariable ActionName(ClassName class) {
Console.WriteLine(class.VarName);
}
The Tag Helpers is used with Model binding and creating and rendering HTML elements(display the model properties) in the web page.
So, in the Web page (or view page), at the top of the header, we should add the following code to assign the model.
#model MVCSample.Models.BookModel
Then, using the following code to display the properties:
<div class="row">
<div class="col-md-4">
<form asp-action="AddBook">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="ID" class="control-label"></label>
<input asp-for="ID" class="form-control" />
<span asp-validation-for="ID" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="bookName" class="control-label"></label>
<input asp-for="bookName" class="form-control" />
<span asp-validation-for="bookName" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
Code in the controller:
[HttpGet]
public IActionResult AddBook()
{
BookModel book = new BookModel()
{
ID = 1001,
bookName = "War and Peace",
Title = "War and Peace"
};
return View(book);
}
Code in the model:
public class BookModel
{
public int ID { get; set; }
public string bookName { get; set; }
public string Title { get; set; }
}
More details information, you could check the Model Binding.

How can I make a fancy checkbox template for ASP.NET Core?

I've got a lot of booleans in my model, and we're using Bootstrap, so for every boolean property I'm copy/paste refactoring:
<div class="form-group">
<div class="custom-control custom-checkbox ">
<input asp-for="IsFoo"/>
<label asp-for="IsFoo"></label>
</div>
</div>
... but that's dumb. I tried adding this to Views/Shared/EditorTemplates/bool.cshtml:
#model bool?
<div class="form-group">
<div class="custom-control custom-checkbox ">
<input asp-for="#Model"/>
<label asp-for="#ViewData.TemplateInfo.FormattedModelValue"></label>
</div>
</div>
... and calling it with #Html.EditorFor(m => m.IsFoo) but all I'm getting back is a plain input element from the default template.
what am I doing wrong here name the template 'boolean.cshtml'
is ViewData.TemplateInfo.FormattedValue the right value to get the Display(Name="xxx") Attribute from the property nope. ViewData.ModelMetadata.DisplayName
is there some new & improved version instead of Editor Templates in ASP.NET Core that I should be using (like Tag Helpers?) instead of the "old" way, and if so, how do I go about it?
Use the <partial> tag-helper:
<partial name="MyCheckbox" for="IsFoo" />
It works with binding properties too:
class MyModel
{
public List<MyCheckboxModel> MyCheckboxList { get; set; }
}
class MyCheckboxModel
{
public Boolean IsChecked { get; set; }
}
#for( Int32 i = 0; i < this.Model.MyCheckboxList.Count; i++ )
{
<partial name="MyCheckbox" for="MyCheckboxList[i]"
}
Change your partial-view to:
#model MyCheckboxModel
<div class="form-group">
<div class="custom-control custom-checkbox">
<input asp-for="#Model"/>
<label asp-for="#Model"></label>
</div>
</div>
The for="" attribute causes the name/id/binding context in the partial to match the named property, so ASP.NET will do the magic to ensure that <input asp-for="#Model" /> will correspond to Model.MyCheckBoxList[0] and so on.

How to prevent immediate trigger jQuery validation?

There is some ViewModel:
class MyViewModel
{
[Required(ErrorMessage = "Field {0} is required")]
public string Email { get; set; }
}
I use jquery validation for front-end:
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validate/1.16.0/jquery.validate.min.js">
</script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.validation.unobtrusive/3.2.6/jquery.validate.unobtrusive.min.js">
</script>
The fragment of Razor markup:
<form asp-controller="Account" asp-action="Register" role="form">
<div class="form-group">
<div asp-validation-summary="All" class="text-danger"></div>
</div>
<div class="form-group">
<label asp-for="Email"></label>
<input asp-for="Email" class="form-control" aria-describedby="email" />
<span asp-validation-for="Email" class="text-danger"></span>
</div>
</form>
The issue is validation is triggered immediately when user get the html page. So one sees error for email field when she inputs nothing yet (Field Email is required). How can I prevent this behavior (triggered on submit)?
There is action:
public IActionResult SomeAction(MyViewModel model = null)
{
return View(model);
}
i.e. controller pass to action null model (value by default). It is the reason of that behavior of jquery validation

Why is the Bind attribute seemingly breaking my model binding of nested objects?

Could someone help me resolve this issue. I'm trying to limit over posting with bind param action but it seems that it doesn't work at all. When I removed the Bind keyword, everything started to work as a charm.
Here is the code sample:
View Model:
public class ProductCreateViewModel
{
public Product Product { get; set; }
public ICollection<IFormFile> Images { get; set; }
}
Action:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Product.Id,Product.CategoryId,Product.Description,Product.Title")] ProductCreateViewModel productVM)
{
if (ModelState.IsValid)
{
_context.Add(productVM.Product);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
ViewData["CategoryId"] = new SelectList(_context.Categories.Include(c => c.Categories).Where(c => c.ParentCategoryId == null), "Id", "Name", productVM.Product.CategoryId);
return View(productVM);
}
View:
#model CatalogWebApp.Models.ProductsViewModels.ProductCreateViewModel
#{
ViewData["Title"] = "Add Product";
ViewData["BigPageTitle"] = "Products";
ViewData["PageBoxTitle"] = "Add New Product";
}
<form asp-action="Create">
<div class="form-horizontal">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Product.CategoryId" class="col-md-2 control-label"></label>
<div class="col-md-10">
<select name="Product.CategoryId" class ="form-control">
#foreach(Category item in (ViewBag.CategoryId as SelectList).Items)
{
<option value="#item.Id">#item.Name</option>
if (item.Categories != null && item.Categories.Count > 0)
{
foreach (var subCat in item.Categories)
{
<option value="#subCat.Id">--#subCat.Name</option>
}
}
}
</select>
</div>
</div>
<div class="form-group">
<label asp-for="Product.Description" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Product.Description" class="form-control" />
<span asp-validation-for="Product.Description" class="text-danger" />
</div>
</div>
<div class="form-group">
<label asp-for="Product.Title" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Product.Title" class="form-control" />
<span asp-validation-for="Product.Title" class="text-danger" />
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Could someone pelase indicate if I have a problem or it is only a known asp.net core issue?
I'm not quite sure why you using Bind for your case.
Just create sepatate ViewModel with only properties you need like ProductCreateStort.
Then use this ViewModel in your controller signature and inherit your main model from it.
This way you won't mess with Bind and limit your params on POST
While I'm fairly new to ASP.NET Core myself (and coming to this question 7 months late), I ran into this same issue. I think the key here is that you have to bind "Product" for it to be considered. Binding "Product.Id" by itself doesn't appear to be good enough. So this should work:
[Bind("Product,Product.Id,Product.CategoryId,Product.Description,Product.Title")]
Of course, Hamid Mosalla's comment is a better option if ALL of your bound properties are on the nested object (which leads to wonder why you need a view model in the first place). In my case, I have a nested object AND a local property, so using the "Prefix" solution wasn't the right thing to do.
Anyway, hope this helps someone.
You need to pass your values as params string[], not as a single string separated by commas:
[Bind("Product.Id","Product.CategoryId","Product.Description","Product.Title")]
See Source