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.