Using Components in a Nop 4.2 Plugin

5 months ago
Hi folks,

Wondering if  anyone has bumped into an issue that I am experiencing.  I have a nop 4.2 based widget plugin.
This plugin has several business functions going on so I  have multiple components setup to inject into the base platform for various widget zones. Everything is good there.

Now, I have a scenario where I am creating my own Component (unrelated to a any widgetzone).  Basically, I have a View in my plugin that has the following in it;

<div class="side-2">
    @await Component.InvokeAsync("CategoryTreeNavigation", new { currentCategoryId = currentCategoryId })
</div>

This line finds the CategoryTreeNavigation correctly in my plugin which looks as follows;

...
[ViewComponent(Name = "CategoryTreeNavigation")]
    public class CategoryTreeNavigationViewComponent : NopViewComponent
    {        
        public IViewComponentResult Invoke(int currentCategoryId)
        {
            var model = new CategoryHelperModel();
            model.CategoryId = currentCategoryId;

            //return View("~/Plugins/Widgets.MyPlugin/Views/Shared/Components/CategoryTreeNavigation/Default.cshtml", model);
            return View(model);

        }
    }
  ...

Now according to a ton of googling, by default, any component view is going to look for "Default.cshtml" in the Views/Shared/Components/.../Default.chstml path.  This is why it seems ootb, Component Views for Nop.Web are stored where they are.

I tried overriding the location of the view (the line commented out) in above and it just never seems to be able to find the view.

Error
The view 'Components/CategoryTreeNavigation/~/Plugins/Widgets.MyPlugin/Views/Shared/Components/CategoryTreeNavigation/Default.cshtml' was not found. The following locations were searched:
...

In a controller, by default, it looks for the same name as the function or Index.cshtml and the View() function is overloaded to be able to just give a direct path to the view.
In Components, it is "supposed" to work this way (with the exception it looks for default.cshtml) but I can't seem to get the right structure in my plugin (Works fine for the nop.web project).  It keeps prefixing with Components/CategoryTreeNavigation.

Anyone have any luck with creating their own components and invoking them in a plugin?

thx
5 months ago
Figured this out.

So when using your own Components in a plugin, basically, the IViewComponent which Nop implements with (NopViewComponent) to add additional functionality has a strict file path convention.

The full view name that is used in the look up is Components/{YourComponentName}/Default.  So in my case it was Components/CategoryTreeNavigation/Default

Using the full navigation to the view, etc. wasn't working because it looks for the entire name, not just Default.

So I updated my ViewLocationExpander to include the required "Shared" directory.  The {{0}} actually gets replaced by "Components/CategoryTreeNavigation/Default", not just "Default" (which is what you get in a controller).

public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
        {
            if (context.Values.TryGetValue(THEME_KEY, out string theme))
            {
...

viewLocations = new[] {

                        $"/Plugins/Widgets.MyPlugin/Views/Shared/{{0}}.cshtml"
                    }
                    .Concat(viewLocations);
                }
...

After doing all this, the only thing that really doesn't make sense to me is...on a Widget based plugin where the widgetzone is a parameter (Being invoked from the main nop.web project), you can just specify the View name without concern of the prefix
eg. (public IViewComponentResult Invoke(string widgetZone, object additionalData) )

Anyway, hope this post helps anyone who bumps into this.
1 month ago
Hi Chuck,

thank you for sharing your code... one question - did you override the ExpandViewLocations or did you edit the file itself? /Presentation/Nop.Web.Framework/Themes/ThemeableViewLocationExpander.cs
1 month ago
mcselasvegas wrote:
Hi Chuck,

thank you for sharing your code... one question - did you override the ExpandViewLocations or did you edit the file itself? /Presentation/Nop.Web.Framework/Themes/ThemeableViewLocationExpander.cs


Hi there, No, I never touch the nop source...I just created my own CustomViewEngine

Basically, my class looks like this;

using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Razor;
using Nop.Web.Framework;
using Nop.Web.Framework.Themes;
namespace Nop.Plugin.Widgets.CorePlugin.Infrastructure
{
    public class CustomViewEngine : IViewLocationExpander
    {
private const string THEME_KEY = "nop.themename";

        public IEnumerable<string> ExpandViewLocations(ViewLocationExpanderContext context, IEnumerable<string> viewLocations)
        {            
            if (context.Values.TryGetValue(THEME_KEY, out string theme))
            {
                if (context.ViewName == "_CheckoutAttributes")
                {
                    viewLocations = new[] {
                        $"/Plugins/Widgets.CorePlugin/Themes/"+theme+"/Views/Checkout/_CheckoutAttributes.cshtml",
                        $"/Plugins/Widgets.CorePlugin/Views/Checkout/_CheckoutAttributes.cshtml"
                    }
                    .Concat(viewLocations);
                }
                else if (context.ViewName == "Components/OrderSummary/Default" | context.ViewName == "Cart" | context.ViewName == "_GiftCardBox")
                {
                    viewLocations = new[] {
                        $"/Plugins/Widgets.CorePlugin/Views/Shared/{{0}}.cshtml",
                        $"/Views/ShoppingCart/{{0}}.cshtml",
                        $"/Themes/" + theme + "/Views/ShoppingCart/{{0}}.cshtml"
                    }
                       .Concat(viewLocations);
                }
  // A bunch More else / ifs
              
              }

            return viewLocations;
        }

        public void PopulateValues(ViewLocationExpanderContext context)
        {
            if (context.AreaName?.Equals(AreaNames.Admin) ?? false)
                return;

            var themeContext = (IThemeContext)context.ActionContext.HttpContext.RequestServices.GetService(typeof(IThemeContext));
            context.Values[THEME_KEY] = themeContext.WorkingThemeName;
        }
    }
}

In 4.2, you need to initialize your CustomViewEngine from your NopStatup class within your plugin

namespace Nop.Plugin.Widgets.CorePlugin.Infrastructure
{
    public class NopStartup : INopStartup
    {
        public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
        {
            services.Configure<RazorViewEngineOptions>(options =>
            {
                options.ViewLocationExpanders.Add(new CustomViewEngine());
            });

           //Thread safe fix for async support
            services.AddDbContext<CorePluginObjectContext>(optionsBuilder =>
            {
                optionsBuilder.UseSqlServerWithLazyLoading(services);
            }).AddTransient<CorePluginObjectContext>();

        }

        public void Configure(IApplicationBuilder application)
        {
        }

        public int Order => int.MaxValue;

    }
}
1 month ago
That's great! Thanx
2 weeks ago
Hi Chuck,
I was wondering if I could pick your brain again.

you mentioned that you had "a nop 4.2 based widget plugin. This plugin has several business functions going on so I  have multiple components setup to inject into the base platform for various widget zones."

How do you determine what business function is displayed in what widgetzone?

So I have this for the different zones...
public IList<string> GetWidgetZones()
        {
            return new List<string>
            {
                PublicWidgetZones.HeaderMiddle,
                PublicWidgetZones.HeaderAfter
            };
        }


and this for one component
public string GetWidgetViewComponentName(string widgetZone)
        {
            return "HeaderLinksWidget";
        }


So if I have multiple components and multiple widget zones, how do I assign each component to its widgetzone on 4.2 and later?

thanx in advance... Oliver
2 weeks ago
you can use if statements, or a switch if you have many:


public string GetWidgetViewComponentName(string widgetZone)
{
        if (widgetZone == AdminWidgetZones.HeaderMiddle)
            return "HeaderLinksWidget";
        if (widgetZone == AdminWidgetZones.HeaderAfter)
            return "HeaderAfterWidget";

        return string.Empty;
}
2 weeks ago
Yep... that got the job done! Thanx