Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC 6: view components in a separate assembly

Tags:

I'd like to define view components (which are new in ASP.NET MVC 6) in a separate assembly from the MVC 6 web startup project so that I can reuse them in multiple web projects. A sample solution might look like this:

  • BookStore.Components (houses common view components)
  • BookStore.Web1 (references BookStore.Components)
  • BookStore.Web2 (references BookStore.Components)

I created a new Class Library (Package) and created a view component inside. I also created the view following the nested folder convention. My BookStore.Components project looks like this:

enter image description here

When I try to invoke this view component from my web project:

@Component.Invoke("BookOfTheMonth")

...I get a 500 error with an empty content body. It seems like the ViewComponent class is discovered, but the razor view for the component isn't.

I also tried to extend DefaultViewComponentDescriptorProvider so that view components from the BookStore.Components assembly can be discovered:

Defined an AssemblyProvider

 public class AssemblyProvider : IAssemblyProvider
    {
        public IEnumerable<Assembly> CandidateAssemblies
        {
            get
            {
                yield return typeof(AssemblyProvider).Assembly;
                yield return typeof(BookStore.Components.BookOfTheMonthViewComponent).Assembly;
            }
        }
    }

Registered AssemblyProvider using Autofac

builder.RegisterType<AssemblyProvider>()
    .AsImplementedInterfaces();

builder.RegisterType<DefaultViewComponentDescriptorProvider>()
    .AsImplementedInterfaces();

I'm not sure if the registration of DefaultViewComponentDescriptorProvider above is needed or not, so I tried with and without it, but I still get a 500 error on a page where the view component is invoked.

How can I invoke a view component that lives in a separate assembly from the MVC6 web project?

like image 506
Johnny Oshika Avatar asked Dec 12 '15 06:12

Johnny Oshika


People also ask

How do you use ViewComponent?

The view component class A view component class can be created by any of the following: Deriving from ViewComponent. Decorating a class with the [ViewComponent] attribute, or deriving from a class with the [ViewComponent] attribute. Creating a class where the name ends with the suffix ViewComponent.

What is the difference between Razor view and Razor page?

The difference between them is that View Pages are Razor views that are used to provide the HTML representations (aka views) for services in much the same way View Pages work for MVC Controllers.

What is the difference between view and partial view?

Views are the general result of a page that results in a display. It's the highest level container except the masterpage. While a partial view is for a small piece of content that may be reused on different pages, or multiple times in a page.

How do I create a partial view in .NET core?

The method name contains Async will be rendered for the asynchronous code. The Render methods result need to be written directly to the response. If we want to Create Partial View, then we need to right click on the view folder > click on the folder > select Add > click on View.


2 Answers

Update 2017-03-09

Things have changed a bit in Visual Studio 2017 using MS Build. Luckily it's much simpler. Here's how to get this to work:

In the external assembly, add this to the csproj file:

<ItemGroup>
   <EmbeddedResource Include="Views/**/*.cshtml" />
</ItemGroup>

In the main web project, add this NuGet package: Microsoft.Extensions.FileProviders.Embedded

Then in Startup, add the external assembly to the list of File Providers:

    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.FileProviders.Add(new EmbeddedFileProvider(
             typeof(SampleClassInAssembly).Assembly
             # Prior to .Net Standard 2.0
             # typeof(SampleClassInAssembly).GetTypeInfo().Assembly
        ));
    });

I'll leave the original answer below for now, in case people are still trying to get this to work with older versions of .Net Core and project.json.

================================================================

Here are the steps to make this work.

  • Make sure your view structure in the components assembly is the same as your web project. Note that there was a mistake in the screenshot that I posted along with my question.
  • Register CompositeFileProvider in Startup.cs of the web project:

    services.Configure<RazorViewEngineOptions>(options =>
    {
        options.FileProvider = new CompositeFileProvider(
            new EmbeddedFileProvider(
                typeof(BookOfTheMonthViewComponent).GetTypeInfo().Assembly,
                "BookStore.Components"
            ),
            options.FileProvider
        );
    });
    

Both CompositeFileProvider and EmbeddedFileProvider are new, so you'll need to get these from the aspnetvnext NuGet feed. I did this by adding this source:

enter image description here

Add the dependencies in project.json:

"Microsoft.AspNet.FileProviders.Composite": "1.0.0-*",
"Microsoft.AspNet.FileProviders.Embedded": "1.0.0-*",

Lastly, add this to the project.json of the Components assembly:

"resource": "Views/**"

That should be enough to get this working.

Here is a working demo: https://github.com/johnnyoshika/mvc6-view-components/tree/master

This answer was formulated from this discussion here: https://github.com/aspnet/Mvc/issues/3750

Update 2016-01-15 There is currently one painful problem with external view components. Any changes you make to the view cshtml file does not automatically get recompiled. Even a forced Visual Studio clean and rebuild doesn't do it. You need to change a .cs file in the components assembly in order to trigger a view recompilation, but it looks like this is something that will be corrected in the future. The reason for this problem is explained here: https://github.com/aspnet/Mvc/issues/3750#issuecomment-171765303

like image 67
Johnny Oshika Avatar answered Oct 04 '22 18:10

Johnny Oshika


I have done some researching on Github and found that PhysicalFileProvider (link) IFileInfo GetFileInfo(string subpath) method is used by Razor engine (link) for getting real files to compile.

Current implementation of this method

public IFileInfo GetFileInfo(string subpath)
{
     if (string.IsNullOrEmpty(subpath))
     {
         return new NotFoundFileInfo(subpath);
     }

     // Relative paths starting with a leading slash okay
     if (subpath.StartsWith("/", StringComparison.Ordinal))
     {
         subpath = subpath.Substring(1);
     }

     // Absolute paths not permitted.
     if (Path.IsPathRooted(subpath))
     {
         return new NotFoundFileInfo(subpath);
     }

     var fullPath = GetFullPath(subpath);
     if (fullPath == null)
     {
         return new NotFoundFileInfo(subpath);
     }

     var fileInfo = new FileInfo(fullPath);
     if (FileSystemInfoHelper.IsHiddenFile(fileInfo))
     {
         return new NotFoundFileInfo(subpath);
     }

     return new PhysicalFileInfo(_filesWatcher, fileInfo);
}

private string GetFullPath(string path)
{
    var fullPath = Path.GetFullPath(Path.Combine(Root, path));
    if (!IsUnderneathRoot(fullPath))
    {
        return null;
    }
    return fullPath;
}

We can see here that absolute paths nor permitted and the GetFullPath method combines path with Root which is your main web application root path.

So I assume that u can't open ViewComponent from other folder than the current one.

like image 39
Mike Avatar answered Oct 04 '22 20:10

Mike