Develop And Understand  Widget plugin in nopCommerce 4.10 with Real-time Communication

Widget plugin tuorial in nopCommerce by nopStation

In this tutorial, our experienced nopCommerce developer Abu Sina has inscribed the nopcommerce widget plugin on the 4.10 version along with the concept of SignalR.

Widget plugin is a special kind of nopcommerce plugin which provides some feature(s) to one or many widget zones.

The plugin that we are going to develop is very simple. But it will cover more or less most of the feature that a real-life plugin should have, like -Services, DomainClass, ModelClass, Component, View, DI, database access (creating simple table), general CRUD operation, adding menu to the admin panel from the plugin, showing specific output to the desire public widget zone etc.

Let's consider a situation where a shop-owner needs to broadcast an announcement to all the customer who is currently present at the shop. The shop-owner wants it dynamic having complete control over this announcement. So we think it will be good to make a widget plugin which will take announcement related data and broadcast this data to the public site and for real-time communication, we decided to go with SignalR.

So let’s jump into the code for creating this widget plugin.

First, we have to create a class library with the name “Nop.Plugin.Widget.LiveAnnouncement” under Plugins folder. It is good to follow the conventional system  like Nop.Plugin.TypeOfPlugin.YourDesireNameOfPlugin.

Now we have to change the name Class1 to LiveAnnouncementPlugin. One of the major task to make any type of plugin of nopcommerce is to inherit “BasePlugin” abstract base class. Because it contains methods and property which are common for all plugins. It has only one property which contains the low-level information of a plugin. All methods and property of this base class are virtual so anyone can override any of them or all.

Let’s override Install and Uninstall methods. Here, we want to override these two methods because we need to create a table at the install time of this plugin and some resource-string and other related work like MarkPluginAsInstalled. To make the plugin as installed, we need to call the base class install method. Also at uninstall time we want to delete the table and the resource-string and MarkPluginAsUninstalled.

To add menu item at the admin panel we need to implement IAdminMenuPlugin interface. As we are going to develop a Widget plugin so we need to implement the widget plugin interface. How the Widget will work, will be described later. All Widgetzone name is listed at the infrastructure of Nop.Web.Framework.

The LiveAnnouncementPlugin class

using Microsoft.AspNetCore.Routing;

using Nop.Core.Plugins;

using Nop.Plugin.Widget.LiveAnnouncement.Data;

using Nop.Services.Cms;

using Nop.Services.Configuration;

using Nop.Services.Localization;

using Nop.Web.Framework.Menu;

using System;

using System.Collections.Generic;

using System.Linq;

using Nop.Web.Framework.Infrastructure;

 

namespace Nop.Plugin.Widget.LiveAnnouncement

{

public class LiveAnnouncementPlugin : BasePlugin, IWidgetPlugin,IAdminMenuPlugin

{

   #region Fields

 

   private readonly LiveAnnouncementObjectContext _context;

   private readonly ILocalizationService _localizationService;

   private readonly ISettingService _settingContext;

 

   #endregion

 

   #region Ctr

 

   public LiveAnnouncementPlugin(LiveAnnouncementObjectContext context, ILocalizationService localizationService, ISettingService settingContext)

   {

       _context = context;

       _localizationService = localizationService;

       _settingContext = settingContext;

   }

 

   #endregion

 

   #region Install / Uninstall

 

   public override void Install()

   {

           _localizationService.AddOrUpdatePluginLocaleResource("Misc.Announcement", "Announcement Create");

           _localizationService.AddOrUpdatePluginLocaleResource("Misc.AnnouncementList", "Announcement List");

 

       _context.InstallSchema();

       base.Install();

   }

   /// <summary>

   /// Uninstall plugin

   /// </summary>

   public override void Uninstall()

   {

       //settings

       //data

      _context.Uninstall();

           _localizationService.DeletePluginLocaleResource("Misc.Announcement");

       _localizationService.DeletePluginLocaleResource("Misc.AnnouncementList");

 

       base.Uninstall();

   }

 

   #endregion

 

   public void ManageSiteMap(SiteMapNode rootNode)

   {

       var liveAnnouncementPluginNode = rootNode.ChildNodes.FirstOrDefault(x => x.SystemName == "LiveAnnouncement");

       if (liveAnnouncementPluginNode == null)

       {

           liveAnnouncementPluginNode = new SiteMapNode()

           {

                   SystemName = "Live Announcement",

               Title = "Live Announcement",

               Visible = true,

               IconClass = "fa-gear"

           };

               rootNode.ChildNodes.Add(liveAnnouncementPluginNode);

       }

 

           liveAnnouncementPluginNode.ChildNodes.Add(new SiteMapNode()

       {

           Title =  _localizationService.GetResource("Misc.Announcement"),

           Visible = true,

           IconClass = "fa-dot-circle-o",

           Url = "~/Admin/LiveAnnouncement/Announcement"

       });

 

           liveAnnouncementPluginNode.ChildNodes.Add(new SiteMapNode()

       {

           Title = _localizationService.GetResource("Misc.AnnouncementList"),

           Visible = true,

           IconClass = "fa-dot-circle-o",

           Url = "~/Admin/LiveAnnouncement/AnnouncementList"

       });

   }

 

   public IList<string> GetWidgetZones()

   {

       return new List<string>

       {

          PublicWidgetZones.HeaderAfter

       };

   }

 

   public string GetWidgetViewComponentName(string widgetZone)

   {

     return  "LiveAnnouncementView";

   }

}

}

Note: LiveAnnouncementObjectContext is not created yet. But after finishing the domain class and map class we will create and then LiveAnnouncementObjectContext. For now, keep it as it is. You can add reference of other but it will over-write after changing the .csproj file later of the tutorial.

To keep announcement related data, we need to create a table first. To do that we need to add a domain class. Let’s add a class named Announcement under Domain folder. Now, we need to inherit BaseEntity abstract base class. Because nopcommerce use fluent API for configuring entity and generic repository pattern for database operation and the nopcommerce team added the constraint in the type which will be passed to the IRepository interface must be inherited from the BaseEntity abstract base class. There are many generic and extension method which also has the same constraint. Another reason is, this base class has a property which will be the primary key of the table when we will create a map class for configuring our entity.

The Domain class

using System;

using Nop.Core;

 

namespace Nop.Plugin.Widget.LiveAnnouncement.Domain

{

public class Announcement : BaseEntity

{

   public string Name { get; set; }

   public string Body { get; set; }

 

   public bool IsActive { get; set; }

 

   public DateTime CreateDate { get; set; }

}

}

Now we need to create an Announcement map class under the Data folder to configure the domain class with basic information. We are inheriting the NopEntityTypeConfiguration class because the configuration is generalizing in this class. You can implement IEntityTypeConfiguration at your plugin directly if you want.

The mapping class

using Microsoft.EntityFrameworkCore;

using Microsoft.EntityFrameworkCore.Metadata.Builders;

using Nop.Data.Mapping;

using Nop.Plugin.Widget.LiveAnnouncement.Domain;

 

namespace Nop.Plugin.Widget.LiveAnnouncement.Data

{

public class AnnouncementMap : NopEntityTypeConfiguration<Announcement>

{

   public override void Configure(EntityTypeBuilder<Announcement> builder)

   {

       builder.ToTable("Announcement");

 

       builder.HasKey(x => x.Id);

       builder.Property(x => x.Name);

       builder.Property(x => x.Body).IsRequired();

       builder.Property(x => x.IsActive);

       builder.Property(x => x.CreateDate);

   }

}

}

At this class(LiveAnnouncementObjectContext) we need to pass our option to the base class constructor. We implement the IDbContext interface because we need to pass the create table sql script through the ExecuteSqlScript extension method whenever the InstallSchema method invoke. To drop the plugin table we need to call the Uninstall method. There is a extension method DropPluginTable which generate the drop dbscript. So the basic setup for database related work for the plugin is done.

 

The LiveAnnouncementObjectContext class

using Microsoft.EntityFrameworkCore;

using Nop.Core;

using Nop.Data;

using Nop.Data.Extensions;

using System;

using System.Collections.Generic;

using System.Data;

using System.Data.Common;

using System.Linq;

 

namespace Nop.Plugin.Widget.LiveAnnouncement.Data

{

public partial class LiveAnnouncementObjectContext : DbContext, IDbContext

{

   #region Ctr

 

   public LiveAnnouncementObjectContext(DbContextOptions<LiveAnnouncementObjectContext> options)

       : base(options)

   {

   }

 

   #endregion

 

   #region Entity

   public virtual new DbSet<TEntity> Set<TEntity>() where TEntity : BaseEntity

   {

       return base.Set<TEntity>();

   }

 

   #endregion

   #region Utility

 

   protected override void OnModelCreating(ModelBuilder modelBuilder)

   {

       modelBuilder.ApplyConfiguration(new AnnouncementMap());

       base.OnModelCreating(modelBuilder);

   }

 

   public void InstallSchema()

   {

       this.ExecuteSqlScript(this.GenerateCreateScript());

   }

   /// <summary>

   /// Uninstall

   /// </summary>

   public void Uninstall()

   {

       //drop the table

 

       this.DropPluginTable("Announcement");

 

   }

 

   public string GenerateCreateScript()

   {

       return Database.GenerateCreateScript();

   }

   public IQueryable<TQuery> QueryFromSql<TQuery>(string sql) where TQuery : class

   {

       return this.Query<TQuery>().FromSql(sql);

   }

 

   public IQueryable<TEntity> EntityFromSql<TEntity>(string sql, params object[] parameters) where TEntity : BaseEntity

   {

       return this.Set<TEntity>().FromSql(CreateSqlWithParameters(sql, parameters), parameters);

   }

 

   protected string CreateSqlWithParameters(string sql, params object[] parameters)

   {

       //add parameters to sql

       for (var i = 0; i <= (parameters?.Length ?? 0) - 1; i++)

       {

           if (!(parameters[i] is DbParameter parameter))

               continue;

 

           sql = $"{sql}{(i > 0 ? "," : string.Empty)} @{parameter.ParameterName}";

 

           //whether parameter is output

           if (parameter.Direction == ParameterDirection.InputOutput || parameter.Direction == ParameterDirection.Output)

               sql = $"{sql} output";

       }

 

       return sql;

   }

 

   #endregion

 

   public virtual void Detach<TEntity>(TEntity entity) where TEntity : BaseEntity

   {

       if (entity == null)

           throw new ArgumentNullException(nameof(entity));

 

       var entityEntry = this.Entry(entity);

       if (entityEntry == null)

           return;

       //set the entity is not being tracked by the context

       entityEntry.State = EntityState.Detached;

   }

 

   public int ExecuteSqlCommand(RawSqlString sql, bool doNotEnsureTransaction = false, int? timeout = null, params object[] parameters)

   {

       using (var transaction = this.Database.BeginTransaction())

       {

           var result = this.Database.ExecuteSqlCommand(sql, parameters);

           transaction.Commit();

           return result;

       }

   }

}

}

Now LiveAnnouncementObjectContext will be available from the LiveAnnouncementPlugin class. Let's call the InstallSchema of the LiveAnnouncementObjectContext class from the LiveAnnouncementPlugin Install method. To do that, we will need to register the dependence of the LiveAnnouncementObjectContext class. We will take DependencyRegister class in the Infrastructure folder. Where we will implement the IDependencyRegistrar interface.

If you go to the LibrariesèNop.CoreèInfrastructureè  NopEngine.cs class, will see that there is a method RegisterDependencies which is called at the startup time. So at the very beginning, the compiler knows the dependencies of this interface. To register PluginDataContext we will need to call the RegisterPluginDataContext extension method with context name.

So, the setup is done for the plugin to install. Before that, we need to add the plugin.json file and also need to change the .csprj according to the nopcommerce core team suggestion. In plugin.json file, we need to add some property according to the PluginDescriptor class or you can copy this file from other plugin and edit it likes bellow.

The json file look like bellow.

{

 "Group": "Widgets",

 "FriendlyName": "LiveAnnouncement",

 "SystemName": "LiveAnnouncement",

 "Version": "1.0",

 "SupportedVersions": ["4.10"],

 "Author": "Sina",

 "DisplayOrder": 1,

 "FileName": "Nop.Plugin.Widget.LiveAnnouncement.dll",

 "Description": "This plugin allows admins to broadcust any update"

}

Do not forget to make the plugin.json file copylocal true.

And the project file needs to be like bellow. Please take the bellow section and past it to the .csproj at edit mode.

 

<Project Sdk="Microsoft.NET.Sdk">

 <PropertyGroup>

<TargetFramework>netcoreapp2.1</TargetFramework>

 </PropertyGroup>

 

 <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

<OutputPath>..\..\Presentation\Nop.Web\Plugins\Widget.LiveAnnouncement</OutputPath>

<OutDir>$(OutputPath)</OutDir>

 </PropertyGroup>

 

 <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">

<OutputPath>..\..\Presentation\Nop.Web\Plugins\Widget.LiveAnnouncement</OutputPath>

<OutDir>$(OutputPath)</OutDir>

 </PropertyGroup>

 <ItemGroup>

<ProjectReference Include="..\..\Presentation\Nop.Web.Framework\Nop.Web.Framework.csproj" />

<ProjectReference Include="..\..\Presentation\Nop.Web\Nop.Web.csproj" />

 </ItemGroup>

 

 <ItemGroup>

<None Update="plugin.json">

 <CopyToOutputDirectory>Always</CopyToOutputDirectory>

</None>

 </ItemGroup>

 <!-- This target execute after "Build" target -->

 <Target Name="NopTarget" AfterTargets="Build">

<!-- Delete unnecessary libraries from plugins path -->

<MSBuild Projects="$(MSBuildProjectDirectory)\..\..\Build\ClearPluginAssemblies.proj" Properties="PluginPath=$(MSBuildProjectDirectory)\$(OutDir)" Targets="NopClear" />

 </Target>

 

</Project>

Now let’s try to understand why I implement the IWidgetPlugin interface. IWidgetPlugin is fully substitute of IPlugin with two extra methods. It is a good example of interface segregation. These two methods have important role for the widgetplugin. Whenever a widget viewcomponent invoke at the public site/ admin site, the solution checks whether any plugin wants to use this zone by GetWidgetZones method if so then GetWidgetViewComponentName method call for rendering that plugin’s viewcomponent.

Create a LiveAnnouncementView viewcomponent at plugin under Components folder and related view(LiveAnnouncement.cshtml) under Viewsè LiveAnnouncementView folder.

 

The LiveAnnouncementView

using Microsoft.AspNetCore.Mvc;

using Nop.Web.Framework.Components;

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

namespace Nop.Plugin.Widget.LiveAnnouncement.Components

{

  [ViewComponent(Name = "LiveAnnouncementView")]

public class AnnouncementViewComponent: NopViewComponent

{

   public IViewComponentResult Invoke(string widgetZone, object additionalData)

   {

return View("~/Plugins/Widget.LiveAnnouncement/Views/LiveAnnouncementView/LiveAnnouncement.cshtml");

   }

}

}

The view of the LiveAnnouncement ViewComponent

@using Nop.Core;

@using Nop.Core.Domain.Seo;

@using Nop.Core.Infrastructure;

@using Nop.Web.Framework;

@using Nop.Web.Framework.UI;

@using Nop.Services.Configuration;

@{

 

ISettingService _settingContext = EngineContext.Current.Resolve<ISettingService>();

IStoreContext _storeContext = EngineContext.Current.Resolve<IStoreContext>();

 

Html.AddScriptParts("~/Plugins/Widget.LiveAnnouncement/Scripts/signalr.js");

Html.AddScriptParts("~/Plugins/Widget.LiveAnnouncement/Scripts/LiveAnnouncement.js");

   Html.AddCssFileParts("~/Plugins/Widget.LiveAnnouncement/Content/toastr.min.css");

Html.AddScriptParts("~/Plugins/Widget.LiveAnnouncement/Scripts/toastr.js");

}

<div class="announcementPage">

</div>

 

All related js and css will be added later of the tutorial.

The WidgetPlugin development is done. We need to add two user-interface from where admin can broadcast their notification. It is a general CRUD operation. We need to write service for plugin. To do that, we will take an interface IAnnouncementService and class AnnouncementService under Services folder. The AnnouncementService will implement the IAnnouncementService interface. We will use this service from controller and inject it at the controller constructor. To do this, we need to register the dependency of the interface. The service has the dependency on the repository so we need to register the repository with domain class with contextname. Let’s create DependencyRegister class under Infrastructure.

 

The DependencyRegister class

using Autofac;

using Autofac.Core;

using Nop.Core.Configuration;

using Nop.Core.Data;

using Nop.Core.Infrastructure;

using Nop.Core.Infrastructure.DependencyManagement;

using Nop.Data;

using Nop.Plugin.Widget.LiveAnnouncement.Data;

using Nop.Plugin.Widget.LiveAnnouncement.Domain;

using Nop.Plugin.Widget.LiveAnnouncement.Services;

using Nop.Web.Framework.Infrastructure.Extensions;

 

namespace Nop.Plugin.Widget.LiveAnnouncement.Infrastructure

{

public partial class DependencyRegister : IDependencyRegistrar

{

   #region Field

   private const string ContextName = "nop_object_context_live_announcement";

   #endregion

 

   #region Register

 

   public void Register(ContainerBuilder builder, ITypeFinder typeFinder, NopConfig config)

   {

           builder.RegisterType<AnnouncementService>().As<IAnnouncementService>().InstancePerLifetimeScope();

           builder.RegisterPluginDataContext<LiveAnnouncementObjectContext>(ContextName);

           builder.RegisterType<EfRepository<Announcement>>().As<IRepository<Announcement>>().WithParameter(ResolvedParameter.ForNamed<IDbContext>(ContextName)).InstancePerLifetimeScope();

 

   }

   #endregion

   #region DB

 

   public int Order

   {

       get { return 0; }

   }

   #endregion

}

}

The service interface and class are bellow.

using System;

using Nop.Core;

using Nop.Plugin.Widget.LiveAnnouncement.Domain;

using System.Collections.Generic;

namespace Nop.Plugin.Widget.LiveAnnouncement.Services

{

public interface IAnnouncementService

{

   void Delete(Announcement AnnouncementDomain);

   void Insert(Announcement item);

   bool Update(Announcement AnnouncementDomain);

   IPagedList<Announcement> GetAnnouncementDomain(int pageIndex = 0, int pageSize = int.MaxValue);

   Announcement GetAnnouncementDesignFirst();

   Announcement GetAnnouncementById(int Id);

}

}

And concrete class

using System;

using System.Linq;

using Nop.Core;

using Nop.Core.Data;

using Nop.Core.Domain.Catalog;

using Nop.Plugin.Widget.LiveAnnouncement.Domain;

using System.Collections.Generic;

using Nop.Services.Events;

namespace Nop.Plugin.Widget.LiveAnnouncement.Services

{

public class AnnouncementService : IAnnouncementService

{

   #region Field

   private readonly IRepository<Announcement> _announcementRepository;

   #endregion

   #region Ctr

   public AnnouncementService(IRepository<Announcement> announcementRepository)

   {

       _announcementRepository = announcementRepository;

   }

   #endregion

   #region Methods

   public void Delete(Announcement AnnouncementDomain)

   {

       _announcementRepository.Delete(AnnouncementDomain);

   }

 

   public bool Update(Announcement AnnouncementDomain)

   {

       if (AnnouncementDomain == null)

           throw new ArgumentNullException("customer");

 

           _announcementRepository.Update(AnnouncementDomain);

       return true;

   }

 

   public void Insert(Announcement item)

   {

           _announcementRepository.Insert(item);

   }

 

   public IPagedList<Announcement> GetAnnouncementDomain(int pageIndex = 0, int pageSize = int.MaxValue)

   {

       var query = from c in _announcementRepository.Table

                   select c;

       query = query.OrderBy(b => b.IsActive);

       var liveAnnouncementDomain = new PagedList<Announcement>(query, pageIndex, pageSize);

       return liveAnnouncementDomain;

   }

 

   public Announcement GetAnnouncementDesignFirst()

   {

       var query = from c in _announcementRepository.Table

                   where c.IsActive==true

                   orderby c.CreateDate descending

                   select c;

       var LatestAnnouncement = query.ToList().FirstOrDefault();

       return LatestAnnouncement;

   }

   public Announcement GetAnnouncementById(int Id)

   {

       return _announcementRepository.GetById(Id);

   }

   #endregion

}

}

Now we need to add a controller for CRUD operation of the announcement under the Controllers folder and related views at Viewsè LiveAnnouncementView. But before that, we need to create a model and integrate the signalR core at our plugin because a notification will show whenever admin creates an announcement and the announcement will show at the real time. To do that I need to create AnnouncementHub which will inherit Hub abstract class of Microsoft.AspNetCore.SignalR. We will take AnnouncementHub class at the root of the plugin project. But before that, We need to take ViewModel class under Models folder.

The AnnouncementModel viewmodel class.

using Nop.Web.Framework.Models;

using Nop.Web.Framework.Mvc.ModelBinding;

namespace Nop.Plugin.Widget.LiveAnnouncement.Models

{

public partial class AnnouncementModel : BaseNopEntityModel

{

   [NopResourceDisplayName("Name")]

   public string Name { get; set; }

   [NopResourceDisplayName("Body")]

   public string Body { get; set; }

   public bool IsActive { get; set; }

}

}

 

The AnnouncementHub

using Microsoft.AspNetCore.SignalR;

using System.Threading.Tasks;

namespace Nop.Plugin.LiveAnnouncement

{

public class AnnouncementHub : Hub

{

   public Task Send(string announcement)

   {

       return Clients.All.SendAsync("Send", announcement);

   }

}

}

At the startup of the application, we need to add SignalR request execution pipeline and configuration of signalR. We also take it at the root of the plugin.

 

The AnnouncementHubAtStartUp class

using Microsoft.AspNetCore.Builder;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.DependencyInjection;

using Nop.Core.Infrastructure;

using Nop.Plugin.LiveAnnouncement;

using System;

 

namespace Nop.Plugin.Widget.LiveAnnouncement

{

public class AnnouncementHubAtStartUp : INopStartup

{

   public int Order => 999;

 

   public void ConfigureServices(IServiceCollection services, IConfiguration configuration)

   {

       services.AddSignalR(hubOptions =>

       {

           hubOptions.KeepAliveInterval = TimeSpan.FromMinutes(1);

       });

   }

 

   public void Configure(IApplicationBuilder application)

   {

       application.UseSignalR(routes =>

       {

           routes.MapHub<AnnouncementHub>("/announcement");

       });

   }

}

}

 

It is important to keep the Order less than 1000. Because it need to execute before NopMvcStartup.

The Controller

using Microsoft.AspNetCore.Mvc;

using Microsoft.AspNetCore.SignalR;

using Nop.Plugin.LiveAnnouncement;

using Nop.Plugin.Widget.LiveAnnouncement.Domain;

using Nop.Plugin.Widget.LiveAnnouncement.Models;

using Nop.Plugin.Widget.LiveAnnouncement.Services;

using Nop.Web.Areas.Admin.Controllers;

using Nop.Web.Framework.Kendoui;

using Nop.Web.Framework.Mvc;

using System;

using System.Linq;

namespace Nop.Plugin.Widget.LiveAnnouncement.Controllers

{

 

public class LiveAnnouncementController : BaseAdminController

{

   #region Field

 

   private readonly IAnnouncementService _announcementService;

   private IHubContext<AnnouncementHub> _announcementHubContext;

 

   #endregion

 

   #region Ctr

 

   public LiveAnnouncementController(

       IAnnouncementService announcementService,

           IHubContext<AnnouncementHub> announcementHubContext)

   {

       _announcementService = announcementService;

       _announcementHubContext = announcementHubContext;

   }

 

   #endregion

   #region Methods    

   public IActionResult Announcement()

   {

       var model = new AnnouncementModel();

       return View("~/Plugins/Widget.LiveAnnouncement/Views/LiveAnnouncementView/Announcement.cshtml", model);

   }

 

   [HttpPost]

   public IActionResult Announcement(AnnouncementModel model)

   {

       Announcement objOfAnnouncementDomain = new Announcement();

           objOfAnnouncementDomain.Name=model.Name;

           objOfAnnouncementDomain.Body=model.Body;

       objOfAnnouncementDomain.IsActive=model.IsActive;

       objOfAnnouncementDomain.CreateDate = DateTime.UtcNow;

              _announcementService.Insert(objOfAnnouncementDomain);

 

       if (model.IsActive == true)

       {

               _announcementHubContext.Clients.All.SendAsync("send", model.Body.ToString());

       }

       return RedirectToAction("AnnouncementList");

   }

   [HttpPost]

   public IActionResult Edit(AnnouncementModel model)

   {

       var entity = _announcementService.GetAnnouncementById(model.Id);

 

       entity.Name = model.Name;

       entity.Body = model.Body;

       entity.IsActive = model.IsActive;

       entity.CreateDate = DateTime.UtcNow;

           _announcementService.Update(entity);

 

       if (model.IsActive == true)

       {

               _announcementHubContext.Clients.All.SendAsync("send", model.Body.ToString());

       }

       return RedirectToAction("AnnouncementList");

   }

   public IActionResult Edit(int Id)

   {

       var singleAnnouncement = _announcementService.GetAnnouncementById(Id);

       var model = new AnnouncementModel();

       model.Id = singleAnnouncement.Id;

       model.Name = singleAnnouncement.Name;

       model.Body = singleAnnouncement.Body;

       model.IsActive = singleAnnouncement.IsActive;

 

       return View("~/Plugins/Widget.LiveAnnouncement/Views/LiveAnnouncementView/Announcement.cshtml", model);

 

   }

   public IActionResult Delete(int Id)

   {

       var singleAnnouncement = _announcementService.GetAnnouncementById(Id);

           _announcementService.Delete(singleAnnouncement);

       return new NullJsonResult();

   }

 

   public IActionResult AnnouncementList()

   {

       var model = new AnnouncementModel();

       return View("~/Plugins/Widget.LiveAnnouncement/Views/LiveAnnouncementView/AnnouncementList.cshtml", model);

   }

 

   [HttpPost]

   public IActionResult AnnouncementList(DataSourceRequest command, AnnouncementModel model)

   {

       var announcementPagedList = _announcementService.GetAnnouncementDomain(pageIndex: command.Page - 1, pageSize: command.PageSize);

       var gridModel = new DataSourceResult();

       gridModel.Data = announcementPagedList.Select(x =>

       {

               return new AnnouncementModel()

           {

               Id = x.Id,

               Name = x.Name,

               Body = x.Body,

               IsActive = x.IsActive

           };

       });

       gridModel.Total = announcementPagedList.TotalCount;

       return Json(gridModel);

   }

   #endregion

}

}

 

The create view of announcement(Announcement.cshtml)

 

@model Nop.Plugin.Widget.LiveAnnouncement.Models.AnnouncementModel

@using Nop.Core.Infrastructure

@using Nop.Web.Framework

@{

var defaultGridPageSize = EngineContext.Current.Resolve<Nop.Core.Domain.Common.AdminAreaSettings>().DefaultGridPageSize;

var gridPageSizes = EngineContext.Current.Resolve<Nop.Core.Domain.Common.AdminAreaSettings>().GridPageSizes;

 

Layout = "_AdminLayout";

//page title

ViewBag.Title = T("Admin.Plugins.HomePageProduct").Text;

}

 

@using (Html.BeginForm())

{

<div class="content-header clearfix">

   <h1 class="pull-left">

       Create Announcement

   </h1>

   <div class="pull-right">

       <button type="submit" class="btn bg-purple">

           <i class="fa fa-file-pdf-o"></i>

           Create Announcement

       </button>

       <a href="/Admin/LiveAnnouncement/AnnouncementList" class="btn bg-olive">LiveAnnouncement</a>

   </div>

</div>

 

<div class="content">

   <div class="form-horizontal">

       <div class="panel-group">

           <div class="panel panel-default panel-search">

               <div class="panel-body">

                   <div class="row">

                       <div class="form-group">

                           <div class="col-md-3" style="text-align:center;">

                               @Html.LabelFor(model => model.Name)

                           </div>

                           <div class="col-md-8">

                               @Html.EditorFor(model => model.Name)

                           </div>

                           <div class="col-md-1">

                               &nbsp;

                               </div>

                           </div>

                       <div class="form-group">

                           <div class="col-md-3" style="text-align:center;">

                             @Html.LabelFor(model => model.Body)

                           </div>

                           <div class="col-md-8">

                               <nop-editor asp-for="@Model.Body" asp-template="RichEditor" />

                           </div>

                           <div class="col-md-1">

                               &nbsp;

                           </div>

                       </div>

                       <div class="form-group">

                           <div class="col-md-3" style="text-align:center;">

                           @Html.LabelFor(model => model.IsActive)

                           </div>

                           <div class="col-md-8">

                        @Html.EditorFor(model => model.IsActive)

                           </div>

                           <div class="col-md-1">

                               &nbsp;

                           </div>

                       </div>

                   </div>

               </div>

           </div>

</div>

       </div>

   </div>

           }

 

The List of created view(AnnouncementList.cshtml)

@model Nop.Plugin.Widget.LiveAnnouncement.Models.AnnouncementModel

@using Nop.Core.Infrastructure

@using Nop.Web.Framework

@{

var defaultGridPageSize = EngineContext.Current.Resolve<Nop.Core.Domain.Common.AdminAreaSettings>().DefaultGridPageSize;

var gridPageSizes = EngineContext.Current.Resolve<Nop.Core.Domain.Common.AdminAreaSettings>().GridPageSizes;

 

Layout = "_AdminLayout";

//page title

ViewBag.Title = T("Admin.Plugins.HomePageProduct").Text;

  }

<div class="content-header clearfix">

<div class="pull-right">

   <a href="../LiveAnnouncement/Announcement" class="btn bg-blue">

       <i class="fa fa-floppy-o"></i>

       Add

   </a>

</div>

</div>

<div class="content">

<div class="form-horizontal">

   <div class="panel-group">

       <div class="panel panel-default">

           <div class="panel-body">

               <div id="Announcement-grid"></div>

 

               <script>

$(document).ready(function () {

   $("#Announcement-grid").kendoGrid({

       dataSource: {

           type: "json",

           transport: {

               read: {

                   url:  "@Html.Raw(Url.Action("AnnouncementList", "LiveAnnouncement"))",

                   type: "POST",

                   dataType: "json",

                   data: addAntiForgeryToken

               },

                               destroy: {

                                   url: "@Html.Raw(Url.Action("Delete", "LiveAnnouncement"))",

                                   type: "POST",

                                       dataType: "json",

                                   data: addAntiForgeryToken

                               }

           },

           schema: {

               data: "Data",

               total: "Total",

               errors: "Errors",

               model: {

                   id: "Id"

               }

           },

           error: function(e) {

                   display_kendoui_grid_error(e);

               // Cancel the changes

               this.cancelChanges();

           },

           pageSize: @(defaultGridPageSize),

           serverPaging: true,

           serverFiltering: true,

           serverSorting: true

       },

       pageable: {

           refresh: true,

           pageSizes: [@(gridPageSizes)]

                   },

                   editable: {

                       confirmation: false,

                       mode: "inline"

                   },

                   scrollable: false,

                   columns: [{

                       field: "Name",

                       title: "Name",

                       width: 100

                   }, {

                       field: "Body",

                       title: "Body",

                       width: 100,

                       headerAttributes: { style: "text-align:center" },

                       attributes: { style: "text-align:center" }

                   },

                   {

                       field: "IsActive",

                      title: "IsActive",

                       width: 100,

                       headerAttributes: { style: "text-align:center" },

                       attributes: { style: "text-align:center" }

                   },

                   {

                       title: "Edite",

                       width: 100,

                       template: '<a href="Edit/#=Id#">@T("Admin.Common.Edit")</a>'

                   },{

                           command: { name: "destroy", text: "@T("Admin.Common.Delete")" },

                           title: "@T("Admin.Common.Delete")",

                           width: 100,

                           headerAttributes: { style: "text-align:center" },

                           attributes: { style: "text-align:center" }

                       }]

               });

           });

               </script>

               </div>

               </div>

               </div>

 

           </div>

       </div>

 

Add _ViewImports.cshtml to the ViewsèLiveAnnouncementView folder. I take from other plugin. It is important to copy local true for all 3 views and also _ViewImports.cshtml.

 

The content of the LiveAnnouncement.js

 

$(function () {

   var connection = new signalR.HubConnectionBuilder()

       .withUrl('/announcement')

       .build();

 

   connection.on('send', data => {

       showannouncement(data);

   });

 

   function start() {

       connection.start().catch(function (err) {

           setTimeout(function () {

               start();

           }, 100000);

       });

   }

 

   connection.onclose(function () {

       start();

   });

 

   start();

});

 

function showannouncement(announcemant) {

   if (announcemant) {

       toastr.options = {

           "closeButton": true,

           "debug": false,

           "newestOnTop": false,

           "progressBar": false,

           "positionClass": "toast-bottom-right",

           "preventDuplicates": false,

           "onclick": null,

           "showDuration": 300,

           "hideDuration": 10000,

           "timeOut": 100000,

           "extendedTimeOut": 20000,

           "showEasing": "swing",

           "hideEasing": "linear",

           "showMethod": "fadeIn",

           "hideMethod": "fadeOut"

       };

 

       tostView = '<div class="item announcemantToast">' + announcemant + '</div>'

       toastr["info"](tostView);

       $('.toast-info').css("background-color", "#008080");

 

       toastr.options.onclick = function () {

           $("html, body").animate(

               { scrollTop: 0 },

               1000);

       }

 

       $(".toast").click(function () {

           $("html, body").animate(

               { scrollTop: 0 },

               1000);

       });

 

       $(".toast-info").click(function () {

           $("html, body").animate(

               { scrollTop: 0 },

               1000);

       });

 

       toastr.options = {

           onclick: function () {

               $("html, body").animate(

                   { scrollTop: 0 },

                   1000);

           }

       }

 

       $(".announcemantToast").on("click", function () {

           $("html, body").animate(

               { scrollTop: 0 },

               1000);

       });

   }

}

Please download signalr.js from NPM and toastr from https://github.com/CodeSeven/toastr . Add these 3 scripts under Scripts folder and do not forget to change “Do not copy” to “Copy always” of key “Copy to Output Directory” and also add a logo also change “Do not copy” to “Copy always”.

For enable Lazyloding we need to add a class PluginDbStartup at the root of the plugin.

The PluginDbStartup class

using Microsoft.AspNetCore.Builder;

using Microsoft.Extensions.Configuration;

using Microsoft.Extensions.DependencyInjection;

using Nop.Core.Infrastructure;

using Nop.Plugin.Widget.LiveAnnouncement.Data;

using Nop.Web.Framework.Infrastructure.Extensions;

 

namespace Nop.Plugin.Widget.LiveAnnouncement

{

/// <summary>

/// Represents object for the configuring plugin DB context on application startup

/// </summary>

public class PluginDbStartup : INopStartup

{

   /// <summary>

   /// Add and configure any of the middleware

   /// </summary>

   /// <param name="services">Collection of service descriptors</param>

   /// <param name="configuration">Configuration of the application</param>

   public void ConfigureServices(IServiceCollection services, IConfiguration configuration)

   {

       //add object context

           services.AddDbContext<LiveAnnouncementObjectContext>(optionsBuilder =>

       {

               optionsBuilder.UseSqlServerWithLazyLoading(services);

       });

   }

 

   /// <summary>

   /// Configure the using of added middleware

   /// </summary>

   /// <param name="application">Builder for configuring an application's request pipeline</param>

   public void Configure(IApplicationBuilder application)

   {

   }

   /// <summary>

   /// Gets order of this startup configuration implementation

   /// </summary>

   public int Order => 12;

}

}

For this project, we do not use automapper to convert viewmodel to domainmodel or vice versa. Because domain/entity model don’t have large property set.

 

Leave your comment
Only registered users can leave comments.
card icon