Post complex model to controller MVC using Facebook application template C# - facebook

I have following model:
class HomePageModel
{
public User user { get; set; }
public ResultHistory resultHistory { get; set; }
public Option option { get; set; }
public HomePageModel()
{
}
}
Code from controller when passing it into view:
[FacebookAuthorize]
public async Task<ActionResult> Index(Context context)
{
ViewBag.AppUrl = GlobalFacebookConfiguration.Configuration.AppUrl;
if (ModelState.IsValid)
{
var user = await context.Client.GetCurrentUserAsync<MyAppUser>();
var option = new Option();
//CODE for reading user from db, simpified
Users dbUser = db.Get(context);
var resultHistory = new ResultHistory(dbUser.NSPGW, dbUser.NSPGL,
dbUser.NMPGW,dbUser.NMPGL,dbUser.NDPGW, dbUser.NDPGL);
HomepageModel homepageModel = new HomepageModel();
homepageModel.option = option;
homepageModel.resultHistory = resultHistory;
homepageModel.user = user;
return View(homepageModel);
}
}
Code from controller
#using (Html.BeginForm("Start", "Home", FormMethod.Post, new { id="opcijeForm"}))
{
<div class="row">
<div class="col-md-4">
<div class="radio">
<label>
<input type="radio" name="radio" value="sp" />SP
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="radio" value="mp" />MP
</label>
</div>
<div>
<input type="submit" value="start" />
</div>
</div>
<div class="col-md-8">
<div id="spoption">
#Html.DropDownListFor(m => m.option.sl, Model.option.slSelectList)
<label style="padding-left:10px">
<input type="checkbox" id="bb" /> BB
</label>
#Html.DropDownListFor(m => m.option.swt, Model.option.swtSelectList, new {id="bbdd" })
<label style="padding-left:10px">
<input type="checkbox" id="ziv" /> ziv
</label>
#* #Html.DropDownListFor(m => m.gameOption.sln, Model.option.slnSelectList, new { id="zivdd"})*#
</div>
<div id="mpoption">
<label>Numer of oppointment</label>
#Html.DropDownListFor(m=>m.option.spn, Model.option.spnSelectList)
#Html.DropDownListFor(m=>m.option.smv, Model.option.smvSelectList)
</div>
</div>
</div>
}
and receiving model on post
[FacebookAuthorize]
[HttpPost]
public ActionResult Start(HomepageModel model)
{
//Some code
//here is for example model.option null
}
When I submit form with above model, in my controller action where I receive same model, all it's properties are null.Each class that is within HomePage has only string and int as properties. If I move something, for example from class Option and put it in HomePageModel direct, then properties I have moved are not null. Is there a way to post model as above, or should I just copy properties from these three class into HomePageModel class?
[EDIT]
It is because of meta-data FacebookAuthorize, when I remove it everything is working
MVC app facebook passing object from view to controller at submit

Related

How can I bind form fields to a nested model on post?

I am coding a solution where the user will submit a form, posting the values back to my ASP.NET MVC controller. My model is complex and the form fields are contained in a nested object (I'm using CQRS via MediatR). When I submit the form, the values come across as null. How can I get the complex model to recognize the form fields?
Here is my code:
Controller:
[HttpPost]
[Route("edit")]
public async Task<IActionResult> Edit(UpdateApplicationCommand command)
{
await _mediator.Send(command)
.ConfigureAwait(false);
return RedirectToAction("Index");
}
Models:
public class UpdateApplicationCommand : IRequest<Unit>
{
public ApplicationEditGeneralViewModel ApplicationEditGeneralViewModel { get; set; } = null!;
}
public class ApplicationEditGeneralViewModel
{
[Required]
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
View:
#model ApplicationEditGeneralViewModel
<form method="post" asp-action="Edit" asp-controller="Applications">
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.Name, new { #class = "form-control", placeholder = "Application Name"})
<label for="Name">Application Name</label>
</div>
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.Description, new { #class = "form-control", placeholder = "Application Description"})
<label for="Description">Application Description</label>
</div>
<div class="d-flex flex-row-reverse bd-highlight">
<input type="submit" value="Submit" class="btn btn-primary mt-2" />
</div>
</form>
I've tried to reduce the complex model to its fields, by placing the contents of the ApplicationEditGeneralViewModel directly into the UpdateApplicationCommand class. This worked, but I'd really like to keep the nested structure so that I can reuse the ApplicationEditGeneralViewModel object.
I saw this solution here:
How to bind nested model in partial view
But I'd rather avoid adding the name as a route object (if possible) for every form field. Is there another, more simple way that I can do this?
The first way, you can custom model binding like below:
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var model = new UpdateApplicationCommand()
{
ApplicationEditGeneralViewModel = new ApplicationEditGeneralViewModel()
{
Description = bindingContext.ValueProvider.GetValue("Description").ToString(),
Name = bindingContext.ValueProvider.GetValue("Name").ToString()
}
};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Apply the custom model binding like below:
[HttpPost]
public async Task<IActionResult> Edit([ModelBinder(typeof(CustomModelBinder))]UpdateApplicationCommand model)
{
//.....
}
The second way, just change your razor view like below:
#model UpdateApplicationCommand
<form method="post">
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Name, new { #class = "form-control", placeholder = "Application Name"})
<label for="Name">Application Name</label>
</div>
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Description, new { #class = "form-control", placeholder = "Application Description"})
<label for="Description">Application Description</label>
</div>
<div class="d-flex flex-row-reverse bd-highlight">
<input type="submit" value="Submit" class="btn btn-primary mt-2" />
</div>
</form>

How to submit a form multiple times (asp.net core razor)

I am trying to create a form that can be submitted multiple times with different information, while retaining a common value in one field.
I have a list view from a SQL table in ASP.NET Core Razor that is a list of construction projects. For each row in the list I have a link that goes to a "create" template page where users can create a bid entry for the project which is stored in a different table. The Project Number is assigned to a route value (asp-route-Number = "the project number from the previous list")and populates a hidden field in the "create new bid" form.
Using the default code for the razor page, everything works great. You click submit and are taken back to the list of projects.
What I want to do is have another option on the "create new bid" form that will allow you to save and enter another bid for the same project. I created another button and handler to do this but I am stuck on actually implementing it. If I use return Page() the form posts and the page is returned with route data intact, but the text fields still contain the previous data and the drop-down list is empty. If I use return RedirectToPage(CreateNewBid, Route data) the form posts but the route data does not seem to be passed along and creates a null value error.
This is the link from the Projects list (inside the foreach table), which takes you to the "Create Bid" form and works fine.
<a asp-page="CreateBid" asp-route-Number="#item.ProjectNumber" asp-route-opwid="#item.Id">New Bid</a>
The Create Bid form has the following to submit and create another entry
int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
And the handler:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == num, opwid == idnum));
}
I have also tried several things in the route parameters (as opposed to using the variables) in the "Redirect to Page" and nothing seems to work.
Is there an easier way, or am I just missing something?
This is the cshtml file:
#page
#model Authorization_AD.Pages.GenSvc.BidEntry.CreateBidModel
#{
ViewData["Title"] = "CreateBid";
}
#{ int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
}
<h1>Create Bid</h1>
<h4>OPW number #num</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<input asp-for="OpwBids.OpwProject" value="#idnum" hidden class="form-control" />
</div>
<div class="form-group">
<label asp-for="OpwBids.OpeningDate" class="control-label"></label>
<input asp-for="OpwBids.OpeningDate" class="form-control" />
<span asp-validation-for="OpwBids.OpeningDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OpwBids.Contractor" class="control-label"></label>
<select asp-for="OpwBids.Contractor" class="form-control" asp-items="ViewBag.Contractor">
<option disabled selected>--- SELECT ---</option>
</select>
</div>
<div class="form-group">
<label asp-for="OpwBids.BidAmount" class="control-label"></label>
<input asp-for="OpwBids.BidAmount" class="form-control" />
<span asp-validation-for="OpwBids.BidAmount" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
<input type="submit" value="Save and return to list" asp-page-handler="Done" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
This is the C# file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Authorization_AD.Models;
namespace Authorization_AD.Pages.GenSvc.BidEntry
{
public class CreateBidModel : PageModel
{
private readonly Authorization_AD.Models.OPWContext _context;
public CreateBidModel(Authorization_AD.Models.OPWContext context)
{
_context = context;
}
public IActionResult OnGet()
{
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ViewData["OpwProject"] = new SelectList(_context.MainProjectsListing, "Id", "ProjectNumber");
return Page();
}
[BindProperty]
public OpwBids OpwBids { get; set; }
public async Task<IActionResult> OnPostDoneAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
You can add a property to your page that will be used to bind the value of the clicked button.
public class CreateBidModel : PageModel {
//...
//Add this property to your page.
[BindProperty]
public string Button {get;set;}
public void OnGet(int number,string opwid){
//Set the number and opwid to the target properties
}
public Task<IActionResult> OnPostAsync(){
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
if(Button == "finish"){
return RedirectToPage("./Index");
}
else {
return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
To the view you need to add two buttons that have the same name and that value will be mapped to the Button property.
<form method="post">
... Other content goes here
<button name="#Html.NameFor(m => m.Button)" value="another">Create another</button>
<button name="#Html.NameFor(m => m.Button)" value="finish">Finish</button>
</form>
The value of the clicked button will be parsed to the Button property of the Pagemodel. Based on the value you can decide how to further handle the response of the request (Finish / Create another one in your case).
Thanks for everyone's help. I got it to do what I want by adding the following to the "OnPostAnotherAsync" task:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ModelState.SetModelValue("OpwBids.BidAmount", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
ModelState.SetModelValue("OpwBids.Contractor", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
return Page();
}
After the "Save Changes" I needed to re-load the view data for the "Contractor" drop down list. Then it was just a matter of clearing the form fields before returning the page.

How to implement validation rules for elements from a partial view

We are developing a .net core 3.1 MVC application (actual with MVVMC).
We have implemented custom validation attributes. And want them to be checked on server and on client side.
The view is a standard view, however the user has the possibility to add multiple partial views to the standard view (via a button).
In the partial view we were not able to use the 'asp-for' tag helper within the input fields, because one can have several times the same item added, and we need to be able to create a list in the ViewModel out of those additional partial views selected. That's why we tried to build the tags by ourselfes.
Once a partial view has been requested via ajax we are calling the validator again. Server validation works, client validation only works for those inputs, which are on the main view, not for the inputs on the partial views. However, after refreshing or when the server sends the user back after a validation error, client validation starts working (because site is completely refreshed and jquery validation takes input fields of partial views into account).
Please find the code below:
Implementation of validation attribute
public class AllowedExtensionsAttribute: ValidationAttribute, IClientModelValidator
{
private readonly List<string> _extensions = new List<string>();
public AllowedExtensionsAttribute(string extensions)
{
_extensions.Add(extensions);
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (value is IFormFile file)
{
var extension = Path.GetExtension(file.FileName);
if (!_extensions.Contains(extension.ToLower()))
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-extension", ErrorMessage);
}
}
MainViewModel
[AllowedExtensions(".pdf", ErrorMessage = "This data type is not allowed!")]
public IFormFile PdfDocumentOne { get; set; }
PartialViewModel
[AllowedExtensions(".pdf", ErrorMessage = "This data type is not allowed!")]
public IFormFile PdfDocumentTwo { get; set; }
Main View
<form method="post" asp-controller="Home" asp-action="MainView" enctype="multipart/form-data">
<div class="form-group top-buffer">
<div class="row">
<div class="col-2">
<label asp-for="PdfDocumentOne" class="control-label"></label>
</div>
<div class="col-3">
<input asp-for="PdfDocumentOne" class="form-control-file" accept="application/pdf" />
<span asp-validation-for="PdfDocumentOne" class="text-danger"></span>
</div>
</div>
</div>
<div id="container">
<div id="containerFull" class="form-group">
<div class="row">
<div class="col-2">
<label class="control-label">...</label>
</div>
<div class="col-10">
<div id="containerPartialView">
</div>
</div>
</div>
<div class="row">
<div class="col-2">
</div>
<div class="col-3">
<button id="AddPartialView" type="button" class="form-control">...</button>
</div>
</div>
</div>
</div>
...
<div class="form-group top-buffer">
<div class="row">
<div class="col-2">
<input type="submit" value="Submit" class="form-control" id="checkBtn" />
</div>
</div>
</div>
</form>
Partial View
<input id="Lists[#Model.ViewId].ViewId" name="Lists[#Model.ViewId].ViewId" class="partialViewModel" type="hidden" value="#Model.ViewId" />
<div id="Lists[#Model.ViewId].MainContainer" class="partialView">
<div class="form-group">
<div>
<div class="col-2">
<label asp-for="PdfDocumentTwo" class="control-label"></label>
</div>
<div class="col-3">
<input name="Lists[#Model.ViewId].PdfDocumentTwo" id="Lists[#Model.ViewId].PdfDocumentTwo " type="file" class="form-control-file" accept="application/pdf"
data-val="true" data-val-extension="This data type is not allowed!"/>
<span class="text-danger field-validation-valid" data-valmsg-for="Lists[#Model.ViewId].PdfDocumentTwo" data-valmsg-replace="true"></span>
</div>
</div>
</div>
...
</div>
Javascript
function AddPartialView() {
var i = $(".partialView").length;
$.ajax({
url: '/Home/AddPartialView?index=' + i,
success: function (data) {
$('#containerPartialView').append(data);
$().rules('remove','extension');
jQuery.validator.unobtrusive.adapters.addBool("extension");
},
error: function (a, b, c) {
console.log(a, b, c);
}
});
}
$('#AddPartialView').click(function () {
AddPartialView();
});
jQuery.validator.addMethod("extension",
function (value, element, param) {
var extension = value.split('.').pop().toLowerCase();
if ($.inArray(extension, ['pdf']) == -1) {
return false;
}
return true;
});
jQuery.validator.unobtrusive.adapters.addBool("extension");
HomeController
[HttpPost]
public IActionResult MainView(MainViewModel vm)
{
if (vm == null)
{
return RedirectToAction("Index");
}
DatabaseHelper.GetMainViewModel(vm);
if (!ModelState.IsValid)
{
#ViewData["StatusMessageNegative"] = "The entered data is not valid. Please scroll down to correct your data.";
return View(vm);
}
return RedirectToAction("UploadDocument", new { Id = vm.Id});
}
[HttpGet]
public ActionResult AddPartialView(int index)
{
PartialViewModel pvm = DatabaseHelper.GetPartialViewModel(index);
return PartialView("Partial", pvm);
}
After searching in the internet we found the following argument (data-ajax="true"). However, we don't know where to place it or how to use it correctly.
Have you tried re-apply validation after loading your partial view?
$.ajax({
url: '/Home/AddPartialView?index=' + i,
success: function (data) {
$('#containerPartialView').append(data);
$().rules('remove','extension');
jQuery.validator.unobtrusive.adapters.addBool("extension");
},
error: function (a, b, c) {
console.log(a, b, c);
}
}).then(function () {
$("form").each(function () { $.data($(this)[0], 'validator', false); });
$.validator.unobtrusive.parse("form");
});

Issue with Custom TagHelper (Autocomplete) when used inside a List of Items (being displayed with EditorFor)

We have written a custom TagHelper to handle Autocomplete in a generic fashion. It has an asp-for attribute, which is defined as as ModelExpression variable.
The autocomplete TagHelper writes out a hidden field (the Id field), as well as an Input field for the autocomplete js code to work on. It ultimately saves the selected items Id value to the hidden field. This autocomplete works very well for multiple fields on a form.
But when the autocomplete TagHelper is incorporated in a list of items using an EditorTemplate to display all items the same (using EditorFor on a List in the model), then we need to set the Z index based name on the hidden field so it comes back to the controller as a List of the items. e.g. z0__*Field*, Z1__*Field*, ...
How do we get the Z index based name prefix that needs to be tacked on to the front of all the field names?
Do we have to make it up ourselves?
or do we extract from the ModelExpression somehow?
The Standard Input TagHelper is getting handled correctly?
Is the EditorFor/EditorTemplate the correct way to handle lists of editable objects in ASP.NET Core 1?
Autocomplete TagHelper
public class AutocompleteTagHelper : TagHelper
{
public ModelExpression AspFor { get; set; }
public ModelExpression AspValue { get; set; }
//public string AspFor { get; set; }
public string Route { get; set; }
public string RouteParameters { get; set; }
public string TargetWrapper { get; set; }
public string DisplayFormat { get; set; }
public string ValueFormat { get; set; }
public string ManageListCallback { get; set; }
public string ListWrapper { get; set; }
public string Placeholder { get; set; }
private SkillDbContext _context;
private readonly UserManager<ApplicationUser> _userManager;
private IMemoryCache cache;
public AutocompleteTagHelper(SkillDbContext Context, UserManager<ApplicationUser> userManager, IMemoryCache cache)
{
_context = Context;
_userManager = userManager;
this.cache = cache;
}
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var hiddenVal = "";
var displayVal = "";
//asp-for="LandingPointId"
//route="/Lookups/GetLandingPoint"
//route-parameter="SomeOtherId"
//target-wrapper="form" key="Id"
//label="{Name} ({Code})"
//output="{code}"
//AspFor.
//get parent model from AspFor
object thisModel = null;
//get value properties
if (AspValue != null)
{
hiddenVal = ValueFormat;
displayVal = DisplayFormat;
thisModel = AspValue.Model;
}
else if (AspFor.Model != null && !AspFor.Model.Equals((object)0))
{
Object Id = AspFor.Model;
string routeMethod = Route.Split('/').Last<string>();
}
if(thisModel != null)
{
PropertyInfo[] propertyInfo = thisModel.GetType().GetProperties();
foreach (var info in propertyInfo)
{
var val = info.GetValue(thisModel);
if (val != null)
{
hiddenVal = hiddenVal.Replace(("{" + info.Name + "}"), val.ToString());
displayVal = displayVal.Replace(("{" + info.Name + "}"), val.ToString());
}
}
}
var isAcList = ManageListCallback != null && ListWrapper != null;
string aspForName = AspFor.Name.Replace(".", "_");
output.TagName = "input"; // replaces <email> with <a> tag
inputId = inputName = aspForName;
output.Attributes["id"] = aspForName;
output.Attributes["name"] = aspForName;
output.Attributes["type"] = "text";
output.Attributes["route"] = Route;
output.Attributes["route-parameters"] = RouteParameters;
output.Attributes["target-wrapper"] = TargetWrapper;
output.Attributes["placeholder"] = Placeholder;
output.Attributes["value-format"] = ValueFormat;
output.Attributes["display-format"] = DisplayFormat;
output.Attributes["value"] = displayVal;
output.Attributes["class"] = "autocomplete form-control" + (isAcList?" hasList":"");
TagBuilder HiddenValue = new TagBuilder("input");
HiddenValue.Attributes["name"] = inputName;
HiddenValue.Attributes["id"] = inputId + "_hidden";
HiddenValue.Attributes["type"] = "hidden";
HiddenValue.Attributes["value"] = hiddenVal;
output.PreElement.SetContent(HiddenValue);
if (isAcList)
{
TagBuilder AddBtn = new TagBuilder("a");
AddBtn.Attributes["id"] = AspFor.Name.Replace(".", "_") + "_submit";
AddBtn.Attributes["class"] = "moana-autocomplete-list-manager disabled btn btn-primary";
AddBtn.Attributes["listwrapper"] = ListWrapper;
AddBtn.Attributes["href"] = ManageListCallback;
AddBtn.InnerHtml.AppendHtml("Add");
output.PostElement.SetContent(AddBtn);
}
}
This is the model
public class AddressEditorModel
{
public int Id { get; set; }
public string AddressLinkTo { get; set; }
public int AddressLink { get; set; }
public string AddressLine { get; set; }
public int ContactTypeId { get; set; }
public string Suburb { get; set; }
public string City { get; set; }
public string Postcode { get; set; }
public int? CountryId { get; set; }
public string ContactTypeName { get; set; }
public string CountryCode { get; set; }
public string CountryName { get; set; }
}
This is the cshtml
#model List<Skill.ViewModels.AddressEditorModel>
<div class="address-info-wrapper">
#Html.EditorFor(m => m)
<div>
This is the controller method call
public async Task<IActionResult> UpdateAddressInfo(List<AddressEditorModel> addresses)
and finally this is the EditorTemplate
#model Skill.ViewModels.AddressEditorModel
<input type="hidden" asp-for="Id" />
<input type="hidden" asp-for="ContactTypeId" />
<input type="hidden" asp-for="AddressLink" />
<input type="hidden" asp-for="AddressLinkTo" />
<input type="hidden" asp-for="CountryId" />
<label class="col-md-12 control-label" style="padding-bottom:20px;">#Model.ContactTypeName</label>
<div class="form-group">
<label asp-for="AddressLine" class="col-md-2 control-label">Address</label>
<div class="col-md-10">
<input asp-for="AddressLine" class="form-control" style="resize:both" />
<span asp-validation-for="AddressLine" class="text-danger" />
</div>
</div>
<div class="form-group">
<label asp-for="Suburb" class="col-md-2 control-label">Suburb</label>
<div class="col-md-10">
<input asp-for="Suburb" class="form-control" />
<span asp-validation-for="Suburb" class="text-danger" />
</div>
</div>
<div class="form-group">
<label asp-for="City" class="col-md-2 control-label">City</label>
<div class="col-md-10">
<input asp-for="City" class="form-control" />
<span asp-validation-for="City" class="text-danger" />
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">Country</label>
<div class="col-md-10">
<autocomplete asp-for="CountryId" route="/Lookups/GetCountry" target-wrapper="form" display-format="{Name} ({Code})" value-format="{Id}"></autocomplete>
<span asp-validation-for="CountryId" class="text-danger" />
</div>
</div>
<div class="form-group">
<label asp-for="Postcode" class="col-md-2 control-label">Post Code</label>
<div class="col-md-10">
<input asp-for="Postcode" class="form-control" />
<span asp-validation-for="Postcode" class="text-danger" />
</div>
</div>
Note that the autocomplete tag above in the EditorTemplate generates its own internal tags (as part of the tag helper).
This is a portion of the page for the first Address Information (as shown in firefox)
<input id="z0__Id" type="hidden" value="5" name="[0].Id" data-val-required="The Id field is required." data-val="true">
<input id="z0__ContactTypeId" type="hidden" value="1" name="[0].ContactTypeId" data-val-required="The ContactTypeId field is required." data-val="true">
<input id="z0__AddressLink" type="hidden" value="1" name="[0].AddressLink" data-val-required="The AddressLink field is required." data-val="true">
<input id="z0__AddressLinkTo" type="hidden" value="F" name="[0].AddressLinkTo">
<label class="col-md-12 control-label" style="padding-bottom:20px;">Work</label>
<div class="form-group">
<label class="col-md-2 control-label" for="z0__AddressLine">Address</label>
<div class="col-md-10">
<input id="z0__AddressLine" class="form-control" type="text" value="4a Lansdowne Street" name="[0].AddressLine" style="resize:both">
<span class="text-danger field-validation-valid" data-valmsg-replace="true" data-valmsg-for="[0].AddressLine"> </span>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="z0__Suburb">Suburb</label>
<div class="col-md-10">
<input id="z0__Suburb" class="form-control" type="text" value="Bayswater" name="[0].Suburb">
<span class="text-danger field-validation-valid" data-valmsg-replace="true" data-valmsg-for="[0].Suburb"> </span>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="z0__City">City</label>
<div class="col-md-10">
<input id="z0__City" class="form-control" type="text" value="Auckland" name="[0].City">
<span class="text-danger field-validation-valid" data-valmsg-replace="true" data-valmsg-for="[0].City"> </span>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label">Country</label>
<div class="col-md-10">
<input id="CountryId_hidden" type="hidden" value="1" name="CountryId">
<input id="CountryId" class="moana-autocomplete form-control ui-autocomplete-input" type="text" value="New Zealand (NZ)" display-format="{Name} ({Code})" value-format="{Id}" placeholder="" target-wrapper="form" route-parameters="" route="/Lookups/GetCountry" name="CountryId" autocomplete="off">
<span class="text-danger field-validation-valid" data-valmsg-replace="true" data-valmsg-for="[0].CountryId"> </span>
</div>
</div>
<div class="form-group">
<label class="col-md-2 control-label" for="z0__Postcode">Post Code</label>
<div class="col-md-10">
<input id="z0__Postcode" class="form-control" type="text" value="0604" name="[0].Postcode">
<span class="text-danger field-validation-valid" data-valmsg-replace="true" data-valmsg-for="[0].Postcode"> </span>
</div>
</div>
Note that the Html.EditorFor generates the Zn__fieldname prefixes to the input name attribute as well as an [n].fieldname name for the input id attribute
The issue is how to access the index value, or get this prefix to tack onto our generated inputs from inside the TagHelper i.e. the Zn__* or [n] value, which is essentially the indexor of the EditorFor as it generates the repeated fields
Thanks for any help
Ok I have been working diligently on this.
I eventually had to look at the TagHelpers from the Asp.Net core source project.
I found that we need to access the ViewContext.ViewData.TemplateInfo;
from inside the TagHelper.
Or alternately we could use the IHtmlGenerator object - calling a GenerateTextBox, GenerateHidden, etc to build the TageHelpers as required

Getting CheckList data in MVC6 post

I am doing user roles in MVC6 (using EF7), and I am think I am just missing something, but on a form used to define a user roles I get nothing in the posted back Model to the controller
This is my Model
public class UserRoleItem
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public IdentityUserRole<int> userRole { get; set; }
public bool HasRole { get; set; }
}
public class UsersRolesViewModel
{
public int UserId { get; set; }
public string FullName { get; internal set; }
public List<UserRoleItem> UserRoles;
}
This is the view
#model Skill.ViewModels.Manage.UsersRolesViewModel
<br />
<h3>Set Roles for user - #Model.FullName</h3>
<form asp-action="ChangeUsersRoles" >
<div class="form-horizontal">
<hr />
<input type="hidden" asp-for="UserId" />
#foreach (var userRole in Model.UserRoles)
{
<input type="hidden" asp-for="#userRole.Id" />
<div class="form-group">
<div class="inline-block col-md-8">
<span class="col-md-1" align="center">
<input type="checkbox" asp-for="#userRole.HasRole" />
</span>
<div class="col-md-7">
#userRole.Name (#userRole.Description)
</div>
</div>
</div>
}
<hr />
<div class="form-group">
<div class="col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
This is the Controller
// GET: Users/ChangeUserRoles/5
public async Task<IActionResult> ChangeUserRoles(int? id)
{
if (id == null)
{
return HttpNotFound();
}
var model = new UsersRolesViewModel();
ApplicationUser appUser = await _context.ApplicationUsers.SingleAsync(m => m.Id == id);
if (appUser == null)
{
return HttpNotFound();
}
else
{
model.UserId = (int)id;
model.FullName = appUser.FullName;
var some = from r in _context.Roles
from ur in _context.UserRoles
.Where(inner => r.Id == inner.RoleId && inner.UserId == id)
.DefaultIfEmpty()
select new UserRoleItem
{
Id = (int)r.Id,
Name = r.Name,
Description = r.NormalizedName,
userRole = ur, // this is needed else it has a hissy fit
HasRole = (ur != null)
};
// get all of the Roles and then also link to the ones the user currently has
model.UserRoles = (some).ToList();
}
return View(model);
}
// POST: Users/ChangeUserRoles/5
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangeUserRoles([Bind(include:"UserId,UserRoles")]UsersRolesViewModel userModel)
{
if (ModelState.IsValid)
{
// update based on the changes
// return RedirectToAction("Edit", new { userModel.UserId });
}
return View(userModel);
}
So when I get the post back on Save, the UserRoles list is null, so I assume I am just missing an obvious thing here?
Also another small issue is the EF Linq statement. If I remove the
userRole = ur,
statement from the Select portion of the Linq query, the system has a fit and says my schema is out of date (which it isn't). I think it is due to the following statement where I am testing the outer join value
HasRole = (ur != null)
Although this seems perfectly reasonable and works if the ur variable is used prior to testing for null (or not)
After entering this question - I further investigated the issue and found that I could do what I needed using an EditorTemplate
So I created the EditorTemplate Folder under my Users View folder as it was a UsersController and then added the following UserRoleItem.cshtml file
#model UserRoleItem
<input type="hidden" asp-for="Id" />
<div class="form-group">
<div class="inline-block col-md-8">
<span class="col-md-1" align="center">
<input type="checkbox" asp-for="HasRole" />
</span>
<div class="col-md-7">
#Model.Name (#Model.Description)
</div>
</div>
</div>
I then changed the view (called ChangeUsersRole.cshtml) to be
#model Skill.ViewModels.Manage.UsersRolesViewModel
<br />
<h3>Set Roles for user - #Model.FullName</h3>
<hr />
<form asp-action="ChangeUsersRoles">
<div class="form-horizontal">
<input type="hidden" asp-for="UserId" />
#Html.EditorFor(m => m.UserRoles)
<hr />
<div class="form-group">
<div class="col-md-10">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</div>
</div>
</form>
Note also: that I tried using the new format
<input asp-for="UserRoles" />
instead of this line in the view
#Html.EditorFor(m => m.UserRoles)
But it did not work as expected. Again maybe I am still doing something wrong - or maybe that feature isn't working yet?