I've been trying to add a new page to an already existing web project that uses T4MVC in ASP.Net as well as Razor. Everything runs smoothly except for this one bug: when I submit my form, the list in the viewmodel is null, as if it couldn't create it with the data.
I've tried changing some names around, I've tried deleting and rewriting everything, I've tried disabling the validator thinking it was causing issues, to no avail. I've also tried regenerating everything using the "T4MVC.tt" file. Even then, the functionality is basically copy-pasted from another page that had something quite similar, yet it works like a charm (hence the "tubpicking" classes in the HTML).
What I've found is that it's most likely related to how "@Html.HiddenIndexerInputForModel()" generates the hidden index field for the MVC to find, since its value is always "0", but even when I edit it manually to make it seem good, I get the same result.
Any pointers would be greatly appreciated. I've been thinking it's a model binding bug, since I'm using the default one, but as much as I know, the entire project relies on the default binder and it works.
Here are the controller, viewmodel and view (create and recepted container) classes.
Controller:
public partial class ContainerReceptionController : MvcController
{
private readonly IDataTypeService _dataTypeService;
private readonly IHttpContextService _httpContext;
private readonly IEntityRepository<TubPicking> _tubPickingRepository;
private readonly IEntityRepository<Image> _imageRepository;
private readonly IEntityRepository<Company> _companyRepository;
private readonly IContainerServices _containerService;
private readonly IEntityRepository<Client> _clientRepository;
private readonly IEntityRepository<Emplacement> _emplacementRepository;
public ContainerReceptionController(IProviderServices provider, IHttpContextService httpContext)
{
_httpContext = httpContext;
_dataTypeService = provider.GetDataTypeService();
_tubPickingRepository = provider.GetTubPickingRepository();
_imageRepository = provider.GetImageRepository();
_containerService = provider.GetContainerServices();
_companyRepository = provider.GetCompanyRepository();
_clientRepository = provider.GetClientRepository();
_emplacementRepository = provider.GetEmplacementRepository();
}
public virtual ActionResult Create()
{
int ownerId = _httpContext.GetUserOwnerId();
string propertyName = "Owner.Id";
string orderBy = "Id";
string orderType = "ASC";
List<Client> clients = _clientRepository.GetByProperty(propertyName, ownerId.ToString(), orderBy + " " + orderType).ToList();
//Create a view model for the view
ContainerReceptionViewModel createViewModel = new ContainerReceptionViewModel
{
OwnerId = ownerId,
PickingId = 0,
ReceivedContainers = new List<ContainerCreateViewModel>(),
Clients = new SelectList(clients, "ID", "Name")
};
return View(createViewModel);
}
[HttpPost]
public virtual ActionResult Create(ContainerReceptionViewModel viewModel)
{
int ownerId = _httpContext.GetUserOwnerId();
// First, add all the containers in the database
foreach (ContainerCreateViewModel item in viewModel.ReceivedContainers)
{
// Database operations here
}
this.Flash("alert alert-success", UiText.Account.Validation.CREATE_ACCOUNT_SUCCESS);
return RedirectToAction(MVC.Container.Index());
}
// Used in the view to generate new containers
public virtual PartialViewResult AddOperation(string id)
{
int ownerId = _httpContext.GetUserOwnerId();
//Create data lists with a default value
List<DataType> containerTypes = new DataType() { Id = -1, Display = UiText.Container.SELECT_CONTAINER_TYPE }.AsList<DataType>();
List<DataType> materialTypes = new DataType() { Id = -1, Display = UiText.TubPicking.SELECT_MATERIAL_TYPE }.AsList<DataType>();
List<Emplacement> emplacementTypes = new Emplacement() { Id = -1 }.AsList<Emplacement>();
//Add data types to the data lists
containerTypes.AddRange(_dataTypeService.GetAllContainerTypes());
materialTypes.AddRange(_dataTypeService.GetAllMaterialTypes());
emplacementTypes.AddRange(_emplacementRepository.GetByProperty("Owner.Id", ownerId.ToString(), "Id ASC"));
//Create a view model from the data lists
ContainerCreateViewModel containerCreateViewModel = new ContainerCreateViewModel
{
SerialNumber = id,
ContainerTypes = new SelectList(containerTypes, "Id", "Display"),
MaterialTypes = new SelectList(materialTypes, "Id", "Display"),
ContainerEmplacements = new SelectList(emplacementTypes, "Id", "Name")
};
return PartialView("ReceptedContainer", containerCreateViewModel);
}
}
ViewModel:
public class ContainerReceptionViewModel : ViewModel
{
[HiddenInput]
public int PickingId { get; set; }
public int OwnerId { get; set; }
[DisplayName(UiText.Client.CLIENT_NAME)]
public int ClientId { get; set; }
[DisplayName(UiText.ContainerReception.RECEPTION_DATE)]
public DateTime ReceptionDate { get; set; }
public SelectList Clients { get; set; }
public SelectList Emplacements { get; set; }
public bool NeedConfirmation { get; set; }
public List<ContainerCreateViewModel> ReceivedContainers { get; set; }
}
View:
@using Externalization
@using Prosyn.Domain
@using Prosyn.Domain.Application
@using Prosyn.Web.ViewModels.TubPickings
@using Prosyn.Web.ViewModels.Containers
@model Prosyn.Web.ViewModels.ContainerReception.ContainerReceptionViewModel
<h2>@UiText.PageTitles.CREATE_CONTAINER_RECEPTION</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
<div class="col-md-12">
<div class="col-md-12">
@Html.ValidationMessageFor(model => Model.ReceivedContainers, "", new { @class = "text-danger" })
<hr />
<div class="col-md-3 form-group">
<div>
@Html.LabelFor(model => model.ClientId, htmlAttributes: new { @class = "control-label" })
<div class="input-group">
@Html.DropDownListFor(model => model.ClientId, Model.Clients, UiText.Shipping.SELECT_CLIENT, htmlAttributes: new { @class = "form-control dropdown_search" })
@Html.ValidationMessageFor(model => model.ClientId, "", new { @class = "text-danger" })
</div>
</div>
<div>
@Html.LabelFor(model => model.ReceptionDate, htmlAttributes: new { @class = "control-label" })
<div class='input-group'>
<span class="input-group-addon">
<span class="fa fa-calendar"></span>
</span>
@Html.EditorFor(model => model.ReceptionDate, new { htmlAttributes = new { @class = "form-control datetime", @id = "arrivalTime" } })
<a href="#" class="btn btn-primary input-group-addon" onclick="timeNow('arrivalTime')"><i class="glyphicon glyphicon-time"></i></a>
</div>
@Html.ValidationMessageFor(model => model.ReceptionDate, "", new { @class = "text-danger" })
</div>
</div>
</div>
<div class="col-md-12">
<div id="TubPickingRows">
@foreach (ContainerCreateViewModel container in Model.ReceivedContainers)
{
Html.RenderPartial("ReceptedContainer", container);
}
</div>
</div>
<div class="btn-group">
<a href="@Url.Action(MVC.ContainerReception.AddOperation(""))" class="btn btn-info btn-outline" id="addTubPicking"><i class="fa fa-plus"></i> @UiText.TubPicking.ADD_OPERATION</a>
</div>
</div>
<div class="form-group">
<div class="col-md-10" style="padding-top: 30px">
<input id="btn-submit" type="submit" value="@UiText.Actions.SAVE" class="btn btn-success" />
@Html.ActionLink(UiText.Actions.RETURN_LIST, MVC.Container.Index(), htmlAttributes: new { @class = "btn btn-default" })
</div>
</div>
</div>
}
<script>
var clickCount = @Model.ReceivedContainers.Count;
$(document).ready(function() {
$(window).keydown(function(event) {
if(event.keyCode === 13) {
event.preventDefault();
return false;
}
return true;
});
});
$("#addTubPicking").click(function () {
clickCount++;
$.ajax({
url: this.href + '?id=@Model.PickingId-' + clickCount,
cache: false,
success: function (html) {
$(html).hide().appendTo($("#TubPickingRows")).fadeIn();
var elements = document.getElementsByClassName("select_all");
for (var i = 0; i < elements.length; i++) {
elements[i].onclick = function() { this.select(); };
}
}
});
return false;
});
function deleteDriverBinPicking(sourceBtn) {
$(sourceBtn).closest('.TubPickingRow').fadeOut('fast', function () { $(sourceBtn).closest('.TubPickingRow').remove(); });
}
function timeNow(id) {
var now = new Date();
var year = now.getFullYear();
var month = now.getMonth() + 1;
if (month < 10) month = "0" + month;
var date = (now.getDate() < 10) ? "0" + now.getDate() : now.getDate();
var hour = ((now.getHours() < 10) ? "0" + now.getHours() : now.getHours()) + ":" + ((now.getMinutes() < 10) ? "0" + now.getMinutes() : now.getMinutes());
document.getElementById(id).value = year + "/" + month + "/" + date + " " + hour;
}
function updateForm(formId, color) {
var form = document.getElementById("bin-pickup-" + formId);
if (color === 0) {
form.className = "panel-body panel-background-g";
} else {
form.className = "panel-body panel-background-b";
}
document.getElementById("transfert_form_" + formId).style.display = (color !== 1) ? "none" : "block";
document.getElementById("pickup_form_" + formId).style.display = (color === 1) ? "none" : "block";;
}
</script>
As well as the partial view I use to render my containers:
@using Externalization
@using Helpers.Html
@using Prosyn.Web.ViewModels
@model Prosyn.Web.ViewModels.Containers.ContainerCreateViewModel
@using (Html.BeginCollectionItem("ReceptedContainers"))
{
<div class="TubPickingRow">
<div class="col-md-4">
<div class="panel panel-default">
@Html.HiddenIndexerInputForModel()
@Html.HiddenFor(model => model.Id)
<div class="panel-heading">
<a class="btn btn-danger" href="javascript:void(0);" onclick="return deleteDriverBinPicking(this)">
<i class="fa fa-trash-o fa-lg"></i>
</a>
<b class="col-sm-offset-1">@UiText.TubPicking.PICKUP</b>
</div>
<div class="panel-body panel-background-b" id="bin-pickup-@Model.Id">
<div id="data_inputs_@Model.Id">
<div id="transfert_form_@Model.Id" style="display: block">
<div class="col-md-12">
@Html.LabelFor(model => model.SerialNumber, new { @class = "control-label" })
@Html.EditorFor(model => model.SerialNumber, new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.SerialNumber, "", new { @class = "text-danger" })
</div>
<div class="col-md-12">
@Html.LabelFor(model => model.ContainerTypeId, new { @class = "control-label" })
@Html.DropDownListFor(model => model.ContainerTypeId, Model.ContainerTypes, htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.ContainerTypeId, "", new { @class = "text-danger" })
</div>
<div class="col-md-12">
@Html.LabelFor(model => model.MaterialTypeId, new { @class = "control-label" })
@Html.DropDownListFor(model => model.MaterialTypeId, Model.MaterialTypes, htmlAttributes: new { @class = "form-control" })
@Html.ValidationMessageFor(model => model.MaterialTypeId, "", new { @class = "text-danger" })
</div>
<div class="col-md-12">
@Html.LabelFor(model => model.ContainerEmplacementId, new { @class = "control-label" })
@Html.DropDownListFor(model => model.ContainerEmplacementId, Model.ContainerEmplacements, htmlAttributes: new { @class = "form-control" })
</div>
</div>
</div>
</div>
</div>
</div>
</div>
}
I found the answer, it's my bad, there was still a name error. The view model had the name "ReceivedContainers" while everything else referred to it as "ReceptedContainers". Always double check every single spelling.