ViewComponent in external assembly cannot be found - asp.net-core

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"

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")

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?

Related

Asp .Net Core 3.1 Publish wwwroot folder Email Templates

I have a simple application using .net core 3.1 with a very simple ContactUs form. Everything is working fine except Publishing.
I am using Core CLI to publish my project using the following command
dotnet build SampleApp.csproj /p:DeployOnBuild=true /p:PublishProfile=Local
Here is my wwwroot folder:
When I publish my app except for templates folder in wwwroot everything is published properly.
Here is my .csproj file:
My question is how to publish the template folder using CLI?
Unlike ASP.NET MVC 5, during publish in ASP.NET Core .cshtml view files are compiled to .dll file, that's why your templates folder containing .cshtml files in wwwroot folder are being omitted during publish.
Better move your EmailTemplates folder inside Views folder in the project. Then everything will work as expected.
Two ways you can do this :
First Way -->
change .cshtml to .html for html templates and it will surely work without any other change.
Second Way -->
Follow this :
1). Make emailtemplates folder in the parent directory.
2).Copy your all email-templates in emailtemplates folder which was created in parent directory.(use .html instead .cshtml for templates)
3). Then use it in any controller like below.
Example :
public class HomeController : ControllerBase
{
private readonly IHostingEnvironment _environment;
public HomeController(IHostingEnvironment iHostingEnvironment)
{
_environment = iHostingEnvironment;
}
public string LoadTemplate()
{
string FilePath = _environment.ContentRootPath;
var PathWithFolderName = Path.Combine(FilePath, "emailtemplates");
var file = PathWithFolderName + "\\" + "contact-us.html";
using (StreamReader r = new StreamReader(file))
{
string html = r.ReadToEnd();
return html;
}
}
}

Is there a way to package an ASP.NET Core app area as a NuGet package?

I'm working on an ASP.NET Core app that I would love to publish as a NuGet package that you can add to any Core web project. The app is actually entirely confined to an area (i.e., /Areas/MyArea) in the project, including controllers, views, service classes, models, views, etc., except for a few pieces. Really, these are the pieces that I'd love to magically add to an existing web app:
The area and everything in it
Its CSS and JS in wwwroot/lib/myapp
Entries in the Startup class
MyApp.json in the root
I know NuGet will restore the package dependencies, but I'm not sure how it would also consider the client-side packages.
Any suggestions? Is NuGet the wrong tool for this?
currently afaik it is not possible to deliver files into the web app from a nuget package. I think there is some discussion and work going on about making that possible in the future.
The way I'm handling that in my projects is to embed the views and the needed static js and css resources which is done like this in project.json:
"buildOptions": {
"embed": [ "Views/", "js/", "css/**" ]
},
I created a controller to serve my static resources:
public class cscsrController : Controller
{
private ContentResult GetContentResult(string resourceName, string contentType)
{
var assembly = typeof(cscsrController).GetTypeInfo().Assembly;
var resourceStream = assembly.GetManifestResourceStream(resourceName);
string payload;
using (var reader = new StreamReader(resourceStream, Encoding.UTF8))
{
payload = reader.ReadToEnd();
}
return new ContentResult
{
ContentType = contentType,
Content = payload,
StatusCode = 200
};
}
[HttpGet]
[AllowAnonymous]
public ContentResult bootstrapdatetimepickercss()
{
return GetContentResult(
"cloudscribe.Core.Web.css.bootstrap-datetimepicker.min.css",
"text/css");
}
[HttpGet]
[AllowAnonymous]
public ContentResult momentwithlocalesjs()
{
return GetContentResult(
"cloudscribe.Core.Web.js.moment-with-locales.min.js",
"text/javascript");
}
}
then I link to the controller action in the views where I need to load the js and/or css.
To make the embedded views work I created an extension method of RazorViewEngineOptions:
public static RazorViewEngineOptions AddEmbeddedViewsForCloudscribeCore(this RazorViewEngineOptions options)
{
options.FileProviders.Add(new EmbeddedFileProvider(
typeof(SiteManager).GetTypeInfo().Assembly,
"cloudscribe.Core.Web"
));
return options;
}
and this must be called from ConfigureServices in the web app Startup like this:
services.AddMvc()
.AddRazorOptions(options =>
{
options.AddEmbeddedViewsForCloudscribeCore();
})
;
this technique should work the same for areas. Note that one cool thing is that users can download the views and install them locally and that will override the use of the embedded views making it easy to customize some or all views. By overriding the views it is also possible to then manually install the js and css locally if desired and change the views to link to those local files if customization is needed. The end result is my nuget has everything it needs so there is just some startup configuration to get things working.
Years later, this is possible in ASP.NET Core > v2.x, by using a Razor class library.

Ninject intercept throwing error. Dynamic proxy

I have a class which I use to bootstrap.
as part of the object creation I use by convention to bind to interfaces.
All works OK until I try to add an interceptor.
public class ContainerBootstrapper : IDisposable
{
StandardKernel _c;
public ContainerBootstrapper()
{
_c =new StandardKernel();
_c.Bind(b => b.FromAssembliesMatching("Facade*.*").SelectAllClasses().BindDefaultInterfaces());
_c.Bind(b => b.FromAssembliesMatching("Object*.*").SelectAllClasses().BindDefaultInterfaces());
_c.Bind(b => b.FromAssembliesMatching("Logger*.*").SelectAllClasses().BindDefaultInterfaces());
//even using the built in ActionInterceptor like this:
_c.Intercept(c => true)
.With(new ActionInterceptor(invocation =>
Console.Write(invocation.Request.Method.Name)));
When this line is hit, I get an error - Error loading Ninject component IAdviceFactory
No such component has been registered in the kernel's component container.
Suggestions:
1) If you have created a custom subclass for KernelBase, ensure that you have properly
implemented the AddComponents() method.
2) Ensure that you have not removed the component from the container via a call to RemoveAll().
3) Ensure you have not accidentally created more than one kernel.
I have at the top:
using Ninject.Extensions.Conventions;
using Ninject.Extensions.Interception.Injection.Dynamic;
using Ninject.Extensions.Interception.Infrastructure.Language;
using Ninject.Extensions.Interception;
and used NuGet for packages. Tried both Dynamic Proxies and LinFu. Both gave same error.
Anyone have any ideas to try?
Thanks in advance.
Turns out that even though I had a reference to the project doing the bootstrapping and I thought all my dlls for ninject where being copied over automatically this was not the case. After moving them manually it worked.

RquireJS with Module in TypeScript

I'm studing TypeScript and RequireJS.
I want to simple module require but module type information missing.
Is there smart solution in such situation?
requirejs(['backbone'], (Backbone) => {
// In this function.
// Backbone is 'any'
});
requirejs(['backbone'], (BackboneRef: Backbone) => {
// error : Type reference cannot refer to container
// 型参照でコンテナー 'Backbone' を参照できません。
});
To do so you need to do the following:
Download backbone.d.ts from https://github.com/borisyankov/DefinitelyTyped, the backbone.d.ts provides typescript strongly-typed interface, if you use IDE like Visual Studio, you can have all the intellisense support to Backbone
(Optional) Config backbone in RequireJS
In your TypeScript class, you can reference Backbone like the following
`
/// <amd-dependency path="backbone" />;
/// <reference path="path/to//backbone.d.ts" />;
export class YourModel extends Backbone.Model {
}
The amd-dependency tells the compiler how to reference backbone, so it would generate the correct define statement in JavaScript.
The reference provides a way to Backbone's definition for typed check.
Hope this helps! TypeScript eliminates the hell of writing long define or require js statement, which can be error-prone in situation where there are lots of dependencies.

EPiServer 7: Trouble adding dojo module to user interface

I have some trouble adding my dojo module to the user interface.It tries to access in the episerver/shell.
I have added a module.conifg;
<module>
<assemblies>
<add assembly="Mobile.Web" />
</assemblies>
<dojoModules>
<add name="MobileWeb" path="Scripts" />
</dojoModules>
</module>
Added my dojo module at ~/ClientResources/js/KeyValueEditor.js,
named the module declare('MobileWeb.js.KeyValueEditor', [widget, templatedMixin] and in my block type:
[ClientEditor(ClientEditingClass = "MobileWeb.js.KeyValueEditor")]
public virtual string KeyValueCategoryData { get; set; }
It works sometimes, but when I changed the dojoModules -> add name attribute to MobileWeb, it wont work anymore.
Anyone knows what this can be?
It looks like the system don’t know where to find client resources.
The name in the dojoModules node is kind of your namespace and the path should point to the folder where Dojo can find resources/scripts for that namespace. This path is relative to your module root directory.
As I understand you probably want to put your JavaScript files in ClientResources/js subfolder and your styles in ClientResources/css subfolder inside your module directory. In this case you can define Dojo module like this:
<dojoModules>
<add name="MobileWeb" path="ClientResources/js" />
</dojoModules>
It means that system will try to find resources in ClientResources/js subfolder in your module directory. When declaring widgets you should follow your namespace and folder structure. You can declare your widget in ClientResources/js/KeyValueEditor.js file like this:
define([
// your imports
],
function (/* imports */) {
return declare("MobileWeb.KeyValueEditor", [_Widget, /* … */ ], {
// implementation
});
});
And then you can use MobileWeb.KeyValueEditor name when you reference your widget in the back-end C# code.
You can find some examples and source code in sample add-on for EPiServer 7.