问题
I\'m trying to using a PagedList in my ASP.Net application and I found this example on the Microsoft website http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/sorting-filtering-and-paging-with-the-entity-framework-in-an-asp-net-mvc-application
How is it possible to use a PagedList in a complex situation that uses a ViewModel? I\'m trying to add a PagedList without success to the Instructor example posted here: http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/reading-related-data-with-the-entity-framework-in-an-asp-net-mvc-application
The problem is that the ViewModel is composed by classes and not simple fields, so I cannot convert the result with the ToPageList() method.
This is the ViewModel structure:
using System.Collections.Generic;
using ContosoUniversity.Models;
namespace ContosoUniversity.ViewModels
{
public class InstructorIndexData
{
public IEnumerable<Instructor> Instructors { get; set; }
public IEnumerable<Course> Courses { get; set; }
public IEnumerable<Enrollment> Enrollments { get; set; }
}
}
I need to join the three tables into the ViewModel and display the result in a View.
回答1:
For anyone who is trying to do it without modifying your ViewModels AND not loading all your records from the database.
Repository
public List<Order> GetOrderPage(int page, int itemsPerPage, out int totalCount)
{
List<Order> orders = new List<Order>();
using (DatabaseContext db = new DatabaseContext())
{
orders = (from o in db.Orders
orderby o.Date descending //use orderby, otherwise Skip will throw an error
select o)
.Skip(itemsPerPage * page).Take(itemsPerPage)
.ToList();
totalCount = db.Orders.Count();//return the number of pages
}
return orders;//the query is now already executed, it is a subset of all the orders.
}
Controller
public ActionResult Index(int? page)
{
int pagenumber = (page ?? 1) -1; //I know what you're thinking, don't put it on 0 :)
OrderManagement orderMan = new OrderManagement(HttpContext.ApplicationInstance.Context);
int totalCount = 0;
List<Order> orders = orderMan.GetOrderPage(pagenumber, 5, out totalCount);
List<OrderViewModel> orderViews = new List<OrderViewModel>();
foreach(Order order in orders)//convert your models to some view models.
{
orderViews.Add(orderMan.GenerateOrderViewModel(order));
}
//create staticPageList, defining your viewModel, current page, page size and total number of pages.
IPagedList<OrderViewModel> pageOrders = new StaticPagedList<OrderViewModel>(orderViews, pagenumber + 1, 5, totalCount);
return View(pageOrders);
}
View
@using PagedList.Mvc;
@using PagedList;
@model IPagedList<Babywatcher.Core.Models.OrderViewModel>
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<div class="container-fluid">
<p>
@Html.ActionLink("Create New", "Create")
</p>
@if (Model.Count > 0)
{
<table class="table">
<tr>
<th>
@Html.DisplayNameFor(model => model.First().orderId)
</th>
<!--rest of your stuff-->
</table>
}
else
{
<p>No Orders yet.</p>
}
@Html.PagedListPager(Model, page => Url.Action("Index", new { page }))
</div>
Bonus
Do above first, then perhaps use this!
Since this question is about (view) models, I'm going to give away a little solution for you that will not only be useful for paging, but for the rest of your application if you want to keep your entities separate, only used in the repository, and have the rest of the application deal with models (which can be used as view models).
Repository
In your order repository (in my case), add a static method to convert a model:
public static OrderModel ConvertToModel(Order entity)
{
if (entity == null) return null;
OrderModel model = new OrderModel
{
ContactId = entity.contactId,
OrderId = entity.orderId,
}
return model;
}
Below your repository class, add this:
public static partial class Ex
{
public static IEnumerable<OrderModel> SelectOrderModel(this IEnumerable<Order> source)
{
bool includeRelations = source.GetType() != typeof(DbQuery<Order>);
return source.Select(x => new OrderModel
{
OrderId = x.orderId,
//example use ConvertToModel of some other repository
BillingAddress = includeRelations ? AddressRepository.ConvertToModel(x.BillingAddress) : null,
//example use another extension of some other repository
Shipments = includeRelations && x.Shipments != null ? x.Shipments.SelectShipmentModel() : null
});
}
}
And then in your GetOrderPage
method:
public IEnumerable<OrderModel> GetOrderPage(int page, int itemsPerPage, string searchString, string sortOrder, int? partnerId,
out int totalCount)
{
IQueryable<Order> query = DbContext.Orders; //get queryable from db
.....//do your filtering, sorting, paging (do not use .ToList() yet)
return queryOrders.SelectOrderModel().AsEnumerable();
//or, if you want to include relations
return queryOrders.Include(x => x.BillingAddress).ToList().SelectOrderModel();
//notice difference, first ToList(), then SelectOrderModel().
}
Let me explain:
The static ConvertToModel
method can be accessed by any other repository, as used above, I use ConvertToModel
from some AddressRepository
.
The extension class/method lets you convert an entity to a model. This can be IQueryable or any other list, collection.
Now here comes the magic: If you have executed the query BEFORE calling SelectOrderModel()
extension, includeRelations
inside the extension will be true because the source
is NOT a database query type (not an linq-to-sql IQueryable
). When this is true, the extension can call other methods/extensions throughout your application for converting models.
Now on the other side: You can first execute the extension and then continue doing LINQ filtering. The filtering will happen in the database eventually, because you did not do a .ToList()
yet, the extension is just an layer of dealing with your queries. Linq-to-sql will eventually know what filtering to apply in the Database. The inlcudeRelations
will be false so that it doesn't call other c# methods that SQL doesn't understand.
It looks complicated at first, extensions might be something new, but it's really useful. Eventually when you have set this up for all repositories, simply an .Include()
extra will load the relations.
回答2:
As Chris suggested the reason you're using ViewModel doesn't stop you from using PagedList
.
You need to form a collection of your ViewModel objects that needs to be send to the view for paging over.
Here is a step by step guide on how you can use PagedList
for your viewmodel data.
Your viewmodel (I have taken a simple example for brevity and you can easily modify it to fit your needs.)
public class QuestionViewModel
{
public int QuestionId { get; set; }
public string QuestionName { get; set; }
}
and the Index method of your controller will be something like
public ActionResult Index(int? page)
{
var questions = new[] {
new QuestionViewModel { QuestionId = 1, QuestionName = "Question 1" },
new QuestionViewModel { QuestionId = 1, QuestionName = "Question 2" },
new QuestionViewModel { QuestionId = 1, QuestionName = "Question 3" },
new QuestionViewModel { QuestionId = 1, QuestionName = "Question 4" }
};
int pageSize = 3;
int pageNumber = (page ?? 1);
return View(questions.ToPagedList(pageNumber, pageSize));
}
And your Index view
@model PagedList.IPagedList<ViewModel.QuestionViewModel>
@using PagedList.Mvc;
<link href="/Content/PagedList.css" rel="stylesheet" type="text/css" />
<table>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.QuestionId)
</td>
<td>
@Html.DisplayFor(modelItem => item.QuestionName)
</td>
</tr>
}
</table>
<br />
Page @(Model.PageCount < Model.PageNumber ? 0 : Model.PageNumber) of @Model.PageCount
@Html.PagedListPager( Model, page => Url.Action("Index", new { page }) )
Here is the SO link with my answer that has the step by step guide on how you can use PageList
回答3:
I modified the code as follow:
ViewModel
using System.Collections.Generic;
using ContosoUniversity.Models;
namespace ContosoUniversity.ViewModels
{
public class InstructorIndexData
{
public PagedList.IPagedList<Instructor> Instructors { get; set; }
public PagedList.IPagedList<Course> Courses { get; set; }
public PagedList.IPagedList<Enrollment> Enrollments { get; set; }
}
}
Controller
public ActionResult Index(int? id, int? courseID,int? InstructorPage,int? CoursePage,int? EnrollmentPage)
{
int instructPageNumber = (InstructorPage?? 1);
int CoursePageNumber = (CoursePage?? 1);
int EnrollmentPageNumber = (EnrollmentPage?? 1);
var viewModel = new InstructorIndexData();
viewModel.Instructors = db.Instructors
.Include(i => i.OfficeAssignment)
.Include(i => i.Courses.Select(c => c.Department))
.OrderBy(i => i.LastName).ToPagedList(instructPageNumber,5);
if (id != null)
{
ViewBag.InstructorID = id.Value;
viewModel.Courses = viewModel.Instructors.Where(
i => i.ID == id.Value).Single().Courses.ToPagedList(CoursePageNumber,5);
}
if (courseID != null)
{
ViewBag.CourseID = courseID.Value;
viewModel.Enrollments = viewModel.Courses.Where(
x => x.CourseID == courseID).Single().Enrollments.ToPagedList(EnrollmentPageNumber,5);
}
return View(viewModel);
}
View
<div>
Page @(Model.Instructors.PageCount < Model.Instructors.PageNumber ? 0 : Model.Instructors.PageNumber) of @Model.Instructors.PageCount
@Html.PagedListPager(Model.Instructors, page => Url.Action("Index", new {InstructorPage=page}))
</div>
I hope this would help you!!
回答4:
I figured out how to do this. I was building an application very similar to the example/tutorial you discussed in your original question.
Here's a snippet of the code that worked for me:
int pageSize = 4;
int pageNumber = (page ?? 1);
//Used the following two formulas so that it doesn't round down on the returned integer
decimal totalPages = ((decimal)(viewModel.Teachers.Count() /(decimal) pageSize));
ViewBag.TotalPages = Math.Ceiling(totalPages);
//These next two functions could maybe be reduced to one function....would require some testing and building
viewModel.Teachers = viewModel.Teachers.ToPagedList(pageNumber, pageSize);
ViewBag.OnePageofTeachers = viewModel.Teachers;
ViewBag.PageNumber = pageNumber;
return View(viewModel);
I added
using.PagedList;
to my controller as the tutorial states.
Now in my view my using statements etc at the top, NOTE i didnt change my using model statement.
@model CSHM.ViewModels.TeacherIndexData
@using PagedList;
@using PagedList.Mvc;
<link href="~/Content/PagedList.css" rel="stylesheet" type="text/css" />
and then at the bottom to build my paged list I used the following and it seems to work. I haven't yet built in the functionality for current sort, showing related data, filters, etc but i dont think it will be that difficult.
Page @ViewBag.PageNumber of @ViewBag.TotalPages
@Html.PagedListPager((IPagedList)ViewBag.OnePageofTeachers, page => Url.Action("Index", new { page }))
Hope that works for you. Let me know if it works!!
回答5:
The fact that you're using a view model has no bearing. The standard way of using PagedList
is to store "one page of items" as a ViewBag
variable. All you have to determine is what collection constitutes what you'll be paging over. You can't logically page multiple collections at the same time, so assuming you chose Instructors
:
ViewBag.OnePageOfItems = myViewModelInstance.Instructors.ToPagedList(pageNumber, 10);
Then, the rest of the standard code works as it always has.
来源:https://stackoverflow.com/questions/25125329/using-a-pagedlist-with-a-viewmodel-asp-net-mvc