Reputation: 10669
I have a view model object that contains a List
of another model object type. When the user does a query on the page, if the List
returned contains more than 300 records we want to use paging to keep the loading time down (some search results can return more than 14k records). The paging plugin we're using can be found here.
Once the results have been displayed on the page, the user has the ability to click a check box next to specific results, type in some information in an input text box, hit submit, and have the selected records edited with the information from the text box.
Since we needed to use an IPagedList<>
in order to enable paging, however, when you hit submit (and before the page even hits the controller) we get the following error:
Cannot create an instance of an interface.
View Model
These are the two list objects that we use for paging. The zipCodeTerritory
object holds the results of the query. The pagedTerritoryList
is used to display only the results on the specific page the user is on.
//Paging List objects
public IPagedList<ZipCodeTerritory> pagedTerritoryList { get; set; }
public List<ZipCodeTerritory> zipCodeTerritory { get; set; }
public IPagedList PagingMetaData { get; set; }
Controller
This is our basic search. The .ToPagedList
method is used to specify what range of results we want to display and place them in the pagedTerritoryList
object.
//set Paged List counter variables
int pageNumber = page ?? 1;
int pageSize = 300;
//Determine if Territory present?
if (string.IsNullOrWhiteSpace(search.searchTerritory))
{
//State Code ONLY search
search.zipCodeTerritory = (from z in db.ZipCodeTerritory
where z.StateCode.Equals(search.searchState)
select z).ToList();
}
else if(string.IsNullOrWhiteSpace(search.searchState))
{
//Territory ONLY search
search.zipCodeTerritory = (from z in db.ZipCodeTerritory
where z.IndDistrnId.Equals(search.searchTerritory)
select z).ToList();
}
else
{
//Territory AND state search
search.zipCodeTerritory = (from z in db.ZipCodeTerritory
where z.IndDistrnId.Equals(search.searchTerritory) &&
z.StateCode.Equals(search.searchState)
select z).ToList();
}
//Convert list to IPagedList for pagining on Index
search.pagedTerritoryList = search.zipCodeTerritory.ToPagedList(pageNumber, pageSize);
//Set Paged List objects
search.PagingMetaData = new StaticPagedList<ZipCodeTerritory>(search.zipCodeTerritory, pageNumber, pageSize,
search.zipCodeTerritory.Count).GetMetaData();
return View(search);
View
This is the form that displays the search results. If a user checks the check box, then hits either clone
or delete
buttons, the results are supposed to be posted back to the controller's Update
method and appropriate edits or deletes performed. The information the user wants to overlay in an edit are input into the newTerritory/Description/etc
fields in the form (above the table
).
Regarding the @Html.PagedListPager
I found I had to pass back to the index method the same search criteria from the page, thus the excessive amount of parameters in the RouteValueDictionary
.
@if (Model.zipCodeTerritory.Count > 0)
{
using (Html.BeginForm("Update", "ZipCodeTerritory", FormMethod.Post))
{
@Html.HiddenFor(model => model.searchZip)
@Html.HiddenFor(model => model.searchDate)
@Html.HiddenFor(model => model.searchState)
<div id="cloneBox">
<div id="rw1">
@Html.LabelFor(model => model.newTerritory)
@Html.TextBoxFor(model => model.newTerritory, new { style = "width: 30px;padding-left:10px;", maxLength = 3 })
@Html.LabelFor(model => model.newDescription)
@Html.TextBoxFor(model => model.newDescription, new { style = "width: 250px;padding-left:10px;", maxLength = 30 })
@Html.LabelFor(model => model.newEffectiveDate)
@Html.TextBoxFor(model => model.newEffectiveDate, new { style = "width: 80px;padding-left:10px;" })
<div id="rw2" style="padding-top: 10px;">
@Html.LabelFor(model => model.newChannelCode)
@Html.DropDownListFor(model => model.newChannelCode, Model.ChannelCodes, " ")
@Html.LabelFor(model => model.newStateCode)
@Html.DropDownListFor(model => model.newStateCode, Model.StateCodes, " ")
</div>
</div>
</div>
<br/>
<div id="buttonDiv">
<button type="submit" id="CloneButton" name="button" value="clone">Apply New Data</button>
<button type="submit" id="deleteButton" name="button" value="delete">Delete Selected Items</button>
</div>
@*Display paging only if necessary*@
if (Model.pagedTerritoryList.Count >= 300)
{
<div id="pagingDiv">
@Html.PagedListPager(new StaticPagedList<Monet.Models.ZipCodeTerritory>(Model.zipCodeTerritory, Model.PagingMetaData) ,
Page => Url.Action("Index", new RouteValueDictionary()
{
{ "Page", Page},
{ "searchZip", Model.searchZip },
{ "searchActiveOnly", Model.searchActiveOnly },
{ "searchDate", Model.searchDate },
{ "searchState", Model.searchState },
{ "searchTerritory", Model.searchTerritory },
{ "searchChannel" , Model.searchChannelCode }
}), PagedListRenderOptions.DefaultPlusFirstAndLast)
</div>
}
<table id="thetable" class="tablesorter" >
<thead>
<th>@Html.CheckBox("SelectAll")</th>
<th>State</th>
<th>Territory</th>
<th>Zip</th>
<th>Description</th>
<th>Effective</th>
<th>End Date</th>
<th>Last Update Date</th>
<th>Channel</th>
<th></th>
</thead>
<tbody id="tableBody">
@for (int i = 0; i < Model.pagedTerritoryList.Count; i++)
{
<tr id="@(Model.lastEditedId == Model.pagedTerritoryList[i].Id ? "lastEdit" : "")">
<td>
@Html.CheckBoxFor(model => model.pagedTerritoryList[i].Update)
@Html.HiddenFor(model => model.pagedTerritoryList[i].Update)
</td>
<td>
@Html.DisplayFor(model => model.pagedTerritoryList[i].StateCode)
@Html.HiddenFor(model => model.pagedTerritoryList[i].StateCode)
</td>
<td>
@Html.DisplayFor(model => model.pagedTerritoryList[i].IndDistrnId)
@Html.HiddenFor(model => model.pagedTerritoryList[i].IndDistrnId)
</td>
<td>
@Html.DisplayFor(model => model.pagedTerritoryList[i].ZipCode)
@Html.HiddenFor(model => model.zipCodeTerritory[i].ZipCode)
</td>
<td>
@Html.DisplayFor(model => model.pagedTerritoryList[i].DrmTerrDesc)
@Html.HiddenFor(model => model.pagedTerritoryList[i].DrmTerrDesc)
</td>
<td>
@Html.DisplayFor(model => model.pagedTerritoryList[i].EffectiveDate)
@Html.HiddenFor(model => model.pagedTerritoryList[i].EffectiveDate)
</td>
<td>
@if (Model.pagedTerritoryList[i].EndDate.Date != DateTime.MaxValue.Date)
{
@Html.DisplayFor(model => model.pagedTerritoryList[i].EndDate)
@Html.HiddenFor(model => model.pagedTerritoryList[i].EndDate)
}
</td>
<td>
@Html.DisplayFor(model => model.pagedTerritoryList[i].LastUpdateDate)
@Html.HiddenFor(model => model.pagedTerritoryList[i].LastUpdateDate)
</td>
<td>
@Html.DisplayFor(model => model.pagedTerritoryList[i].ChannelCode)
@Html.HiddenFor(model => model.pagedTerritoryList[i].ChannelCode)
</td>
@if (ViewBag.SecurityLevel >= 4)
{
<td>
@Html.ActionLink("Edit", "Edit", new
{
id = Model.zipCodeTerritory[i].Id,
searchZip = Model.searchZip,
searchActiveOnly = Model.searchActiveOnly,
searchDate = Model.searchDate,
searchState = Model.searchState,
searchTerritory = Model.searchTerritory,
searchChannelCode = Model.searchChannelCode
})
@Html.HiddenFor(model => model.zipCodeTerritory[i].Id)
</td>
}
</tr>
}
</tbody>
</table>
}
}
EDIT
Per the comment below, here is the signature for the method the form is posting to. It contains an instance of the ZipCodeIndex
that gets loaded on the page originally, plus the text from the button
to determine whether we're doing a clone
or delete
[HttpPost]
public ActionResult Update(ZipCodeIndex updateZip, string button)
{
Second Edit
Tried the method from this question but still receiving the original error message ("cannot create instance of an interface").
Upvotes: 2
Views: 4850
Reputation: 10669
I was able to completely hack my way out of this, however I don't think it's the best solution. Would love it if someone could provide a better answer however I'll post this up here in the meantime.
Since the IPagedList
object was built to hold a specific range of the List<>
I just made a display property on my view model and used this on my view. This list, not the IPagedList
gets posted back to the controller for the updates, so no interface weirdness occurs.
View Model
//Paging List objects
public IPagedList<ZipCodeTerritory> pagedTerritoryList { get; set; }
public List<ZipCodeTerritory> zipCodeTerritory { get; set; }
public List<ZipCodeTerritory> displayForPaging { get; set; }
Controller
//Convert list to IPagedList for pagining on Index
search.pagedTerritoryList = search.zipCodeTerritory.ToPagedList(pageNumber, pageSize);
search.displayForPaging = search.pagedTerritoryList.ToList();
View
<td>
@Html.CheckBoxFor(model => model.displayForPaging[i].Update)
@Html.HiddenFor(model => model.displayForPaging[i].Update)
</td>
<td>
.
.
Upvotes: 5
Reputation: 8475
I've found that models with interfaces as properties will need to have a ModelBinder created to determine which implementation to use.
ASP.Net MVC Custom Model Binding explanation
Basically, your binder will parse your model for you, determining which type of IPagedList<> to implement.
Upvotes: 0