3 Ways to Display Views in Your nopCommerce Plugins (Embedded Resource, Theme Override and Custom View Engine)

There has been numerous questions posted on nopCommerce forums and StackOverflow about how to make nopCommerce plugins' Views text-editeable as CSHTML files. This is because the most widely known method of displaying Views in nopCommerce is to mark the .CSHTML files as Embedded Resource, which essentially makes the Views behave like binary files (such as DLLs), which in turn makes the Views uneditable in text editors. In fact, you can't even find the CSHTML files on the compiled plugins folder!

For example, the figure below is how a compiled nopCommerce plugin looks like. You notice that there's no .CSHTML that you can edit. Why is this a bad thing? Because if you are developing an nopCommerce plugin for your clients, that means there is no easy way they can edit how the plugin renders on their shop.

Typical nopCommerce plugin files

How can we go about solving this limitation? Read on to learn!

The Original Method - "Embedded Resource"

This is the method that's most widely known by nopCommerce developers, because the official tutorials have been using this method. This method goes by making the .CSHTML file an embedded resource (shown below), and then referece it in code by its Fully-qualified Name like how we would refer to a C# Class.

nopCommerce Plugin - Marking a View as an Embedded Resource

The fully-qualified name for a View file that's been marked as Embedded Resource would be:

{default_namespace}.{view_folder_name}.{nested_sub_folder_name_if_any}.{view_name_without_cshtml_extension}

For example, the default "DiscountRules.BillingCountry" returns its "Configure" View by using the following code:

Fully-qualified Name for Views marked as Embedded Resource

The Better Method - "Theme Override"

The "Embedded Resource" method is very inflexible because you cannot edit the Views as you wish. But wait! You actually can! You can override the View in your nopCommerce Theme, and the good news is: it's very easy to do so!

Firstly, create a folder under your theme's Views folder, and name it CONTROLLER_NAME (without the trailing 'Controller'). Taking the original NivoSlider plugin as example, the folder name would be WidgetsNivoSlider because the Controller name is WidgetsNivoSliderController. Then, copy the View you want to override to your theme, and name it using the fully-qualified name (with CSHTML extension). So eventually you have a setup as follow:

The "Theme Override" method of display nopCommerce plugin views

The Advanced Method - "Custom View Engine"

The "Theme Overridge" method is a major improvement over the "Embedded Resource" method, but it does have its own downside. Because the Views is located under the Themes folder, it means that on deployment your clients have to perform 2 operations for their plugin to run: first copying the necessary plugin files to the Plugins folder, then the necessary View files to the Themes folder.

To simplify the deployment process, we can employ the third method of specifying Views for plugins, the "Custom View Engine" method. If you are familiar enough with ASP.Net MVC, you know that Razor is just an extension of the view engine architecture employed by ASP.Net MVC. In other words, we can create any other view engines just like how Microsoft created Razor!

We are, of course, not going to write a completely new view engine. But we can make use of ASP.Net MVC's extensible architecture by entending the Razor view engine to search for custom paths for Views! In fact, that's essentially how nopCommerce theming system works! I am not going to go into this part as I believe it deserve an entire blog post on its own. If you are interested, you can look at the source code, starting from Nop.Web.Framework.Themes.ThemeableVirtualPathProviderViewEngine.cs and navigate from there!

Without further ado, let's just jump into the code! Let's create a folder named ViewEngines directly under your plugin, and add a new class CustomViewEngine.cs to the folder.

Creating custom view engine in nopCommerce plugin

Then, add the following code to CustomViewEngine.cs. Make sure you replace panoRazzi.ExpressCheckout with your own namespace.

CustomViewEngine code

What this code does is that it adds additional locations for the ViewEngine to search for Views. The placeholder {0} will be replaced by your Controller's Action name.

But we are not done yet! With the new ViewEngine in place, we need to register it such that nopCommerce can pick it up! We want the new ViewEngine to be registered on Application_Start, and there are 1 class that gets called on Application_Start and is frequently used by plugins - RouteProvider.cs. Let'sadd the highlighted code to RouteProvider.cs:

Registering CustomViewEngine in RouteProvider.cs

This is actually a little hacky, but I think it doesn't matter much. Use it at your own risk though!

By the way, after adding this line of code, it means that nopCommerce will be able to pick up your CustomViewEngine as another way to render .CSHTML Views, and it'll actually search for Views in the path specified in your CustomViewEngine (together with the other paths that originally work in nopCommerce; more on this later)!

With the CustomViewEngine in place, you can now include Views in your plugins (mark it as Content instead of Embedded Resource) as shown below. Please note the web.config have to be present in the plugin's Views folder. You need to copy the same file from ~/Views folder.

Views folder of a plugin, with the CSHTML marked as Content instead of Embedded Resource

Now, in your plugin's Controller, you can use the normal way of returning Views in the Actions (no need for the fully-qualified name), such as:

Returning Views from your plugin's Action

Bonus: "Theme Override" On Top of "Custom View Engines"

Now you may ask: does "Theme Override" still work when we implement "Custom View Engines"? The answer is YES, using the same way you override "Embedded Resource" Views (only with a different View name)! Why would we want to do this? Because we want to supply our clients with default, viewable and editable set of CSHTML Views; while still allowing them to quickly copy and paste the default Views into the Themes folder and override the rendering.

Explaining using the figure below, our plugin has a default implementation of Display.cshtml (left part of the figure). If at any time the customers want to customize the rendering of Display.cshtml, they can simply copy Display.cshtml (bottom part of the figure) into ~/Themes/THEME_NAME/Views/ExpressCheckout (right part of the figure).

Using "Theme Override" with "Custom View Engine"

On the other hand, they wouldn't be able to know how the default Display.cshtml implementation is like if the View is rendered using the "Embedded Resource" method. "Theme Override" is definitely powerful when implemented together with "Custom View Engines"!

Conclusion

With a little bit of hack, we can actually implement a very powerful View-rendering mechanism for plugins, thank to the comprehensive theming architecture of nopCommerce (which is built on top of the extensible View Engine architecture of ASP.Net MVC).

If you have any question about nopCommerce theming architecture or plugin View-rendering mechanism, please feel free to leave your comments below! I'll also hopefully be able to do a lower-level analysis of nopCommerce theming architecture in the near future!

Leave your comment
*
Only registered users can leave comments.
Comments
5/11/2013 1:39 AM
nice blog man, keep up the good work!
5/11/2013 2:09 AM
Thanks! :)
6/20/2013 10:01 AM
Thanks for the hint. I will definitely use this "hack"
6/20/2013 10:59 AM
No problem Alex. :)
6/20/2013 9:08 PM
I implemented this hint. but noticed  that bonus "Theme Override" On Top of "Custom View Engines" is not  bonus. This is a mandatory action. View should be added to the theme views or to the main section of views.
After adding  CustomViewEngine the nop engine is not looking for view in embeded resources and without added view to other folders generates exceprion:

Exception Details: System.InvalidOperationException: The view 'Configure' or its master was not found or no view engine supports the searched locations. The following locations were searched:
~/Themes/[theme]/Views/[plugin]/Configure.cshtml
~/Themes/[theme]/Views/[plugin]/Configure.vbhtml
~/Themes/[theme]/Views/Shared/Configure.cshtml
~/Themes/[theme]/Views/Shared/Configure.vbhtml
~/Views/[plugin]/Configure.cshtml
~/Views/[plugin]/Configure.vbhtml
~/Views/Shared/Configure.cshtml
~/Views/Shared/Configure.vbhtml
~/Administration/Views/[plugin]/Configure.cshtml
~/Administration/Views/[plugin]/Configure.vbhtml
~/Administration/Views/Shared/Configure.cshtml
~/Administration/Views/Shared/Configure.vbhtml
~/Plugins/Nop.Plugin.Widgets.[plugin]/Views/Configure.cshtml

may be it is may wrong I don't know but it's working if I will add view to theme or build plugin without hint.
6/20/2013 9:25 PM
"After adding  CustomViewEngine the nop engine is not looking for view in embeded resources and..."

If you are using CustomViewEngine, then you do not mark it as Embedded Resource. If you mark it as Embedded Resource, there's no point implementing the CustomViewEngine anyway.

The point of using CustomViewEngine is to be able to let the View be a normal file that will be output to the Plugins folder as .cshtml. And if you mark it as Embedded Resource, that means it'll not be output as an individual .cshtml, which makes the CustomViewEngine breaks: because what CustomViewEngine does is to find that physical .cshtml files in the path we've specified.

I hope this is clear. :)
6/20/2013 10:18 PM
It's working! I was inattentive.
Thanks)
6/20/2013 10:50 PM
No problem Alex. :)
9/24/2013 5:47 PM
Hello

I am wondering if is possible to override admin order edit view? i just want to add an extra method (button) and add it to existing view?
Which way is the best?

Best regards
9/24/2013 7:06 PM
Hi Andrej,

Here's a little experiment you can try: remove ~/Administration/Views/Home/Index.cshtml and ~/Views/Home/Index.cshtml and try to access admin home page. You'll see a list of locations that nopCommerce searches for the views. :)
10/1/2013 3:09 PM
Hey, nice blog..

I just want to ask a question that I am creating a plug-in by referencing Nopcommerce's in-built plug-in, but still i am no able to get Layout & Html Helpers in my CSHTML Page. Can you help me. Please.

I have copied its Plug-in and done necessary changes according to my plug-in.
10/1/2013 9:38 PM
Hi Abhay,

I don't think I know what you mean, can you please elaborate more? :)
11/11/2013 6:18 PM
Hi Woon Cherk Lam,

I am creating a plugin in nopcommerce, in which I want to display my own view in ProductDetails Page like if my plugin is enabled then My View should be replaced with ProductImage in ProductDetails page and if it is set to disable then nopcommerce's in-built ProductImage should be displayed in ProductDetails Page.

Can you help me how to do this using my plugin?

Give an example if possible. It will be great help for me to understand.

Thanks in anticipation.
11/11/2013 8:33 PM
Hi Abhay,

Following the concept laid out in the blog, you should be able make your view take precedence by naming your view the same as the view you want to override.
11/18/2013 3:24 PM
I have already tried those links and could not find any solution.

Actually i have created one partial view that i want to replace with existing partial view in Product Details Page.

My Route is as follows:

using System.Web.Mvc;
using System.Web.Routing;
using Nop.Web.Framework.Mvc.Routes;

namespace Nop.Plugin.Indies.ProductVideo
{
    public partial class RouteProvider : IRouteProvider
    {
        public void RegisterRoutes(RouteCollection routes)
        {
         //   System.Web.Mvc.ViewEngines.Engines.Add(new MyCustomViewEngine());
            
            routes.MapRoute("Plugin.Indies.ProductVideo.Configure",
                 "Plugins/IndiesProductVideo/Configure",
                 new { controller = "ProductVideo", action = "Configure" },
                 new[] { "Nop.Plugin.Indies.ProductVideo.Controllers" }
            );

            routes.MapRoute("Plugin.Indies.ProductVideo.VideoList",
                "Plugins/IndiesProductVideo/VideoList",
                new { controller = "ProductVideo", action = "VideoList" },
                new[] { "Nop.Plugin.Indies.ProductVideo.Controllers" }
           );

            routes.MapRoute("Plugin.Indies.ProductVideo._ProductDetailsPictures",
                "Plugins/IndiesProductVideo/_ProductDetailsPictures",
                new { controller = "ProductVideo", action = "_ProductDetailsPictures" },
                new[] { "Nop.Plugin.Indies.ProductVideo.Controllers" }
           );

        }
        public int Priority
        {
            get
            {
                return 100;
            }
        }
    }
}

MY Controller Code:

        [ChildActionOnly]
        public ActionResult _ProductDetailsPictures()
        {
            var model = this._productVideoService.GetVideoListByProductid(7);
            return PartialView("Nop.Plugin.Indies.ProductVideo.Views.ProductVideo._ProductDetailsPictures", model);
            //return View(model);
        }

In Above Route i want third route to be replaced with @Html.Partial("_ProductDetailsPictures", Model) of ProductTemplate.Simple.cshtml page in ProductDetails page but don't know where i am doing mistake.
1/23/2014 11:22 PM
Hi Woon,

I did have an issue with "The name 'model' does not exist in the current context" which stumped me for a bit - I fixed it by making sure the Web.config file was deployed to the plugins binary folder as well...

Many Thanks,

Marc
1/27/2014 6:40 PM
I Had another issue using this techniques on 2 plugins - the same view was coming through for both of them, even though there were 2 unique CustomViewEngine() classes in their own namespaces - I resolved this by renaming the second CustomViewEngine() to be different from the other plugin, and I had to rename the View that is registered on start up in the GetDisplayWidgetRoute, they were both called PublicInfo - I changed the name of one and them the second plugin found it's own View, I think this maybe an MVC caching issue.
3/26/2014 6:53 PM
Hi Woon,

I've been following your blog entitled

3 Ways to Display Views in Your nopCommerce Plugins..

And have used the CustomViewEngine Method and have followed your instructions to the letter but still nopCommerce goes to the old view that I'm trying to override.

I have set my view to Content instead of Embedded Resource and have specified that this should be sent to the output directory always.

This particular view works as an embedded resource but I just can't get it to work with the view engine.  Any help would be greatly appreciated.

Regards

Andrew
4/18/2014 5:44 AM
Are you able to have 2 plugin projects with 2 different custom ViewEngines running on the site at the same time?

I created one plugin (a widget one) and followed your article.  It worked perfectly.  I have another plugin (an ExternalAuth one) that was using the views as embedded resources, but I followed the same steps as before to convert it.

Now that I have them both deployed, the 2nd plugin is saying the model item passed into the dictionary is of the type from the 1st plugin!  I've poured through the project and made sure nothing referenced or was called by the other plugin, but it's all namespaced correctly.

I have also renamed the ViewEngine in the 2nd plugin, even though they are in different namespaces.

Any ideas?
4/18/2014 9:17 PM
Please ignore my last comment above.  I noticed Marc was having the same issue, and after following his steps, it worked.

Does anyone know why that happens?
4/18/2014 9:28 PM
Hi Ron,

I am not sure what is happening. I believe I need to run a debug to find out the issue. Will post back my results as soon as I have time to test. :D