How can I call a controller action when rendering

2020-04-05 09:06发布

问题:

I am creating a partial view for a sidebar that will show the most popular posts in my site. How can I create a separated controller for loading the model required by the partial view? (The IEnumerable<Post> with the popular posts)

Currently I have created a controller class that loads the popular posts but I keep getting errors when rendering the partial as I cannot call the controller and load the partial model. For example, if I call it from a view where I render a single post, the model types won't match (Post vs IEnumerable<Post>)

This is my SidebarController:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using GoBaron.Front.Models;

namespace GoBaron.Front.Controllers
{
    public class SidebarController : Controller
    {
        private readonly ApplicationDbContext _context;

        public SidebarController(ApplicationDbContext context)
        {
            _context = context;
        }

        public async Task<IActionResult> PopularPosts()
        {
            return PartialView(await _context.Posts
                .Where(p => p.IsActive == true)
                .OrderByDescending(p => p.Id)
                .Take(5)
                .ToListAsync());
        }
    }
}

And this is my partial view PopularPosts.cshtml:

@model IEnumerable<GoBaron.Front.Models.Post>
@using GoBaron.Front.Data.Extensions

@if (Model.Any())
{
    <div class="sidebar-item" id="topTrending">
        <header class="sidebar-item__header">
            <h1>Najpopularniejsze</h1>
        </header>
        <div class="sidebar-item__content">
            @foreach (var item in Model)
            {
                <a asp-controller="Post" asp-action="Details" asp-route-id="@item.Id" asp-route-slug="@item.Title.ConvertToSlug()" title="@item.Title">
                    <div class="sidebar-post-item" style="background-image:url('@item.PosterUrl')">
                        <span class="sidebar-post-item__counter">+5</span>
                        <span class="sidebar-post-item__title">@item.Title</span>
                    </div>
                </a>
            }
        </div>
    </div>
}

When I want to include the popular posts sidebar, for example in the layout, I just add:

@await Html.PartialAsync("~/Views/Sidebar/PopularPosts.cshtml")

回答1:

This is a good use case for a View Component instead of a partial view. You not only need to render a view, you also need to execute that bit of logic that loads the Popular Posts from the db.

Instead of a Controller, create a new ViewComponent class. This contains the logic that prepares the model for the view component in its InvokeAsync method, and it could take any number of parameters. In your case InvokeAsync takes no parameters, will load the popular posts from the db and will render the view component view:

public class PopularPostsViewComponent: ViewComponent
{
    private readonly ApplicationDbContext _context;

    public PopularPostsViewComponent(ApplicationDbContext context)
    {
        _context = context;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        var posts = await _context.Posts
            .Where(p => p.IsActive == true)
            .OrderByDescending(p => p.Id)
            .Take(5)
            .ToListAsync();

        return View(posts);
    }
}

Now you need to create the view for that view component. The default view name Default.cshtml (as when you do return View() without specifying a view name) and it should be located in Views/Shared/Components/PopularPosts/Default.cshtml. This will basically be the same as your current partial view:

@model IEnumerable<GoBaron.Front.Models.Post>
@using GoBaron.Front.Data.Extensions

@if (Model.Any())
{
    <div class="sidebar-item" id="topTrending">
        <header class="sidebar-item__header">
            <h1>Najpopularniejsze</h1>
        </header>
        <div class="sidebar-item__content">
            @foreach (var item in Model)
            {
                <a asp-controller="Post" asp-action="Details" asp-route-id="@item.Id" asp-route-slug="@item.Title.ConvertToSlug()" title="@item.Title">
                    <div class="sidebar-post-item" style="background-image:url('@item.PosterUrl')">
                        <span class="sidebar-post-item__counter">+5</span>
                        <span class="sidebar-post-item__title">@item.Title</span>
                    </div>
                </a>
            }
        </div>
    </div>
}

Then from any other view, you would render your view component instead of a partial, passing any parameters required by InvokeAsync. In your case you take no parameters so it will be:

@await Component.InvokeAsync("PopularPosts")

Finally, if you are in .Net Core 1.1 or higher, you can also invoke it as a tag helper:

<vc:popular-posts></vc:popular-posts>

PS. I wrote an article describing usages for partials, tag helpers and view components that you might find interesting.