question

atamaoka avatar image
0 Votes"
atamaoka asked atamaoka edited

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

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://docs.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://docs.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);
         }


dotnet-aspnet-mvcdotnet-entity-framework-coredotnet-aspnet-core-razor
· 2
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

You defined viewModel as a List<QuestionIndexData>. Define viewModel as QuestionIndexData.

0 Votes 0 ·

Thank you for the reply.

I understand that too, but if I change .ToListAsync() to .AsQueryable, it says it does not contain a definition for 'GetAwaiter'. What is the correct way of implementing this change?
102201-image.png


0 Votes 0 ·
image.png (34.2 KiB)
atamaoka avatar image
0 Votes"
atamaoka answered atamaoka edited

@YihuiSun-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>



5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

YihuiSun-MSFT avatar image
0 Votes"
YihuiSun-MSFT answered

Hi @atamaoka ,

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

5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

atamaoka avatar image
0 Votes"
atamaoka answered YihuiSun-MSFT commented

@YihuiSun-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




image.png (46.6 KiB)
· 1
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

Hi @atamaoka ,
Is PaginatedList your custom method?If so, can you share the code of PaginatedList?
You can refer to PaginatedList.cs in this link.

0 Votes 0 ·
atamaoka avatar image
0 Votes"
atamaoka answered

@YihuiSun-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();
         }
     }
 }




image.png (20.6 KiB)
5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.

YihuiSun-MSFT avatar image
0 Votes"
YihuiSun-MSFT answered

Hi @atamaoka,

 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

5 |1600 characters needed characters left characters exceeded

Up to 10 attachments (including images) can be used with a maximum of 3.0 MiB each and 30.0 MiB total.