一.插件简介

插件用于扩展nopCommerce的功能。nopCommerce有几种类型的插件如:支付、税率、配送方式、小部件等(接口如下图),更多插件可以访问nopCommerce官网

我们看下后台如何配置管理插件的。

【后台管理】【商城配置】【挂件管理】用于配置小部件,【插件管理】【本地插件】管理本地所有插件

nop自带小部件:Nop.Plugin.Widgets.NivoSlider插件用于主页显示幻灯片,后续我们以此插件为例介绍nop是如何加载小部件的

本文主要介绍的是用于在网站中显示的小部件widgets(实现IWidgetPlugin接口)。

只介绍网站中是如何调用的,后续文章中再介绍如何创建IWidgetPlugin插件。

二.小部件介绍及使用

小部件也可以叫小挂件,继承IWidgetPlugin接口。

是用于在网站中显示的小插件。

自带有:Nop.Plugin.Widgets.NivoSlider插件显示幻灯片。如下图红色区域

我们先看下NivoSlider插件文档结构,每一个插件都有一个Description.txt用于插件的描述。

NivoSlider插件Description.txt内容如下图。

SystemName:系统名称唯一。

SupportedVersions: 该插件支持的nop版本号,nop版本号不对可是在插件列表里不显示的。

FileName:程序集dll文件名,一定要和插件生成的dll文件名一样,否则报错

对比后台理解更直观些

安装、卸载插件在后台管理中心进行管理,这里就不多说了。

安装成功的插件会将系统名称保存在项目文件"~/App_Data/InstalledPlugins.txt"中。

三.小部件调用原理分析

我们看网站是如显示小部件的,还是以NivoSlider插件为例.NivoSlider是在首页显示的,打开首页试图“Home\Index.chtml”

我们发现有很多像@Html.Widget("home_page_top")的节点。

@Html.Widget会调用WidgetController控制器下WidgetsByZone方法从而获取显示内容并输出到页面中

  public static MvcHtmlString Widget(this HtmlHelper helper, string widgetZone, object additionalData = null, string area = null)
{
return helper.Action("WidgetsByZone", "Widget", new { widgetZone = widgetZone, additionalData = additionalData, area = area });
}

再来看下小部件中NivoSliderPlugin类,它继承了IWidgetPlugin接口

并实现了IWidgetPlugin接口GetWidgetZones()方法返回显示位置名称集合。

我们发现NivoSlider插件包含了“home_page_top”的位置。

Index.chtml试图中也出现了@Html.Widget("home_page_top"),因此该小部件会在首页中显示。

所以试图中想要使用小部件,使用@Html.Widget("位置名称")就可以了。

ok,知道怎么使用了,我们再看看源码中涉及到哪些相关接口,之间调用关系是怎样的,先上图。

首先WidgetController控制器WidgetsByZone会返回部分视图

  [ChildActionOnly]
public virtual ActionResult WidgetsByZone(string widgetZone, object additionalData = null)
{
//查找到符合要求的List<RenderWidgetModel>
var model = _widgetModelFactory.GetRenderWidgetModels(widgetZone, additionalData); //no data?
if (!model.Any())
return Content(""); return PartialView(model);
}

WidgetsByZone.cshtml中代码如下,我们发现这里重新调用了插件中某个action

 @model List<RenderWidgetModel>
@using Nop.Web.Models.Cms;
@foreach (var widget in Model)
{
@Html.Action(widget.ActionName, widget.ControllerName, widget.RouteValues)
}

那上边的Action信息又是哪里得到的呢?IWidgetPlugin接口GetDisplayWidgetRoute就是用来返回显示时调用的处理Action信息。

NivoSliderPlugin类实现了GetDisplayWidgetRoute代码如下。

 /// <summary>
/// 获取显示插件的路由
/// </summary>
/// <param name="widgetZone">Widget zone where it's displayed</param>
/// <param name="actionName">Action name</param>
/// <param name="controllerName">Controller name</param>
/// <param name="routeValues">Route values</param>
public void GetDisplayWidgetRoute(string widgetZone, out string actionName, out string controllerName, out RouteValueDictionary routeValues)
{
actionName = "PublicInfo";
controllerName = "WidgetsNivoSlider";
routeValues = new RouteValueDictionary
{
{"Namespaces", "Nop.Plugin.Widgets.NivoSlider.Controllers"},
{"area", null},
{"widgetZone", widgetZone}
};
}

总结下:WidgetController->WidgetsByZone负责调用显示插件。

而我们开发的小部件需要实现IWidgetPlugin接口GetDisplayWidgetRoute方法告诉上层,我的显示入口是哪个controller  下的action。

下面我们分析下nop是如何找到我们开发的小部件呢?继续看图。

IWidgetModelFactory

GetRenderWidgetModels(widgetZone, additionalData)方法,

传入小部件位置widgetZone(本例中为"home_page_top")获取List<RenderWidgetModel>

IWidgetService

LoadActiveWidgetsByWidgetZone(widgetZone, _workContext.CurrentCustomer, _storeContext.CurrentStore.Id)

负责返回符合要求的IList<IWidgetPlugin>集合,过滤条件为部件位置,用户,商城。

  /// <summary>
/// Load active widgets
/// </summary>
/// <param name="widgetZone">Widget zone</param>
/// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param>
/// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
/// <returns>Widgets</returns>
public virtual IList<IWidgetPlugin> LoadActiveWidgetsByWidgetZone(string widgetZone, Customer customer = null, int storeId = 0)
{
if (String.IsNullOrWhiteSpace(widgetZone))
return new List<IWidgetPlugin>(); return LoadActiveWidgets(customer, storeId)
.Where(x => x.GetWidgetZones().Contains(widgetZone, StringComparer.InvariantCultureIgnoreCase)).ToList();
} /// <summary>
/// Load active widgets
/// </summary>
/// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param>
/// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
/// <returns>Widgets</returns>
public virtual IList<IWidgetPlugin> LoadActiveWidgets(Customer customer = null, int storeId = 0)
{
return LoadAllWidgets(customer, storeId)
.Where(x => _widgetSettings.ActiveWidgetSystemNames.Contains(x.PluginDescriptor.SystemName, StringComparer.InvariantCultureIgnoreCase)).ToList();
} /// <summary>
/// Load all widgets
/// </summary>
/// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param>
/// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param>
/// <returns>Widgets</returns>
public virtual IList<IWidgetPlugin> LoadAllWidgets(Customer customer = null, int storeId = 0)
{
return _pluginFinder.GetPlugins<IWidgetPlugin>(customer: customer, storeId: storeId).ToList();
}

按条件获取到可用小部件

IPluginFinder

_pluginFinder.GetPlugins<IWidgetPlugin>(customer: customer, storeId: storeId).ToList();

查询继承IWidgetPlugin接口的插件也就是小部件了,这里只返回IWidgetPlugin实现类。

PluginManager

 using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
using System.Web.Compilation;
using Nop.Core.ComponentModel;
using Nop.Core.Plugins; //Contributor: Umbraco (http://www.umbraco.com). Thanks a lot!
//SEE THIS POST for full details of what this does - http://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]
namespace Nop.Core.Plugins
{
/// <summary>
/// Sets the application up for the plugin referencing
/// </summary>
public class PluginManager
{
#region Const private const string InstalledPluginsFilePath = "~/App_Data/InstalledPlugins.txt";
private const string PluginsPath = "~/Plugins";
private const string ShadowCopyPath = "~/Plugins/bin"; #endregion #region Fields private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim();
private static DirectoryInfo _shadowCopyFolder;
private static bool _clearShadowDirectoryOnStartup; #endregion #region Methods /// <summary>
/// Returns a collection of all referenced plugin assemblies that have been shadow copied
/// </summary>
public static IEnumerable<PluginDescriptor> ReferencedPlugins { get; set; } /// <summary>
/// Returns a collection of all plugin which are not compatible with the current version
/// </summary>
public static IEnumerable<string> IncompatiblePlugins { get; set; } /// <summary>
/// Initialize
/// </summary>
public static void Initialize()
{
using (new WriteLockDisposable(Locker))
{
// TODO: Add verbose exception handling / raising here since this is happening on app startup and could
// prevent app from starting altogether
var pluginFolder = new DirectoryInfo(CommonHelper.MapPath(PluginsPath));
_shadowCopyFolder = new DirectoryInfo(CommonHelper.MapPath(ShadowCopyPath)); var referencedPlugins = new List<PluginDescriptor>();
var incompatiblePlugins = new List<string>(); _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) &&
Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]); try
{
var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); Debug.WriteLine("Creating shadow copy folder and querying for dlls");
//ensure folders are created
Directory.CreateDirectory(pluginFolder.FullName);
Directory.CreateDirectory(_shadowCopyFolder.FullName); //get list of all files in bin
var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories);
if (_clearShadowDirectoryOnStartup)
{
//clear out shadow copied plugins
foreach (var f in binFiles)
{
Debug.WriteLine("Deleting " + f.Name);
try
{
File.Delete(f.FullName);
}
catch (Exception exc)
{
Debug.WriteLine("Error deleting file " + f.Name + ". Exception: " + exc);
}
}
} //load description files
foreach (var dfd in GetDescriptionFilesAndDescriptors(pluginFolder))
{
var descriptionFile = dfd.Key;
var pluginDescriptor = dfd.Value; //ensure that version of plugin is valid
if (!pluginDescriptor.SupportedVersions.Contains(NopVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase))
{
incompatiblePlugins.Add(pluginDescriptor.SystemName);
continue;
} //some validation
if (String.IsNullOrWhiteSpace(pluginDescriptor.SystemName))
throw new Exception(string.Format("A plugin '{0}' has no system name. Try assigning the plugin a unique name and recompiling.", descriptionFile.FullName));
if (referencedPlugins.Contains(pluginDescriptor))
throw new Exception(string.Format("A plugin with '{0}' system name is already defined", pluginDescriptor.SystemName)); //set 'Installed' property
pluginDescriptor.Installed = installedPluginSystemNames
.FirstOrDefault(x => x.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)) != null; try
{
if (descriptionFile.Directory == null)
throw new Exception(string.Format("Directory cannot be resolved for '{0}' description file", descriptionFile.Name));
//get list of all DLLs in plugins (not in bin!)
var pluginFiles = descriptionFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories)
//just make sure we're not registering shadow copied plugins
.Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName))
.Where(x => IsPackagePluginFolder(x.Directory))
.ToList(); //other plugin description info
var mainPluginFile = pluginFiles
.FirstOrDefault(x => x.Name.Equals(pluginDescriptor.PluginFileName, StringComparison.InvariantCultureIgnoreCase));
pluginDescriptor.OriginalAssemblyFile = mainPluginFile; //shadow copy main plugin file
pluginDescriptor.ReferencedAssembly = PerformFileDeploy(mainPluginFile); //load all other referenced assemblies now
foreach (var plugin in pluginFiles
.Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase))
.Where(x => !IsAlreadyLoaded(x)))
PerformFileDeploy(plugin); //init plugin type (only one plugin per assembly is allowed)
foreach (var t in pluginDescriptor.ReferencedAssembly.GetTypes())
if (typeof(IPlugin).IsAssignableFrom(t))
if (!t.IsInterface)
if (t.IsClass && !t.IsAbstract)
{
pluginDescriptor.PluginType = t;
break;
} referencedPlugins.Add(pluginDescriptor);
}
catch (ReflectionTypeLoadException ex)
{
//add a plugin name. this way we can easily identify a problematic plugin
var msg = string.Format("Plugin '{0}'. ", pluginDescriptor.FriendlyName);
foreach (var e in ex.LoaderExceptions)
msg += e.Message + Environment.NewLine; var fail = new Exception(msg, ex);
throw fail;
}
catch (Exception ex)
{
//add a plugin name. this way we can easily identify a problematic plugin
var msg = string.Format("Plugin '{0}'. {1}", pluginDescriptor.FriendlyName, ex.Message); var fail = new Exception(msg, ex);
throw fail;
}
}
}
catch (Exception ex)
{
var msg = string.Empty;
for (var e = ex; e != null; e = e.InnerException)
msg += e.Message + Environment.NewLine; var fail = new Exception(msg, ex);
throw fail;
} ReferencedPlugins = referencedPlugins;
IncompatiblePlugins = incompatiblePlugins; }
} /// <summary>
/// Mark plugin as installed
/// </summary>
/// <param name="systemName">Plugin system name</param>
public static void MarkPluginAsInstalled(string systemName)
{
if (String.IsNullOrEmpty(systemName))
throw new ArgumentNullException("systemName"); var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
if (!File.Exists(filePath))
using (File.Create(filePath))
{
//we use 'using' to close the file after it's created
} var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
bool alreadyMarkedAsInstalled = installedPluginSystemNames
.FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;
if (!alreadyMarkedAsInstalled)
installedPluginSystemNames.Add(systemName);
PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath);
} /// <summary>
/// Mark plugin as uninstalled
/// </summary>
/// <param name="systemName">Plugin system name</param>
public static void MarkPluginAsUninstalled(string systemName)
{
if (String.IsNullOrEmpty(systemName))
throw new ArgumentNullException("systemName"); var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
if (!File.Exists(filePath))
using (File.Create(filePath))
{
//we use 'using' to close the file after it's created
} var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
bool alreadyMarkedAsInstalled = installedPluginSystemNames
.FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null;
if (alreadyMarkedAsInstalled)
installedPluginSystemNames.Remove(systemName);
PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath);
} /// <summary>
/// Mark plugin as uninstalled
/// </summary>
public static void MarkAllPluginsAsUninstalled()
{
var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
if (File.Exists(filePath))
File.Delete(filePath);
} #endregion #region Utilities /// <summary>
/// Get description files
/// </summary>
/// <param name="pluginFolder">Plugin directory info</param>
/// <returns>Original and parsed description files</returns>
private static IEnumerable<KeyValuePair<FileInfo, PluginDescriptor>> GetDescriptionFilesAndDescriptors(DirectoryInfo pluginFolder)
{
if (pluginFolder == null)
throw new ArgumentNullException("pluginFolder"); //create list (<file info, parsed plugin descritor>)
var result = new List<KeyValuePair<FileInfo, PluginDescriptor>>();
//add display order and path to list
foreach (var descriptionFile in pluginFolder.GetFiles("Description.txt", SearchOption.AllDirectories))
{
if (!IsPackagePluginFolder(descriptionFile.Directory))
continue; //parse file
var pluginDescriptor = PluginFileParser.ParsePluginDescriptionFile(descriptionFile.FullName); //populate list
result.Add(new KeyValuePair<FileInfo, PluginDescriptor>(descriptionFile, pluginDescriptor));
} //sort list by display order. NOTE: Lowest DisplayOrder will be first i.e 0 , 1, 1, 1, 5, 10
//it's required: http://www.nopcommerce.com/boards/t/17455/load-plugins-based-on-their-displayorder-on-startup.aspx
result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
return result;
} /// <summary>
/// Indicates whether assembly file is already loaded
/// </summary>
/// <param name="fileInfo">File info</param>
/// <returns>Result</returns>
private static bool IsAlreadyLoaded(FileInfo fileInfo)
{
//compare full assembly name
//var fileAssemblyName = AssemblyName.GetAssemblyName(fileInfo.FullName);
//foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
//{
// if (a.FullName.Equals(fileAssemblyName.FullName, StringComparison.InvariantCultureIgnoreCase))
// return true;
//}
//return false; //do not compare the full assembly name, just filename
try
{
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
if (fileNameWithoutExt == null)
throw new Exception(string.Format("Cannot get file extension for {0}", fileInfo.Name));
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
{
string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault();
if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase))
return true;
}
}
catch (Exception exc)
{
Debug.WriteLine("Cannot validate whether an assembly is already loaded. " + exc);
}
return false;
} /// <summary>
/// Perform file deply
/// </summary>
/// <param name="plug">Plugin file info</param>
/// <returns>Assembly</returns>
private static Assembly PerformFileDeploy(FileInfo plug)
{
if (plug.Directory == null || plug.Directory.Parent == null)
throw new InvalidOperationException("The plugin directory for the " + plug.Name + " file exists in a folder outside of the allowed nopCommerce folder hierarchy"); FileInfo shadowCopiedPlug; if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
{
//all plugins will need to be copied to ~/Plugins/bin/
//this is absolutely required because all of this relies on probingPaths being set statically in the web.config //were running in med trust, so copy to custom bin folder
var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName);
shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder);
}
else
{
var directory = AppDomain.CurrentDomain.DynamicDirectory;
Debug.WriteLine(plug.FullName + " to " + directory);
//were running in full trust so copy to standard dynamic folder
shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory));
} //we can now register the plugin definition
var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName)); //add the reference to the build manager
Debug.WriteLine("Adding to BuildManager: '{0}'", shadowCopiedAssembly.FullName);
BuildManager.AddReferencedAssembly(shadowCopiedAssembly); return shadowCopiedAssembly;
} /// <summary>
/// Used to initialize plugins when running in Full Trust
/// </summary>
/// <param name="plug"></param>
/// <param name="shadowCopyPlugFolder"></param>
/// <returns></returns>
private static FileInfo InitializeFullTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
{
var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name));
try
{
File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
}
catch (IOException)
{
Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename");
//this occurs when the files are locked,
//for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
//which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
try
{
var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
File.Move(shadowCopiedPlug.FullName, oldFile);
}
catch (IOException exc)
{
throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
}
//ok, we've made it this far, now retry the shadow copy
File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
}
return shadowCopiedPlug;
} /// <summary>
/// Used to initialize plugins when running in Medium Trust
/// </summary>
/// <param name="plug"></param>
/// <param name="shadowCopyPlugFolder"></param>
/// <returns></returns>
private static FileInfo InitializeMediumTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder)
{
var shouldCopy = true;
var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name)); //check if a shadow copied file already exists and if it does, check if it's updated, if not don't copy
if (shadowCopiedPlug.Exists)
{
//it's better to use LastWriteTimeUTC, but not all file systems have this property
//maybe it is better to compare file hash?
var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plug.CreationTimeUtc.Ticks;
if (areFilesIdentical)
{
Debug.WriteLine("Not copying; files appear identical: '{0}'", shadowCopiedPlug.Name);
shouldCopy = false;
}
else
{
//delete an existing file //More info: http://www.nopcommerce.com/boards/t/11511/access-error-nopplugindiscountrulesbillingcountrydll.aspx?p=4#60838
Debug.WriteLine("New plugin found; Deleting the old file: '{0}'", shadowCopiedPlug.Name);
File.Delete(shadowCopiedPlug.FullName);
}
} if (shouldCopy)
{
try
{
File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
}
catch (IOException)
{
Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename");
//this occurs when the files are locked,
//for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them
//which releases the lock, so that it what we are doing here, once it's renamed, we can re-shadow copy
try
{
var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
File.Move(shadowCopiedPlug.FullName, oldFile);
}
catch (IOException exc)
{
throw new IOException(shadowCopiedPlug.FullName + " rename failed, cannot initialize plugin", exc);
}
//ok, we've made it this far, now retry the shadow copy
File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
}
} return shadowCopiedPlug;
} /// <summary>
/// Determines if the folder is a bin plugin folder for a package
/// </summary>
/// <param name="folder"></param>
/// <returns></returns>
private static bool IsPackagePluginFolder(DirectoryInfo folder)
{
if (folder == null) return false;
if (folder.Parent == null) return false;
if (!folder.Parent.Name.Equals("Plugins", StringComparison.InvariantCultureIgnoreCase)) return false;
return true;
} /// <summary>
/// Gets the full path of InstalledPlugins.txt file
/// </summary>
/// <returns></returns>
private static string GetInstalledPluginsFilePath()
{
return CommonHelper.MapPath(InstalledPluginsFilePath);
} #endregion
}
}

PluginManager

PluginManager.ReferencedPlugins获取到所有的插件。

PluginManager类用于管理插件,我们发现[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]表示应用程序启动就调用PluginManager类下边的Initialize方法进行初始化。初始化过程中会把所有插件保存到ReferencedPlugins变量中。

经过上边一层层的过滤查找,终于找到了符合要求的插件了并保存在IList<IWidgetPlugin>集合中。最后经过IWidgetModelFactory接口GetRenderWidgetModels(widgetZone, additionalData)方法处理保存为List<RenderWidgetModel>最后为WidgetsByZone试图使用。

ps: 调用关系描述的不是很清晰,请见谅,大家还是看图,结合代码理解吧。

四.总结

1.nop支持各种插件,不同插件继承接口不一样

2.小部件继承IWidgetPlugin,用于在网页中显示。

3.IWidgetPlugin接口 IList<string> GetWidgetZones()返回显示部件的位置名称集合。GetDisplayWidgetRoute方法返回显示插件时的路由。

4.网站视图中用@Html.Widget("位置名称")来显示插件。”位置名称”包含在插件GetWidgetZones()返回的集合中。

文中有错误的理解和不正确的观点请指正、留言、一起交流共同进步。

本文地址:http://www.cnblogs.com/yaoshangjin/p/7239183.html

本文为大波浪原创、转载请注明出处。

nopCommerce 3.9 大波浪系列 之 网页加载Widgets插件原理的更多相关文章

  1. nopCommerce 3.9 大波浪系列 之 微信公众平台登录插件

    一.简介 插件源码下载:点击下载 微信公众平台网站授权帮助地址:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp142114084 ...

  2. nopCommerce 3.9 大波浪系列 之 可退款的支付宝插件(上)

    一.简介 nop通过插件机制可以支持更多的支付扩展,我们通过编写支持退款的支付宝插件来更好的理解支付插件的扩展. 先分享下支付宝插件源码点击下载,由于时间原因,本篇只介绍使用该插件,下一篇结合插件进行 ...

  3. nopCommerce 3.9 大波浪系列 之 可退款的支付宝插件(下)

    一.回顾 支付宝插件源码下载地址:点击下载 上篇介绍了使用支付宝插件进行支付,全额退款,部分退款还有插件的多店铺配置,本文介绍下如何实现的. 二.前期准备 插件主要有3个功能: 多店铺插件配置 支付功 ...

  4. Qt加载网页(加载浏览器插件)和制作托盘后台运行(南信大财务报账看号)

    程序模块要添加QNetWork和QWebKit模块: nuistfinancevideo.h文件: #ifndef NUISTFINANCEVIDEO_H #define NUISTFINANCEVI ...

  5. nopCommerce 3.9 大波浪系列 之 引擎 NopEngine

    本章涉及到的内容如下 1.EngineContext初始化IEngine实例 2.Autofac依赖注入初始化 3.AutoMapper框架初始化 4.启动任务初始化 一.EngineContext初 ...

  6. nopCommerce 3.9 大波浪系列 之 外部授权登录插件的开发实现

    一.简介 nop支持第三方外部授权登录的扩展,本篇通过编写微信公众平台登录插件进一步了解nop授权登录的开发过程. 微信公众平台.微信开放平台使用场景不一样,前者通过微信客户端进行开发如公众号,后者基 ...

  7. nopCommerce 3.9 大波浪系列 之 使用Redis主从高可用缓存

    一.概述 nop支持Redis作为缓存,Redis出众的性能在企业中得到了广泛的应用.Redis支持主从复制,HA,集群. 一般来说,只有一台Redis是不可行的,原因如下: 单台Redis服务器会发 ...

  8. nopCommerce 3.9 大波浪系列 之 使用部署在Docker中的Redis缓存主从服务

    一.概述 nop支持Redis作为缓存,Redis出众的性能在企业中得到了广泛的应用.Redis支持主从复制,HA,集群. 一般来说,只有一台Redis是不可行的,原因如下: 单台Redis服务器会发 ...

  9. nopCommerce 3.9 大波浪系列 之 global.asax

    一.nop的global.asax文件 nop3.9基于ASP.NET MVC 5框架开发,而ASP.NET MVC中global.asax文件包含全局应用程序事件的事件处理程序,它响应应用程序级别和 ...

随机推荐

  1. linux--centos服务器配置

    配置Centos服务器   1. 安装centos 安装时注意分区留下一个/data区给网站内容存储. 2. 配置网络连接 默认centos网络连接不是开机启动的,需要配置. vi /etc/sysc ...

  2. DNS全局负载均衡(GSLB)基本原理

    原理 DNS全局负载均衡通过智能DNS解析来实现,通常在不同的地区设立多个数据中心,每个数据中心又使用多个运营商的线路.目前很多DNS服务商都提供了智能DNS服务,智能DNS通常是利用各运营商分省IP ...

  3. webpack 插件拾趣 (1) —— webpack-dev-server

    结束了一季的忙碌,我这封笔已久的博客也终究该从春困的咒印中复苏,想来写些实用易读的作为开篇,自然是最好不过. 新开个 webpack 插件/工具介绍的文章系列,约莫每周更新一篇篇幅适中的文章聊以共勉, ...

  4. Cordova各个插件使用介绍系列(一)—$cordovaSms发送短信

    详情链接地址:http://www.ncloud.hk/%E6%8A%80%E6%9C%AF%E5%88%86%E4%BA%AB/cordova-1-cordovasms/ 这是调用手机发送短信的插件 ...

  5. PHPCMS v9 自定义表单添加验证码

    1.  在 \phpcms\templates\default\formguide\show.html 中添加验证码显示 <input type="text" id=&quo ...

  6. CentOS7安装docker 启动不了解决篇

    [root@test ~]# yum update [root@test ~]# yum install docker [root@test ~]# service docker start Redi ...

  7. centos6.7下安装mysql5.6.22同时解决中文乱码问题

    1.下载 http://dev.mysql.com/downloads/mysql/ 或者使用wget下载: wget http://dev.mysql.com/get/Downloads/MySQL ...

  8. Flash TextField selectable bug block TextEvent.Link solution

    There is an old version Felx SDK bug(in my case it's Flex SDK v3.3.0.4852) that when TextField.selec ...

  9. 修改apache的默认www目录

    httpd.conf配置文件#DocumentRoot "/var/www/html" //修改前DocumentRoot "/www" #<Direct ...

  10. php防止浏览器点击返回按钮重复提交数据

    <!--html中存放隐藏域数据--> <input type="hidden" value='{$sun_nums}' name='sub_nums' /> ...