Using PaginatedList with ViewModel (ASP.NET Core 5.0, C#, EntityFramework)

Anna Tamaoka 136 Reputation points
2021-06-03T15:48:32.613+00:00

I'm creating a Frequently Asked Questions web application. I followed Microsoft's tutorial for adding sorting, filtering, and paging to an ASP.NET MVC EF Core application, using the PaginatedList:
https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/sort-filter-page?view=aspnetcore-5.0#add-paging-to-students-index

My question was answered here:
https://forums.asp.net/t/2112532.aspx?How+to+Paging+the+Index+Page+implemented+by+ViewModel+

but I don't think I'm implementing the solution correctly. I understand that I need to create a collection of objects that need to be paginated, but when I try to change

var viewModel = new QuestionIndexData();  

to

var viewModel = new List<QuestionIndexData>();  

it then gives me an error that says 'List<QuestionIndexData> does not contain a definition for 'Questions'...
102125-2021-06-03-10-13-20.jpg102147-homecontroller.txt102105-questionindexdata.txt

If I add

public List<QuestionAndAnswer> Questions { get; set; }  

to the QuestionIndexData model, the error does not go away.

Code truncated for brevity. Full code attached. Commented lines are solutions I have tried unsuccessfully.

QuestionIndexData model created following this tutorial:
https://learn.microsoft.com/en-us/aspnet/core/data/ef-mvc/read-related-data?view=aspnetcore-5.0#create-an-instructors-page

public class QuestionIndexData  
    {  
        public IEnumerable<QuestionAndAnswer> Questions { get; set; }  
        public IEnumerable<Tag> Tags { get; set; }  
        public IEnumerable<QuestionTag> QuestionTags { get; set; }  
        public IEnumerable<Vote> Votes { get; set; }  
        //public IList<QuestionAndAnswer> Questions { get; set; }  
        //public PaginatedList<QuestionAndAnswer> PagedQuestions { get; set; }  
    }  

Home Controller Index method:

public async Task<IActionResult> Index(  
                int? id,  
                string searchString,  
                int? pageNumber)  
    	{  
    		//...  
    		var viewModel = new QuestionIndexData();  
    		  
    		if (!String.IsNullOrEmpty(searchString))  
    		{  
    			//here is the error in my screenshot  
    			viewModel.Questions = await _context.QuestionAndAnswers  
    				.Include(i => i.Votes)  
    				.Include(i => i.QuestionTags)  
    					.ThenInclude(i => i.Tags)  
    				.Where(s =>  
    					   s.Question.Contains(searchString) ||  
    					   s.Answer.Contains(searchString))  
    				.AsNoTracking()  
    				.OrderByDescending(i => i.Votes.Count)  
    				.ToListAsync();  
    		}  
    		else  
    		{  
    			viewModel.Questions = await _context.QuestionAndAnswers  
    				.Include(i => i.Votes)  
    				.Include(i => i.QuestionTags)  
    					.ThenInclude(i => i.Tags)  
    				.AsNoTracking()  
    				.OrderByDescending(i => i.Votes.Count)  
    				.ToListAsync();  
    		}  
      
    		//return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), pageNumber ?? 1, pageSize));  
      
    		//return View(await PaginatedList<QuestionAndAnswer>.CreateAsync(viewModel.Questions, pageNumber ?? 1, pageSize));  
      
    		//return View(await PaginatedList<QuestionAndAnswer>.CreateAsync(viewModel.Questions.AsQueryable(), pageNumber ?? 1, pageSize));  
    		//returns error: InvalidOperationException: The provider for the source 'IQueryable' doesn't implement 'IAsyncQueryProvider'. Only providers that implement 'IAsyncQueryProvider' can be used for Entity Framework asynchronous operations.  
      
    		//return View(await PaginatedList<QuestionIndexData>.CreateAsync(viewModel.Questions.AsQueryable().AsNoTracking(), pageNumber ?? 1, pageSize));  
    		//return View(new QuestionIndexData { Questions = await PaginatedList<QuestionAndAnswer>.CreateAsync(viewModel.Questions.AsQueryable().AsNoTracking(), pageNumber ?? 1, pageSize) });  
    		  
    		//int pageSize = 3;  
    		  
    		//this works without pagination  
    		return View(viewModel);  
    	}  
Entity Framework Core
Entity Framework Core
A lightweight, extensible, open-source, and cross-platform version of the Entity Framework data access technology.
696 questions
ASP.NET Core
ASP.NET Core
A set of technologies in the .NET Framework for building web applications and XML web services.
4,188 questions
ASP.NET
ASP.NET
A set of technologies in the .NET Framework for building web applications and XML web services.
3,272 questions
{count} votes

Accepted answer
  1. Anna Tamaoka 136 Reputation points
    2021-06-09T15:58:18.317+00:00

    @Yihui Sun-MSFT

    Thank you for your reply. I apologize for all the back-and-forth. I really appreciate you taking the time to help me! This is exactly what I needed!

    All of my updated code for future reference (truncated for brevity):

    Model:

    public class QuestionIndexData  
        {  
            public PaginatedList<QuestionAndAnswer> Questions { get; set; }  
            public IEnumerable<Tag> Tags { get; set; }  
            public IEnumerable<QuestionTag> QuestionTags { get; set; }  
            public IEnumerable<Vote> Votes { get; set; }  
        }  
    

    HomeController.cs Index method:

    public async Task<IActionResult> IndexAsync(  
    	int? id,  
    	string searchString,  
    	int? pageNumber)  
    	{  
    		//...  
    		ViewData["CurrentFilter"] = searchString;  
    		var viewModel = new QuestionIndexData();  
    		var questions = _context.QuestionAndAnswers  
    		.Include(i => i.Votes)  
    		.Include(i => i.QuestionTags)  
    		.ThenInclude(i => i.Tags)  
    		.AsNoTracking()  
    		.OrderByDescending(i => i.Votes.Count)  
    		.ThenBy(i => i.Modified)  
    		.AsQueryable();  
    		if (!String.IsNullOrEmpty(searchString))  
    		{  
    			questions = questions  
    			.Where(s =>  
    			(s.Question.Contains(searchString) ||  
    			s.Answer.Contains(searchString)));  
    		}  
    		//populates each QuestionAndAnswer with related data (Votes and Tags)  
    		if (id != null)  
    		{  
    			ViewData["QuestionID"] = id.Value;  
    			QuestionAndAnswer question = viewModel.Questions.Where(  
    			i => i.ID == id.Value).Single();  
    			Vote votes = viewModel.Votes.Where(  
    			i => i.ID == id.Value).Single();  
    			viewModel.Tags = question.QuestionTags.Select(s => s.Tags);  
    		}  
    		else  
    		{  
    			viewModel.Tags = _context.Tags  
    			.Include(i => i.QuestionTags)  
    			.AsNoTracking()  
    			.OrderByDescending(t => t.QuestionTags.Count)  
    			.AsQueryable();  
    		}  
    		int pageSize = 3;  
    		return View(new QuestionIndexData  
    		{  
    			Questions = await PaginatedList<QuestionAndAnswer>.CreateAsync(questions.AsQueryable().AsNoTracking(), pageNumber ?? 1, pageSize),  
    			Tags = viewModel.Tags,  
    			Votes = viewModel.Votes  
    		});  
    	}  
    

    Home > Index.cshtml View:

    @model FrequentlyAskedQuestionsCodeFirst.Models.FaqViewModels.QuestionIndexData  
    @* ... *@  
    @foreach (var i in Model.Questions)  
    {  
        <div>  
            <div>  
                @Html.Raw(i.Question)<br />  
                @Html.Raw(i.Answer)  
                <hr />  
                Votes: @i.Votes.Count()  
                <hr />  
                Tags:  
                @foreach (var t in i.QuestionTags)  
                {  
                    @t.Tags.TagName  
                }  
            </div>  
        </div>  
    }  
    @{  
        var prevDisabled = !Model.Questions.HasPreviousPage ? "disabled" : "";  
        var nextDisabled = !Model.Questions.HasNextPage ? "disabled" : "";  
    }  
      
    <a asp-action="Index" asp-route-pageNumber="@(Model.Questions.PageIndex - 1)" class="btn btn-outline-info @prevDisabled">  
        Previous  
    </a>  
    <a asp-action="Index" asp-route-pageNumber="@(Model.Questions.PageIndex + 1)" class="btn btn-outline-info @nextDisabled">  
        Next  
    </a>  
    
    0 comments No comments

4 additional answers

Sort by: Most helpful
  1. Yihui Sun-MSFT 801 Reputation points
    2021-06-04T06:46:39.03+00:00

    Hi @Anna Tamaoka ,

    public IEnumerable<QuestionAndAnswer> Questions { get; set; }

    The await keyword is usually used with asynchronous methods. AsQueryable() is not an asynchronous method, so you don't need to use the await keyword.


    If the answer is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.
    Best Regards,
    YihuiSun

    0 comments No comments

  2. Anna Tamaoka 136 Reputation points
    2021-06-04T16:45:01.353+00:00

    @Yihui Sun-MSFT

    Thank you for your reply. This was the problem!

    For anyone following along, I removed the await operator from lines 12 and 25, and changed .ToListAsync() to .AsQueryable() on lines 21 and 31. I changed the return and set each QuestionIndexData value to the correct query.
    My code now reads:

    public async Task<IActionResult> Index(  
     int? id,  
     string searchString,  
     int? pageNumber)  
    {  
     //...  
     var viewModel = new QuestionIndexData();  
      
     if (!String.IsNullOrEmpty(searchString))  
     {  
     viewModel.Questions = _context.QuestionAndAnswers  
     .Include(i => i.Votes)  
     .Include(i => i.QuestionTags)  
     .ThenInclude(i => i.Tags)  
     .Where(s =>  
        s.Question.Contains(searchString) ||  
        s.Answer.Contains(searchString))  
     .AsNoTracking()  
     .OrderByDescending(i => i.Votes.Count)  
     .AsQueryable();  
     }  
     else  
     {  
     viewModel.Questions = _context.QuestionAndAnswers  
     .Include(i => i.Votes)  
     .Include(i => i.QuestionTags)  
     .ThenInclude(i => i.Tags)  
     .AsNoTracking()  
     .OrderByDescending(i => i.Votes.Count)  
     .AsQueryable();  
     }  
      
     int pageSize = 3;  
     return View(new QuestionIndexData  
     {  
     Questions = await PaginatedList<QuestionAndAnswer>.CreateAsync(viewModel.Questions.AsQueryable().AsNoTracking(), pageNumber ?? 1, pageSize),  
     Tags = viewModel.Tags,  
     Votes = viewModel.Votes  
     });  
    }  
    

    My view loads with the correct number of items per page (set to 3 in this code). However, now I have a problem in the view when I try to implement the paging links:

    @model FrequentlyAskedQuestionsCodeFirst.Models.FaqViewModels.QuestionIndexData  
    /* ... */  
    @{  
        var prevDisabled = !Model.HasPreviousPage ? "disabled" : "";  
        var nextDisabled = !Model.HasNextPage ? "disabled" : "";  
    }  
    <a asp-action="Index" asp-route-pageNumber="@(Model.PageIndex - 1)" class="btn btn-outline-info @prevDisabled">  
        Previous  
    </a>  
    <a asp-action="Index" asp-route-pageNumber="@(Model.PageIndex + 1)" class="btn btn-outline-info @nextDisabled">  
        Next  
    </a>  
    

    I tried changing it to !Model.Questions.HasPreviousPage but it gives me the same error:
    102601-image.png


  3. Anna Tamaoka 136 Reputation points
    2021-06-09T01:48:59.057+00:00

    @Yihui Sun-MSFT
    Yes, this is the same PaginatedList.cs that I'm using. I copied and pasted the code from the tutorial to my project.
    103604-image.png

    using System;  
    using System.Collections.Generic;  
    using System.Linq;  
    using System.Threading.Tasks;  
    using FrequentlyAskedQuestionsCodeFirst.Models;  
    using Microsoft.EntityFrameworkCore;  
    namespace FrequentlyAskedQuestionsCodeFirst  
    {  
        public class PaginatedList<T> : List<T>  
        {  
            public int PageIndex { get; private set; }  
            public int TotalPages { get; private set; }  
    		public PaginatedList(List<T> items, int count, int pageIndex, int pageSize)  
            {  
                PageIndex = pageIndex;  
                TotalPages = (int)Math.Ceiling(count / (double)pageSize);  
    		    this.AddRange(items);  
            }  
    		public bool HasPreviousPage  
            {  
                get  
                {  
                    return (PageIndex > 1);  
                }  
            }  
    		public bool HasNextPage  
            {  
                get  
                {  
                    return (PageIndex < TotalPages);  
                }  
            }  
    		public static async Task<PaginatedList<T>> CreateAsync(IQueryable<T> source, int pageIndex, int pageSize)  
            {  
                var count = await source.CountAsync();  
                var items = await source.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToListAsync();  
                return new PaginatedList<T>(items, count, pageIndex, pageSize);  
            }  
    		internal static Task<IEnumerable<QuestionAndAnswer>> CreateAsync<TEntity>(IQueryable<TEntity> entities, object p, int pageSize) where TEntity : class  
            {  
                throw new NotImplementedException();  
            }  
        }  
    }  
    
    0 comments No comments

  4. Yihui Sun-MSFT 801 Reputation points
    2021-06-09T06:44:48.72+00:00

    Hi @Anna Tamaoka ,

    Questions = await PaginatedList<QuestionAndAnswer>.CreateAsync(viewModel.Questions.AsQueryable().AsNoTracking(), pageNumber ?? 1, pageSize)  
    

    According to your code here, Questions should be PaginatedList<QuestionAndAnswer> type, you need to modify your model to:

         public class QuestionIndexData  
         {  
             public PaginatedList<QuestionAndAnswer> Questions { get; set; }  
         }  
    

    If the answer is helpful, please click "Accept Answer" and upvote it.
    Note: Please follow the steps in our documentation to enable e-mail notifications if you want to receive the related email notification for this thread.
    Best Regards,
    YihuiSun

    0 comments No comments