nopCommerce 3.9 中插件類型整理,詳細介紹 IWidgetPlugin 插件顯示原理,載入流程分析。 ...
一.插件簡介
插件用於擴展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方法從而獲取顯示內容並輸出到頁面中
1 public static MvcHtmlString Widget(this HtmlHelper helper, string widgetZone, object additionalData = null, string area = null) 2 { 3 return helper.Action("WidgetsByZone", "Widget", new { widgetZone = widgetZone, additionalData = additionalData, area = area }); 4 }
再來看下小部件中NivoSliderPlugin類,它繼承了IWidgetPlugin介面
並實現了IWidgetPlugin介面GetWidgetZones()方法返回顯示位置名稱集合。
我們發現NivoSlider插件包含了“home_page_top”的位置。
Index.chtml試圖中也出現了@Html.Widget("home_page_top"),因此該小部件會在首頁中顯示。
所以試圖中想要使用小部件,使用@Html.Widget("位置名稱")就可以了。
ok,知道怎麼使用了,我們再看看源碼中涉及到哪些相關介面,之間調用關係是怎樣的,先上圖。
首先WidgetController控制器WidgetsByZone會返回部分視圖
1 [ChildActionOnly] 2 public virtual ActionResult WidgetsByZone(string widgetZone, object additionalData = null) 3 { 4 //查找到符合要求的List<RenderWidgetModel> 5 var model = _widgetModelFactory.GetRenderWidgetModels(widgetZone, additionalData); 6 7 //no data? 8 if (!model.Any()) 9 return Content(""); 10 11 return PartialView(model); 12 }
WidgetsByZone.cshtml中代碼如下,我們發現這裡重新調用了插件中某個action
1 @model List<RenderWidgetModel> 2 @using Nop.Web.Models.Cms; 3 @foreach (var widget in Model) 4 { 5 @Html.Action(widget.ActionName, widget.ControllerName, widget.RouteValues) 6 }
那上邊的Action信息又是哪裡得到的呢?IWidgetPlugin介面GetDisplayWidgetRoute就是用來返回顯示時調用的處理Action信息。
NivoSliderPlugin類實現了GetDisplayWidgetRoute代碼如下。
1 /// <summary> 2 /// 獲取顯示插件的路由 3 /// </summary> 4 /// <param name="widgetZone">Widget zone where it's displayed</param> 5 /// <param name="actionName">Action name</param> 6 /// <param name="controllerName">Controller name</param> 7 /// <param name="routeValues">Route values</param> 8 public void GetDisplayWidgetRoute(string widgetZone, out string actionName, out string controllerName, out RouteValueDictionary routeValues) 9 { 10 actionName = "PublicInfo"; 11 controllerName = "WidgetsNivoSlider"; 12 routeValues = new RouteValueDictionary 13 { 14 {"Namespaces", "Nop.Plugin.Widgets.NivoSlider.Controllers"}, 15 {"area", null}, 16 {"widgetZone", widgetZone} 17 }; 18 }
總結下: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>集合,過濾條件為部件位置,用戶,商城。

1 /// <summary> 2 /// Load active widgets 3 /// </summary> 4 /// <param name="widgetZone">Widget zone</param> 5 /// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param> 6 /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param> 7 /// <returns>Widgets</returns> 8 public virtual IList<IWidgetPlugin> LoadActiveWidgetsByWidgetZone(string widgetZone, Customer customer = null, int storeId = 0) 9 { 10 if (String.IsNullOrWhiteSpace(widgetZone)) 11 return new List<IWidgetPlugin>(); 12 13 return LoadActiveWidgets(customer, storeId) 14 .Where(x => x.GetWidgetZones().Contains(widgetZone, StringComparer.InvariantCultureIgnoreCase)).ToList(); 15 } 16 17 /// <summary> 18 /// Load active widgets 19 /// </summary> 20 /// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param> 21 /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param> 22 /// <returns>Widgets</returns> 23 public virtual IList<IWidgetPlugin> LoadActiveWidgets(Customer customer = null, int storeId = 0) 24 { 25 return LoadAllWidgets(customer, storeId) 26 .Where(x => _widgetSettings.ActiveWidgetSystemNames.Contains(x.PluginDescriptor.SystemName, StringComparer.InvariantCultureIgnoreCase)).ToList(); 27 } 28 29 /// <summary> 30 /// Load all widgets 31 /// </summary> 32 /// <param name="customer">Load records allowed only to a specified customer; pass null to ignore ACL permissions</param> 33 /// <param name="storeId">Load records allowed only in a specified store; pass 0 to load all records</param> 34 /// <returns>Widgets</returns> 35 public virtual IList<IWidgetPlugin> LoadAllWidgets(Customer customer = null, int storeId = 0) 36 { 37 return _pluginFinder.GetPlugins<IWidgetPlugin>(customer: customer, storeId: storeId).ToList(); 38 }按條件獲取到可用小部件
IPluginFinder
_pluginFinder.GetPlugins<IWidgetPlugin>(customer: customer, storeId: storeId).ToList();
查詢繼承IWidgetPlugin介面的插件也就是小部件了,這裡只返回IWidgetPlugin實現類。
PluginManager

1 using System; 2 using System.Collections.Generic; 3 using System.Configuration; 4 using System.Diagnostics; 5 using System.IO; 6 using System.Linq; 7 using System.Reflection; 8 using System.Threading; 9 using System.Web; 10 using System.Web.Compilation; 11 using Nop.Core.ComponentModel; 12 using Nop.Core.Plugins; 13 14 //Contributor: Umbraco (http://www.umbraco.com). Thanks a lot! 15 //SEE THIS POST for full details of what this does - http://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx 16 17 [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")] 18 namespace Nop.Core.Plugins 19 { 20 /// <summary> 21 /// Sets the application up for the plugin referencing 22 /// </summary> 23 public class PluginManager 24 { 25 #region Const 26 27 private const string InstalledPluginsFilePath = "~/App_Data/InstalledPlugins.txt"; 28 private const string PluginsPath = "~/Plugins"; 29 private const string ShadowCopyPath = "~/Plugins/bin"; 30 31 #endregion 32 33 #region Fields 34 35 private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); 36 private static DirectoryInfo _shadowCopyFolder; 37 private static bool _clearShadowDirectoryOnStartup; 38 39 #endregion 40 41 #region Methods 42 43 /// <summary> 44 /// Returns a collection of all referenced plugin assemblies that have been shadow copied 45 /// </summary> 46 public static IEnumerable<PluginDescriptor> ReferencedPlugins { get; set; } 47 48 /// <summary> 49 /// Returns a collection of all plugin which are not compatible with the current version 50 /// </summary> 51 public static IEnumerable<string> IncompatiblePlugins { get; set; } 52 53 /// <summary> 54 /// Initialize 55 /// </summary> 56 public static void Initialize() 57 { 58 using (new WriteLockDisposable(Locker)) 59 { 60 // TODO: Add verbose exception handling / raising here since this is happening on app startup and could 61 // prevent app from starting altogether 62 var pluginFolder = new DirectoryInfo(CommonHelper.MapPath(PluginsPath)); 63 _shadowCopyFolder = new DirectoryInfo(CommonHelper.MapPath(ShadowCopyPath)); 64 65 var referencedPlugins = new List<PluginDescriptor>(); 66 var incompatiblePlugins = new List<string>(); 67 68 _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) && 69 Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]); 70 71 try 72 { 73 var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); 74 75 Debug.WriteLine("Creating shadow copy folder and querying for dlls"); 76 //ensure folders are created 77 Directory.CreateDirectory(pluginFolder.FullName); 78 Directory.CreateDirectory(_shadowCopyFolder.FullName); 79 80 //get list of all files in bin 81 var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories); 82 if (_clearShadowDirectoryOnStartup) 83 { 84 //clear out shadow copied plugins 85 foreach (var f in binFiles) 86 { 87 Debug.WriteLine("Deleting " + f.Name); 88 try 89 { 90 File.Delete(f.FullName); 91 } 92 catch (Exception exc) 93 { 94 Debug.WriteLine("Error deleting file " + f.Name + ". Exception: " + exc); 95 } 96 } 97 } 98 99 //load description files 100 foreach (var dfd in GetDescriptionFilesAndDescriptors(pluginFolder)) 101 { 102 var descriptionFile = dfd.Key; 103 var pluginDescriptor = dfd.Value; 104 105 //ensure that version of plugin is valid 106 if (!pluginDescriptor.SupportedVersions.Contains(NopVersion.CurrentVersion, StringComparer.InvariantCultureIgnoreCase)) 107 { 108 incompatiblePlugins.Add(pluginDescriptor.SystemName); 109 continue; 110 } 111 112 //some validation 113 if (String.IsNullOrWhiteSpace(pluginDescriptor.SystemName)) 114 throw new Exception(string.Format("A plugin '{0}' has no system name. Try assigning the plugin a unique name and recompiling.", descriptionFile.FullName)); 115 if (referencedPlugins.Contains(pluginDescriptor)) 116 throw new Exception(string.Format("A plugin with '{0}' system name is already defined", pluginDescriptor.SystemName)); 117 118 //set 'Installed' property 119 pluginDescriptor.Installed = installedPluginSystemNames 120 .FirstOrDefault(x => x.Equals(pluginDescriptor.SystemName, StringComparison.InvariantCultureIgnoreCase)) != null; 121 122 try 123 { 124 if (descriptionFile.Directory == null) 125 throw new Exception(string.Format("Directory cannot be resolved for '{0}' description file", descriptionFile.Name)); 126 //get list of all DLLs in plugins (not in bin!) 127 var pluginFiles = descriptionFile.Directory.GetFiles("*.dll", SearchOption.AllDirectories) 128 //just make sure we're not registering shadow copied plugins 129 .Where(x => !binFiles.Select(q => q.FullName).Contains(x.FullName)) 130 .Where(x => IsPackagePluginFolder(x.Directory)) 131 .ToList(); 132 133 //other plugin description info 134 var mainPluginFile = pluginFiles 135 .FirstOrDefault(x => x.Name.Equals(pluginDescriptor.PluginFileName, StringComparison.InvariantCultureIgnoreCase)); 136 pluginDescriptor.OriginalAssemblyFile = mainPluginFile; 137 138 //shadow copy main plugin file 139 pluginDescriptor.ReferencedAssembly = PerformFileDeploy(mainPluginFile); 140 141 //load all other referenced assemblies now 142 foreach (var plugin in pluginFiles 143 .Where(x => !x.Name.Equals(mainPluginFile.Name, StringComparison.InvariantCultureIgnoreCase)) 144 .Where(x => !IsAlreadyLoaded(x))) 145 PerformFileDeploy(plugin); 146 147 //init plugin type (only one plugin per assembly is allowed) 148 foreach (var t in pluginDescriptor.ReferencedAssembly.GetTypes()) 149 if (typeof(IPlugin).IsAssignableFrom(t)) 150 if (!t.IsInterface) 151 if (t.IsClass && !t.IsAbstract) 152 { 153 pluginDescriptor.PluginType = t; 154 break; 155 } 156 157 referencedPlugins.Add(pluginDescriptor); 158 } 159 catch (ReflectionTypeLoadException ex) 160 { 161 //add a plugin name. this way we can easily identify a problematic plugin 162 var msg = string.Format("Plugin '{0}'. ", pluginDescriptor.FriendlyName); 163 foreach (var e in ex.LoaderExceptions) 164 msg += e.Message + Environment.NewLine; 165 166 var fail = new Exception(msg, ex); 167 throw fail; 168 } 169 catch (Exception ex) 170 { 171 //add a plugin name. this way we can easily identify a problematic plugin 172 var msg = string.Format("Plugin '{0}'. {1}", pluginDescriptor.FriendlyName, ex.Message); 173 174 var fail = new Exception(msg, ex); 175 throw fail; 176 } 177 } 178 } 179 catch (Exception ex) 180 { 181 var msg = string.Empty; 182 for (var e = ex; e != null; e = e.InnerException) 183 msg += e.Message + Environment.NewLine; 184 185 var fail = new Exception(msg, ex); 186 throw fail; 187 } 188 189 190 ReferencedPlugins = referencedPlugins; 191 IncompatiblePlugins = incompatiblePlugins; 192 193 } 194 } 195 196 /// <summary> 197 /// Mark plugin as installed 198 /// </summary> 199 /// <param name="systemName">Plugin system name</param> 200 public static void MarkPluginAsInstalled(string systemName) 201 { 202 if (String.IsNullOrEmpty(systemName)) 203 throw new ArgumentNullException("systemName"); 204 205 var filePath = CommonHelper.MapPath(InstalledPluginsFilePath); 206 if (!File.Exists(filePath)) 207 using (File.Create(filePath)) 208 { 209 //we use 'using' to close the file after it's created 210 } 211 212 213 var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); 214 bool alreadyMarkedAsInstalled = installedPluginSystemNames 215 .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null; 216 if (!alreadyMarkedAsInstalled) 217 installedPluginSystemNames.Add(systemName); 218 PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath); 219 } 220 221 /// <summary> 222 /// Mark plugin as uninstalled 223 /// </summary> 224 /// <param name="systemName">Plugin system name</param> 225 public static void MarkPluginAsUninstalled(string systemName) 226 { 227 if (String.IsNullOrEmpty(systemName)) 228 throw new ArgumentNullException("systemName"); 229 230 var filePath = CommonHelper.MapPath(InstalledPluginsFilePath); 231 if (!File.Exists(filePath)) 232 using (File.Create(filePath)) 233 { 234 //we use 'using' to close the file after it's created 235 } 236 237 238 var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); 239 bool alreadyMarkedAsInstalled = installedPluginSystemNames 240 .FirstOrDefault(x => x.Equals(systemName, StringComparison.InvariantCultureIgnoreCase)) != null; 241 if (alreadyMarkedAsInstalled) 242 installedPluginSystemNames.Remove(systemName); 243 PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath); 244 } 245 246 /// <summary> 247 /// Mark plugin as uninstalled 248 /// </summary> 249 public static void MarkAllPluginsAsUninstalled() 250 { 251 var filePath = CommonHelper.MapPath(InstalledPluginsFilePath); 252 if (File.Exists(filePath)) 253 File.Delete(filePath); 254 } 255 256 #endregion 257 258 #region Utilities 259 260 /// <summary> 261 /// Get description files 262 /// </summary> 263 /// <param name="pluginFolder">Plugin directory info</param> 264 /// <returns>Original and parsed description files</returns> 265 private static IEnumerable<KeyValuePair<FileInfo, PluginDescriptor>> GetDescriptionFilesAndDescriptors(DirectoryInfo pluginFolder) 266 { 267 if (pluginFolder == null) 268 throw new ArgumentNullException("pluginFolder"); 269 270 //create list (<file info, parsed plugin descritor>) 271 var result = new List<KeyValuePair<FileInfo, PluginDescriptor>>(); 272 //add display order and path to list 273 foreach (var descriptionFile in pluginFolder.GetFiles("Description.txt", SearchOption.AllDirectories)) 274 { 275 if (!IsPackagePluginFolder(descriptionFile.Directory)) 276 continue; 277 278 //parse file 279 var pluginDescriptor = PluginFileParser.ParsePluginDescriptionFile(descriptionFile.FullName); 280 281 //populate list 282 result.Add(new KeyValuePair<FileInfo, PluginDescriptor>(descriptionFile, pluginDescriptor)); 283 } 284 285 //sort list by display order. NOTE: Lowest DisplayOrder will be first i.e 0 , 1, 1, 1, 5, 10 286 //it's required: http://www.nopcommerce.com/boards/t/17455/load-plugins-based-on-their-displayorder-on-startup.aspx 287 result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder)); 288 return result; 289 } 290 291 /// <summary> 292 /// Indicates whether assembly file is already loaded 293 /// </summary> 294 /// <param name="fileInfo">File info</param> 295 /// <returns>Result</returns> 296 private static bool IsAlreadyLoaded(FileInfo fileInfo) 297 { 298 //compare full assembly name 299 //var fileAssemblyName = AssemblyName.GetAssemblyName(fileInfo.FullName); 300 //foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) 301 //{ 302 // if (a.FullName.Equals(fileAssemblyName.FullName, StringComparison.InvariantCultureIgnoreCase)) 303 // return true; 304 //} 305 //return false; 306 307 //do not compare the full assembly name, just filename 308 try 309 { 310 string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName); 311 if (fileNameWithoutExt == null) 312 throw new Exception(string.Format("Cannot get file extension for {0}", fileInfo.Name)); 313 foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) 314 { 315 string assemblyName = a.FullName.Split(new[] { ',' }).FirstOrDefault(); 316 if (fileNameWithoutExt.Equals(assemblyName, StringComparison.InvariantCultureIgnoreCase)) 317 return true; 318 } 319 } 320 catch (Exception exc) 321 { 322 Debug.WriteLine("Cannot validate whether an assembly is already loaded. " + exc); 323 } 324 return false; 325 } 326 327 /// <summary> 328 /// Perform file deply 329 /// </summary> 330 /// <param name="plug">Plugin file info</param> 331 /// <returns>Assembly</returns> 332 private static Assembly PerformFileDeploy(FileInfo plug) 333 { 334 if (plug.Directory == null || plug.Directory.Parent == null) 335 throw new InvalidOperationException("The plugin directory for the " + plug.Name + " file exists in a folder outside of the allowed nopCommerce folder hierarchy"); 336 337 FileInfo shadowCopiedPlug; 338 339 if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted) 340 { 341 //all plugins will need to be copied to ~/Plugins/bin/ 342 //this is absolutely required because all of this relies on probingPaths being set statically in the web.config 343 344 //were running in med trust, so copy to custom bin folder 345 var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName); 346 shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder); 347 } 348 else 349 { 350 var directory = AppDomain.CurrentDomain.DynamicDirectory; 351 Debug.WriteLine(plug.FullName + " to " + directory); 352 //were running in full trust so copy to standard dynamic folder 353 shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory)); 354 } 355 356 //we can now register the plugin definition 357 var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName)); 358 359 //add the reference to the build manager 360 Debug.WriteLine("Adding to BuildManager: '{0}'", shadowCopiedAssembly.FullName); 361 BuildManager.AddReferencedAssembly(shadowCopiedAssembly); 362 363 return shadowCopiedAssembly; 364 } 365 366 /// <summary> 367 /// Used to initialize plugins when running in Full Trust 368 /// </summary> 369 /// <param name="plug"></param> 370 /// <param name="shadowCopyPlugFolder"></param> 371 /// <returns></returns> 372 private static FileInfo InitializeFullTrust(FileInfo plug, DirectoryInfo shadowCopyPlugFolder) 373 { 374 var shadowCopiedPlug = new FileInfo(Path.Combine(shadowCopyPlugFolder.FullName, plug.Name)); 375 try 376 { 377 File.Copy(plug.FullName, shadowCopiedPlug.FullName, true); 378 } 379 catch (IOException) 380 { 381 Debug.WriteLine(shadowCopiedPlug.FullName + " is locked, attempting to rename"); 382 //this occurs when the files are locked, 383 //for some reason devenv locks plugin files some times and for another crazy reason you are allowed to rename them 384 <