ASP.NET Core MVC - Common Components/(Partial)Views across applications

By the end of this post you'll have an understanding of how to share common View Components and (Partial)Views across different Web Applications in ASP.NET Core MVC.

I'm in the process of building a website, I'm lazy and don't like writing things twice/maintaining two slightly different copies of code/components.
I recently found the need to share components across two different web applications (they're really 2 parts of the same goal - one is a Blog, the other is the main Web Application). There's a few components I wanted to share, mainly these 2:

  • A "Paging" View Component - given a collection of items and a "totalItems" I want to show a page of items, with current page, other (close) page numbers before/after current page, total number of results, a "showing 1-8 of 16" indicator and "prev" and "next" buttons
  • A "MetaTag" Partial View - given an object, with some information, write that information to the correct MetaTags in the Partial View

In the following post I'm going to walk you through setting up the common MVC project and getting those components/views shared.
All code in this post can be found in the following GitHub repo

Let's begin!

First, you'll need 2 (or more Web Applications) that need to share common components/views and then another to hold those components/views.

I've set up mine in the following way:

I've got 3 projects:

  • CarShop.Web - the site that will be selling the cars
  • CarShop.Blog - our custom-made blogging site
  • CarShop.Mvc.Common - our project for common MVC components/views

Originally I had tried to keep the common MVC stuff in a Class Library project (.dll), but I couldn't find an easy way to get intellisense working for the .cshtml files. And I didn't want to sacrifice that benefit of my dev workflow

Make that common project - a useless Web Project

As above, we've got a web project in our solution, that isn't really a web project. We wont be deploying that to a web server and we really don't ever want it started and serving requests.

Let's disable it...

  1. Empty the contents of the wwwroot folder
  2. Delete the Controllers folder
  3. Delete the appsettings.json, bower.json and bundleconfig.json files
  4. Delete the Startup.cs class. Yep! We're not going to be needing this ever...
  5. Delete the Views folder, we don't need any of the defaults
  6. Modify the Program.cs file, to look like this:
public class Program  
{
    public static void Main(string[] args)
    {
        // This is never run!
    }
}

I initially deleted this class too, but VS started kicking off as it assumes that all ASP.NET Core MVC Web Applications have a Program.cs with a public static void Main() method.
So I just emptied it!

Brilliant, CarShop.Mvc.Common is now NOT a Web Application, however VS still thinks it is, so we'll still get Intellisense support!

That "Web Application" is looking kinda empty:

Building a Common Component

For brevity, I'm going to build a simplistic Component (as the purpose of this post is to show how to use common components across web applications, without going heavily into implementation details of said component)

I'm going to be building a simplistic paging control component. Which is going to show us the current page number (derived from the "page" url query string) and show previous/next buttons, navigating to their respective pages

First, create a Components folder in CarShop.Mvc.Common. And paste in the following code to a class called Pagination:

public class Pagination : ViewComponent  
{
    public IViewComponentResult Invoke(int total)
    {
        // Default to page 1 if no page number specified
        int page = 1;
        var query = HttpContext.Request.Query["page"];
        if (query.FirstOrDefault() != null)
        {
            // Try to get the page number from the query string
            int.TryParse(query.ToString(), out page);
        }

        var model = new PaginationModel
        {
            CurrentPage = page,
            NextPage = page + 1,
            PreviousPage = page - 1,
            TotalResults = total
        };

        return View(model);
    }
}

public class PaginationModel  
{
    public int CurrentPage { get; set; }
    public int TotalResults { get; set; }
    public int NextPage { get; set; }
    public int PreviousPage { get; set; }
}

Next up, we need to create the "View part" for the component.

Add a folder called Views in the root of the project. And also mimic the following Structure:

Then create a new View called Default.cshtml in the Pagination folder.
Then delete the contents as we'll be writing our own.

Copy this view code into the Default.cshtml file:

@{
    @using CarShop.Mvc.Common.Components
    var mdl = Model as PaginationModel;
}
<div class="pagination">  
    <div class="row">
        <div class="col-xs-4">
            <a class="btn btn-primary" href="?page=@mdl.PreviousPage">Previous</a>
        </div>
        <div class="col-xs-4">
            <p>Page: @mdl.CurrentPage</p>
        </div>
        <div class="col-xs-4">
            <a class="btn btn-primary" href="?page=@mdl.NextPage">Next</a>
        </div>
    </div>
</div>  

NOTE: You need to set the File Properties>Build Action to Embedded resource

Building a Common Partial View

Again, for brevity I'm just going to demonstrate a very light example of a Partial View which takes in an object (defining properties for HTML Meta Tags) and the View which will insert those meta tags into the page.

The main difference between Partial Views and View Components is that a Partial View should not really contain much code. Partial Views should be used mainly for splitting out parts of common HTML. View Components are used when there is code that needs to be run (as in my example above, we needed to get the current page number and work out a few other page numbers too).

First create the following structure in the root of the CarShop.Mvc.Common project: Partials>Meta
Now create a new (empty) View in the newly created Meta folder and call it _MetaTags.cshtml

Copy the following code into the view:

@using CarShop.Mvc.Common.Models.Meta;

@{
    var metaTags = ViewData["MetaTags"] as MetaTags;
    if (metaTags != null)
    {
        // Do the tags
        <meta name="description" content="@metaTags.Description" />
        <meta name="keywords" content="@metaTags.Keywords" />
        <meta name="image" content="@metaTags.ImageUrls.FirstOrDefault()" />
        <meta property="og:site_name" content="Car Shop Ltd." />
        <meta property="og:title" content="@metaTags.Title" />
        <meta property="og:description" content="@metaTags.Description" />
        <meta property="og:language" content="@metaTags.Language" />
        foreach (var url in metaTags.ImageUrls)
        {
            <meta property="og:image" content="@url" />
        }
    }
}

Again.....NOTE: You need to set the File Properties>Build Action to Embedded resource

Also, we'll need a Model to pass to the view, so lets create one in CarShop.Mvc.Common.Models.Meta and call it MetaTags

public class MetaTags  
{
    public string Description { get; set; }
    public string Keywords { get; set; }
    public string Title { get; set; }
    public string Language { get; set; }
    public List<string> ImageUrls { get; set; }
}

Let's use them!

Now we've created the Partial View and the View Component, we need to actually use them.

First off, we need to add a reference to the CarShop.Mvc.Common from the project that wants to use the components.

To load the files we've just created, from the project that doesn't own them, we need to use the EmbeddedFileProvider. So we need to add the following NuGet Package: Microsoft.Extensions.FileProviders.Embedded
...Remember we set the Build Action of the View files in CarShop.Mvc.Common to EmbeddedResource, now we need to use this NuGet package to read them back out.

Next, open the Startup.cs class of the project of either (proper) Web Projects. In your ConfigureServices method add the following code before the services.AddMvc():

services.Configure<RazorViewEngineOptions>(options =>  
{
    options.FileProviders.Add(new CompositeFileProvider(
        new EmbeddedFileProvider(
            typeof(Pagination).GetTypeInfo().Assembly,
            "CarShop.Mvc.Common"
            )));
});

This code is telling the Razor View Engine that when searching for files it's been asked for, that there is another location to check.
In the constructor of EmbeddedFileProvider I'm just using the Pagination class to get the Assembly from that Type. You could easily use the Program class for this too!

Using the Pagination View Component

Add the following line to any part of a page where you want to display the pagination control:

@await Component.InvokeAsync("Pagination", new { total = 6 })

Note that the anonymous object we've created, needs to have properties which match the parameter names on the Pagination Invoke method.

In your browser you should see something like the following (after hitting the buttons a few times):

Obviously this is an over-simplistic example, the pagination component would (in real life) take in the total number, the number of items per page, and a whole load of other information to really get the benefit of re-use.

Using the Meta Tags Partial View

Add the following line (in the head section) to your _Layout.cshtml page:

@Html.Partial("/Partials/Meta/_MetaTags.cshtml")

Now, before you see the Meta Tags appear in your generated Markup, you'll need to populate them

Here's some sample code to populate the Meta Tag from the HomeController, Index method, with some data:

public IActionResult Index()  
{
    ViewData["MetaTags"] = new MetaTags
    {
        Description = "The home page of a car selling website",
        Title = "The Car Shop",
        Keywords = "cars,shop,ferrari,porsche,lamborghini,bugatti",
        Language = "en-GB",
        ImageUrls = new List<string> { "http://assets.bugatti.com/fileadmin/user_upload/Web/Pages/Models/Super_Sport/BUG_super_sport_02.jpg",
            "http://o.aolcdn.com/commerce/autodata/images/USC30FRC151A021001.jpg" }
    };

    return View();
}

Running that Web Application will render the page, and using F12 Developer tools, you should see the following:

Caveats

One caveat I've come across while using this solution is that when I make a change to any of the cshtml files in the CarShop.Mvc.Common project, and refresh my browser, the changes are not reflected. Which makes sense, as they're embedded resources (compiled into the code), so how is your CarShop.Web to know that something's changed in one of view files.

So just remember when editing the view files in the common project, to build when you're done, then refresh the browser window, then you'll see your changes.

In this post I've demonstrated how to share common View Components and Partial Views to two different Web Applications, using a single Common Web Application.

Don't forget to dig around the code on GitHub and get in touch with me on Twitter or in the comments below

ryansouthgate

Software developer, living in Coventry, loves .Net, JavaScript and learning new languages.

Coventry