Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewComponent in external assembly cannot be found

I am using the latest VS.2017 updates and templates for an MVC .NET Core web application. I decided I wanted ViewComponents in an external assembly since I read several posts that indicated it was not possible without odd tricks.

I have my main web application and then I created a .NET Framework class library named MySite.Components which is the "external assembly". In it I installed the ViewFeatures NuGet. I created my View component CSHTML in its /Views/Shared/Components/GoogleAdsense/Default.cshtml.

I noticed that my CSPROJ already has the GoogleAdSense as an embedded resource:

 <ItemGroup>
  <None Include="app.config" />
  <None Include="packages.config" />
  <EmbeddedResource Include="Views\Shared\Components\GoogleAdsense\Default.cshtml" />
 </ItemGroup>

The view component is actually quite simple:

namespace MySite.Components.ViewComponents {
     [ViewComponent(Name = "GoogleAdsense")]
     public class GoogleAdsense : ViewComponent {
        public async Task<IViewComponentResult> InvokeAsync(string adSlot, string clientId, string adStyle = "")
        {
          var model = await GetConfigAsync(adSlot, clientId, adStyle); 
          return View(model); 
        }

        private Task<GoogleAdUnitCompModel> GetConfigAsync(string adSlot, string clientId, string adStyle)
        {
             GoogleAdUnitCompModel model = new GoogleAdUnitCompModel
           {
            ClientId = clientId,    // apparently we can't access App_Data because there is no AppDomain in .NET core
            SlotNr = adSlot,
            Style = adStyle
           };
           return Task.FromResult(model); 
        }
     }
}

Then in the main project (the ASP.NET Core web application) I installed the File Provider NuGet and modified my Startup:

services.Configure<RazorViewEngineOptions>(options =>
        {
            options.FileProviders.Add(new EmbeddedFileProvider(
                 typeof(MySite.Components.ViewComponents.GoogleAdsense).GetTypeInfo().Assembly,
                 "MySite.Components.ViewComponents"
            ));
        });

Then I try to use the view component in a view like this:

@using MySite.Components.ViewComponents
            :
@Component.InvokeAsync(nameof(GoogleAdsense), new { adSlot = "2700000000", clientId = "ca-pub-0000000000000000", adStyle="" }) 

And I get an error saying

*InvalidOperationException: A view component named 'GoogleAdsense' could not be found.*

Also tried using the notation without nameof() that uses a generic parameter for InvokeAsync but that fails too but with

 *"Argument 1: cannot convert from 'method group' to 'object'"*

And using the TagHelper form simply renders it as an unrecognized HTML:

<vc:GoogleAdsense adSlot = "2700000000" clientId = "ca-pub-0000000000000000"></vc:GoogleAdsense>

Finally, on the Main Assembly (the actual web application) I used the GetManifestResourceNames() on the external assembly type to verify it was embedded and the returned list had it listed as:

[0] = "MySite.Components.Views.Shared.Components.GoogleAdsense.Default.cshtml"
like image 251
Lord of Scripts Avatar asked May 10 '17 20:05

Lord of Scripts


1 Answers

I did a lot of trial-and-error and was finally able to get this working. There's a number of guides on this, but they're all for .NET Core 1.0, and I also found they did not work when using a DLL reference from another solution.

Let's talk about component name first. The component name is determined either by convention or attribute. To name by convention, the class name must end in "ViewComponent", and then the component name will be everything prior to "ViewComponent" (just like Controller names work). If you just decorate the class with [ViewComponent], the component name will explicitly be the class name. You can also directly set the name to something else with the attribute's Name parameter.

All three of these examples produce a component name of "GoogleAdsense".

public class GoogleAdsenseViewComponent : ViewComponent { }

[ViewComponent]
public class GoogleAdsense : ViewComponent { }

[ViewComponent(Name = "GoogleAdsense")]
public class Foo: ViewComponent { }

After that, be sure your views are in the proper folder structure.

├── Views
│   ├── Shared
│   │   ├── Components
│   │   │   ├── GoogleAdsense <--component name
│   │   │   │   ├── Default.cshtml

Then, the Views must all be included as embedded resources. Right-click > Properties on the view and set the Build Action to "Embedded resource". You can also do this manually in the .csproj (and take advantage of globbing if you have a lot of Views).

  <ItemGroup>
    <EmbeddedResource Include="Views\Shared\Components\GoogleAdsense\Default.cshtml" />
  </ItemGroup>

That's it for the source project. Make note that you must do a build for any changes to your views to show up, since they are being included in the DLL. This seems obvious, but it's a change from how you normally interact with views.

Now to the consuming project. In ConfigureServices in Startup.cs, you must add your component's assembly as both an MVC ApplicationPart and as an EmbeddedFileProvider. The EmbeddedFileProvider gives access to the views embedded in the assembly, and the ApplicationPart sets up MVC to include it in its search paths.

var myAssembly = typeof(My.External.Project.GoogleAdsenseViewComponent).Assembly;

services.AddMvc().AddApplicationPart(myAssembly);

services.Configure<RazorViewEngineOptions>(options =>
{
    options.FileProviders.Add(new EmbeddedFileProvider(myAssembly, "My.External.Project"));
});

If you have multiple ViewComponents in that assembly, this will suffice for all of them. You can optionally provide a base namespace to EmbeddedFileProvider. I have found times when it was necessary and times when it was not, so it is best to just provide it. This namespace should be the Default Namespace property of your project (Properties -> Application -> Default Namespace).

Finally, to invoke the ViewComponent, use the component name. Remember that the component name may differ from the class name. Unless you used [ViewComponent] to set the component name to be the class name, you cannot use nameof.

@await Component.InvokeAsync("GoogleAdsense")
like image 136
friggle Avatar answered Nov 15 '22 07:11

friggle