Extending entities without editing the nopCommerce (3.30) core

This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.
Hace 10 años
Hello,

I'm working on a couple of nopCommerce plugins. To ensure that I can perform nopCommerce updates as easy as possible, I would like to touch the core packages by no means. But that makes it pretty laborious to extend entities.

I think it would be useful to show you how I did it. After that, maybe someone can show me an easier way to extend entities without touching the core packages.

After some research in this forum, I decided to use generic attributes to extend my entities. For example, I added a packaging unit to the entity "product". This is pretty easy:

To get the packaging unit for an entity I use the following code:

int packagingUnit = product.GetAttribute<int>( CustomProductAttributeNames.PackagingUnit );


To save the packaging unit into an entity I use the SaveAttribute-method of the IGenericAttributeService:

_genericAttributeService.SaveAttribute( product, CustomProductAttributeNames.PackagingUnit, model.PackagingUnit );


When it comes to displaying the new properties, it gets more complicated. Is there an easy way to display these new properties in the edit-view of the product entity?

Here is what I did:
At first a created a new controller which inherits from ProductController to intercept the Edit action. I also created a new class named "CustomProductModel" which inherits from "ProductModel" which receives the packaging unit after editing the product:

public class CustomProductModel : ProductModel
{
    [NopResourceDisplayName( "Admin.Catalog.Products.Fields.PackagingUnit" )]
    public int PackagingUnit { get; set; }
}


After that I created the new Edit-actions:

[ActionName( "CustomEdit" )]
new public ActionResult Edit( int id )
{
    ActionResult actionResult = base.Edit( id );
    return View( "Edit", ( (ViewResult) actionResult ).Model );
}

[ActionName( "CustomEdit" )]
[HttpPost, ParameterBasedOnFormName( "save-continue", "continueEditing" )]
public ActionResult Edit( CustomProductModel model, bool continueEditing )
{
    /* ... validation and so on ... */

    if ( ModelState.IsValid )
    {
        _genericAttributeService.SaveAttribute( product, CustomProductAttributeNames.PackagingUnit, model.PackagingUnit );
        ActionResult actionResult = base.Edit( model, continueEditing );
    }
    return RedirectToAction( "List", "Product" );
}


To redirect http://my.url.de/Admin/Product/Edit/{id} to my controller actions, I have to create a class that inherits from AreaRegistration, because using a normal RouteProvider to add a custom route will not do the trick in the admin area.


public class CustomAdminAreaRegistration : AreaRegistration
{
    public override string AreaName
    {
         get
         {
             return "Admin";
         }
    }

    public override void RegisterArea( AreaRegistrationContext context )
    {
        context.MapRoute(
            "Admin_Custom",
            "Admin/Product/Edit/{id}",
            new { controller = "CustomProduct", action = "CustomEdit", id = "", area = "Admin" },
            new[] { "My.Namespace.Controllers" }
        );
    }
}


Now I want to create my view for the admin area. It could look like this:

@model My.Namespace.Models.CustomProductModel
@using Nop.Web.Framework;

<table class="adminContent">
    <tr>
        <td class="adminTitle">
            @Html.NopLabelFor( model => model.PackagingUnit )
        </td>
        <td class="adminData">
            @Html.EditorFor( model => model.PackagingUnit )
            @Html.ValidationMessageFor( model => model.PackagingUnit )
        </td>
    </tr>
</table>


But I do not want to touch the core packages. So, if the view is located in my plugin project, I have to have my own ViewEngine.


public class CustomViewEngine : ThemeableRazorViewEngine
{
    public CustomViewEngine()
    {
        PartialViewLocationFormats = new[] { "~/Plugins/My.Plugin/Views/CustomProduct/{0}.cshtml", "~/Administration/Views/Product/{0}.cshtml" };
      ViewLocationFormats = new[] { "~/Plugins/My.Plugin/Views/MyPlugin/{0}.cshtml", "~/Administration/Views/Product/{0}.cshtml" };
    }
}


The ViewEnigne has to added in a custom RouteProvider:

public class CustomRouteProvider : IRouteProvider
{
    public void RegisterRoutes( RouteCollection routes )
    {
        System.Web.Mvc.ViewEngines.Engines.Add( new CustomViewEngine() );
    }

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


To use a custem ViewEngine the pageBaseType-attribute of the pages-tag in the Web.config needs to be adapted.

<pages pageBaseType="Nop.Web.Framework.ViewEngines.Razor.WebViewPage">


Now we need to view the newly created view in the edit-view of the product. Therefore my CustomProductController implements the IConsumer<AdminTabStripCreated> interface. With this interface I can react to the "product-edit" event. When the "product-edit" event is fired, I can use the BlocksToRender property of the eventMessage to add a new tab to the product edit view.

public void HandleEvent( AdminTabStripCreated eventMessage )
{
    if ( eventMessage.TabStripName == "product-edit" )
    {
        int productId = /* reading productId from HttpContext */;
        this.ControllerContext = this.ControllerContext ?? new System.Web.Mvc.ControllerContext( System.Web.HttpContext.Current.Request.RequestContext, this );

          eventMessage.BlocksToRender.Add( new MvcHtmlString( "<script>" +
              "$(document).ready(function() {" +
              "$('#product-edit').data('kendoTabStrip').append(" +
              "[{" +
              "text: 'My Header'," +
              "contentUrl: '/Admin/UrlTo/MyAction/'" + productId +
              "}]);" +
              "});" +
              "</script>" ) );
      }
}



public ActionResult MyAction( int id )
{
    Product product = _productService.GetProductById( id );

    CustomProductModel model = Mapper.Map<Product, CustomProductModel>( product );
    model.PackagingUnit = product.GetAttribute<int>( CustomProductAttributeNames.PackagingUnit ); ;
    return PartialView( "_ProductExtensionTab", model );
}


If I'm not mistaken, that's all.
I think this is a lot of work "just" for extending an existing entity.
Maybe somebody knows an easier way to do it.

Thanks in advance.
Hace 10 años
I realy would appreciate a tutorial or best practices guide for that kind of problem!

If you use this approach, you will get in trouble, if there is another plugin using the same approach. If you would have another plugin to extend the product with another property, then still just one of your controllers would be called an only one set of additonal properties would be saved. So either the packaging unit or the other exta property would be saved, but not both.
Hace 10 años
Great post, but i can not believe, that it should be that complicated?

Why don't you just modify the original Classes?
Hace 10 años
Mazo wrote:
Great post, but i can not believe, that it should be that complicated?

Why don't you just modify the original Classes?


It becomes harder to upgrade a customised nopCommerce to new official versions when they are released.

I've basically created my own 'Custom' folder inside that project and am putting partial classes there to extend the core functionality with what I need. I've applied this to domain objects in nop.core and for the EF mapping inside nop.data
Hace 9 años
sproaty wrote:
Why don't you just modify the original Classes?

It becomes harder to upgrade a customised nopCommerce to new official versions when they are released.

I've basically created my own 'Custom' folder inside that project and am putting partial classes there to extend the core functionality with what I need. I've applied this to domain objects in nop.core and for the EF mapping inside nop.data


This is what I want to do.  Now that you have lived with this for a while (and probably refined it), would you please post an example of your recommended best practice?  Thanks!
Hace 9 años
I don't know if this helps but it is a real cool feature of MVC that is easy to implement in nopCommerce, it lets you intercept an action either before or after it has executed and will insert some custom code at that point. Like in a plugin for those of us who like to keep the upgrades simple.

http://www.pronopcommerce.com/overriding-intercepting-nopcommerce-controllers-and-actions?utm_source=nopcommerce&utm_campaign=forum_blog
Hace 8 años
Thanks for your post, it is really help me lot. I am developing a plugin which same scenario which you have described in your post. I have followed all your steps, all are working like char. But only the view is not showing in admin product section. I have inject a html control in product page. If you please guide me then it will be very helpful for me.
Hace 8 años
Really nice post.i have use this approach bt register route in admin area not works every time when application get restart so some time route net get register and page not found error occure.
So can you provide which register the plugin route everytime whenever application get and works 100%.
Hace 6 años
sproaty wrote:
Great post, but i can not believe, that it should be that complicated?

Why don't you just modify the original Classes?

It becomes harder to upgrade a customised nopCommerce to new official versions when they are released.

I've basically created my own 'Custom' folder inside that project and am putting partial classes there to extend the core functionality with what I need. I've applied this to domain objects in nop.core and for the EF mapping inside nop.data


'Custom' folder ,this seems to be a good solution to avoid complication and it is update friendly.
This topic was automatically closed 365 days after the last reply. New replies are no longer allowed.