ViewComponent in external assembly cannot be found
Asked Answered
L

2

5

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"
Likable answered 10/5, 2017 at 20:34 Comment(0)
A
8

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")
Ane answered 27/4, 2018 at 14:58 Comment(2)
"Right-click > Properties on the view and set the Build Action to "Embedded resource" - this is the one thing I was missing to get mine working, thanks.Foote
This got me further than any other answer, but it was the setting the view to Embedded Resource that was missing. Also, the .AddApplicationPart() isn't necessary as mine is working without it.Corroborate
P
0

I was able to get a ViewComponent working from an external solution by generating and installing a NuGet package from the "external" assembly into the consuming solution with no problem. I had originally tried to add a reference to the dll without creating my own NuGet package and it did not work.

I'd recommend trying the NuGet package first. If it still doesn't work, can you post both projects so I can help debug?

Poseur answered 6/6, 2017 at 19:1 Comment(3)
Would it help this answer to give instructions on generating & installing a NuGet package?Conceit
I somehow managed to get it working both ways. But for what you ask simply make sure the external assembly where your ViewComponent is located is an ASP.NET Core project. Click on the project properties and then on the Package tab, there check the Build NuGet Package option which is the first. Upon successful build the .nupkg will be in your bin\Release or bin\Debug folder. Copy to your own repository and install from there. I added a build task to automatically deploy the built NuGet package into my own NuGet (local) repository.Likable
That is how I did the NuGet package as well. If you were able to get it to work as a dll reference, what did you do differently? When I included it in the package as a dll, I kept getting the view component could not be found error.Poseur

© 2022 - 2024 — McMap. All rights reserved.