Using Components in a Nop 4.2 Plugin

6 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
6 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.
3 months 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
3 months 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;

    }
}
2 months ago
That's great! Thanx
1 month 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
1 month 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;
}
1 month ago
Yep... that got the job done! Thanx
2 weeks ago
How do you return more than one compontent view for a Widget Zone?

Example, I want to return 2 Components for PublicWidgetZones.OrderSummaryContentBefore.Equals(widgetZone)

here is my code now:

if (PublicWidgetZones.OrderSummaryContentBefore.Equals(widgetZone))
            {
                return "WidgetsCartFundraiserAlert";
            }

I want to return "WidgetsCartFundraiserCheckoutAttributes" AND  "WidgetsCartFundraiserAlert"
2 weeks ago
Its pretty straight forward to do.

Create a standard cshtml page for WidgetsCartFundraiser that your plugin will use to render on ordersummaryonenttbefore widgetzone

Then on that view, call your real components you want (any number you need) like the following;

@await Component.InvokeAsync("WidgetsCartFundraiserCheckoutAttributes", new { optionalparam= optionalparamvalue })

@await Component.InvokeAsync("WidgetsCartFundraiserAlert", new { optionalparam= optionalparamvalue })

And this is where the core theme of this thread is important. Your 2 components must follow the strict path .../views/shared/components/{nameofcomponent}/default.cshtml

thx