MVC插件实现
本人第一篇随笔,在园子里逛了这么久,今天也记录一篇自己的劳动成果,也是给自己以后留个记录。
最近领导让我搞一下插件化,就是实现多个web工程通过配置文件进行组装。之前由于做过一个简单的算是有点经验,当时使用的不是area,后来通过翻看orchard源码有点启发,打算使用area改一下。
实现插件化,需要解决四个问题:
1、如何发现插件以及加载插件及其所依赖的dll
2、如何注册路由,正确调用插件的Controller和Action
3、如何实现ViewEngine,正确的发现View
4、页面中的Url如何自动生成
以下下我们带着这四个问题依次分析解决:
1、如何发现插件以及加载插件及其所依赖的dll
该问题我完全使用了Nop插件的实现方式,为每个工程定义一个Plugin.txt配置文件,运行时通过注册[assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]这个方法,在Application_Start()之前发现和加载插件。PluginManager负责管理加载插件,通过解析Plugin.txt,识别插件的dll和它所依赖的dll。通过Assembly.Load()方法加载dll并使用BuildManager.AddReferencedAssembly(shadowCopiedAssembly)为web项目动态添加引用。由于web项目存在不同的信任级别,在FullTrust级别可以将这些dll直接拷贝到AppDomain.CurrentDomain.DynamicDirectory文件夹下面。但是在其他信任级别下无法访问该目录,Nop通过复制到一个临时目录并在web.config中修改 <probingprivatePath="Plugins/bin/" />的值来让iis自动探索该目录。
代码如下:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks; namespace Framework.Core.Plugins
{
public class Plugin
{
/// <summary>
/// 插件名称,唯一标识
/// </summary>
public string PluginName { get; set; } /// <summary>
/// 插件显示名称
/// </summary>
public virtual string PluginFriendlyName { get; set; } /// <summary>
/// 插件主文件(DLL)名称
/// </summary>
public string PluginFileName { get; set; } /// <summary>
/// 插件控制器命名空间
/// </summary>
public string ControllerNamespace { get; set; } /// <summary>
/// 插件主文件文件信息
/// </summary>
public virtual FileInfo PluginFileInfo { get; internal set; } /// <summary>
/// 插件程序集
/// </summary>
public virtual Assembly ReferencedAssembly { get; internal set; } /// <summary>
/// 描述
/// </summary>
public virtual string Description { get; set; } /// <summary>
/// 显示顺序
/// </summary>
public virtual int DisplayOrder { get; set; } /// <summary>
/// 是否已安装
/// </summary>
public virtual bool Installed { get; set; }
}
}
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 Framework.Core.Plugins;
using Framework.Core.Infrastructure; [assembly: PreApplicationStartMethod(typeof(PluginManager), "Initialize")]
namespace Framework.Core.Plugins
{
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 public static IEnumerable<Plugin> ReferencedPlugins { get; set; } /// <summary>
/// 初始化插件
/// </summary>
public static void Initialize()
{
using (new WriteLockDisposable(Locker))
{
var pluginFolder = new DirectoryInfo(CommonHelper.MapPath(PluginsPath));
_shadowCopyFolder = new DirectoryInfo(CommonHelper.MapPath(ShadowCopyPath));
var referencedPlugins = new List<Plugin>(); _clearShadowDirectoryOnStartup = !String.IsNullOrEmpty(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]) &&
Convert.ToBoolean(ConfigurationManager.AppSettings["ClearPluginsShadowDirectoryOnStartup"]); try
{
//获取已经加载的插件名称
var installedPluginNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath()); Debug.WriteLine("创建临时目录");
Directory.CreateDirectory(pluginFolder.FullName);
Directory.CreateDirectory(_shadowCopyFolder.FullName); //获取临时目录中的dll文件
var binFiles = _shadowCopyFolder.GetFiles("*", SearchOption.AllDirectories);
if (_clearShadowDirectoryOnStartup)
{
//清除临时目录中的数据
foreach (var f in binFiles)
{
Debug.WriteLine("删除文件: " + f.Name);
try
{
File.Delete(f.FullName);
}
catch (Exception exc)
{
Debug.WriteLine("删除文件异常: " + f.Name + ". 异常信息: " + exc);
}
}
} //加载插件
foreach (var dfd in GetPluginFilesAndPlugins(pluginFolder))
{
var pluginFile = dfd.Key;
var plugin = dfd.Value;
//验证插件名称
if (String.IsNullOrWhiteSpace(plugin.PluginName))
throw new Exception(string.Format("插件:'{0}' 没有设置名称. 请设置唯一的PluginName,重新编译.", pluginFile.FullName));
if (referencedPlugins.Contains(plugin))
throw new Exception(string.Format("插件名称:'{0}' 已经被占用,请重新设置唯一的PluginName,重新编译", plugin.PluginName)); //设置是否已经安装
plugin.Installed = installedPluginNames
.FirstOrDefault(x => x.Equals(plugin.PluginName, StringComparison.InvariantCultureIgnoreCase)) != null; try
{
if (pluginFile.Directory == null)
throw new Exception(string.Format("'{0}'插件目录无效,无法解析插件dll文件", pluginFile.Name)); //获取插件中的所有DLL
var pluginDLLs = pluginFile.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(); //获取主插件文件
var mainPluginDLL = pluginDLLs
.FirstOrDefault(x => x.Name.Equals(plugin.PluginFileName, StringComparison.InvariantCultureIgnoreCase));
plugin.PluginFileInfo = mainPluginDLL; //复制主文件到临时目录,并加载主文件
plugin.ReferencedAssembly = PerformFileDeploy(mainPluginDLL); //加载其他插件相关dll
foreach (var dll in pluginDLLs
.Where(x => !x.Name.Equals(mainPluginDLL.Name, StringComparison.InvariantCultureIgnoreCase))
.Where(x => !IsAlreadyLoaded(x)))
PerformFileDeploy(dll);
referencedPlugins.Add(plugin);
}
catch (ReflectionTypeLoadException ex)
{
var msg = string.Format("Plugin '{0}'. ", plugin.PluginFriendlyName);
foreach (var e in ex.LoaderExceptions)
msg += e.Message + Environment.NewLine; var fail = new Exception(msg, ex);
throw fail;
}
catch (Exception ex)
{
var msg = string.Format("Plugin '{0}'. {1}", plugin.PluginFriendlyName, 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; }
} /// <summary>
/// 安装插件
/// </summary>
/// <param name="pluginName">插件名称</param>
public static void MarkPluginAsInstalled(string pluginName)
{
if (String.IsNullOrEmpty(pluginName))
throw new ArgumentNullException("pluginName"); var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
if (!File.Exists(filePath))
using (File.Create(filePath))
{ } var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
bool alreadyMarkedAsInstalled = installedPluginSystemNames
.FirstOrDefault(x => x.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)) != null;
if (!alreadyMarkedAsInstalled)
installedPluginSystemNames.Add(pluginName);
PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath);
} /// <summary>
/// 卸载插件
/// </summary>
/// <param name="pluginName">插件名称</param>
public static void MarkPluginAsUninstalled(string pluginName)
{
if (String.IsNullOrEmpty(pluginName))
throw new ArgumentNullException("pluginName"); var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
if (!File.Exists(filePath))
using (File.Create(filePath))
{ } var installedPluginSystemNames = PluginFileParser.ParseInstalledPluginsFile(GetInstalledPluginsFilePath());
bool alreadyMarkedAsInstalled = installedPluginSystemNames
.FirstOrDefault(x => x.Equals(pluginName, StringComparison.InvariantCultureIgnoreCase)) != null;
if (alreadyMarkedAsInstalled)
installedPluginSystemNames.Remove(pluginName);
PluginFileParser.SaveInstalledPluginsFile(installedPluginSystemNames,filePath);
} /// <summary>
/// 卸载所有插件
/// </summary>
public static void MarkAllPluginsAsUninstalled()
{
var filePath = CommonHelper.MapPath(InstalledPluginsFilePath);
if (File.Exists(filePath))
File.Delete(filePath);
} #endregion #region 工具 /// <summary>
///获取指定目录下的所有插件文件(Plugin.text)和插件信息(Plugin)
/// </summary>
/// <param name="pluginFolder">Plugin目录</param>
/// <returns>插件文件和插件</returns>
private static IEnumerable<KeyValuePair<FileInfo, Plugin>> GetPluginFilesAndPlugins(DirectoryInfo pluginFolder)
{
if (pluginFolder == null)
throw new ArgumentNullException("pluginFolder"); var result = new List<KeyValuePair<FileInfo, Plugin>>();
//add display order and path to list
foreach (var descriptionFile in pluginFolder.GetFiles("Plugin.txt", SearchOption.AllDirectories))
{
if (!IsPackagePluginFolder(descriptionFile.Directory))
continue; //解析插件配置文件
var plugin = PluginFileParser.ParsePluginFile(descriptionFile.FullName);
result.Add(new KeyValuePair<FileInfo, Plugin>(descriptionFile, plugin));
}
//插件排序,数字越低排名越高
result.Sort((firstPair, nextPair) => firstPair.Value.DisplayOrder.CompareTo(nextPair.Value.DisplayOrder));
return result;
} /// <summary>
/// 判断程序集是否已经加载
/// </summary>
/// <param name="fileInfo">程序集文件</param>
/// <returns>Result</returns>
private static bool IsAlreadyLoaded(FileInfo fileInfo)
{ try
{
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(fileInfo.FullName);
if (fileNameWithoutExt == null)
throw new Exception(string.Format("无法获取文件名:{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("无法判断程序集是否加载。" + exc);
}
return false;
} /// <summary>
///执行解析文件
/// </summary>
/// <param name="plug">插件文件</param>
/// <returns>Assembly</returns>
private static Assembly PerformFileDeploy(FileInfo plug)
{
if (plug.Directory.Parent == null)
throw new InvalidOperationException("插件" + plug.Name + ":目录无效" ); FileInfo shadowCopiedPlug; if (CommonHelper.GetTrustLevel() != AspNetHostingPermissionLevel.Unrestricted)
{
//运行在MediumTrust下(在MediumTrust下无法访问DynamicDirectory,也无法设置ResolveAssembly event)
//需要将所有插件dll都需要拷贝到~/Plugins/bin/下的临时目录,因为web.config中的probingPaths设置的是该目录
var shadowCopyPlugFolder = Directory.CreateDirectory(_shadowCopyFolder.FullName);
shadowCopiedPlug = InitializeMediumTrust(plug, shadowCopyPlugFolder);
}
else
{
//运行在FullTrust下,可以直接使用标准的DynamicDirectory文件夹,作为临时目录
var directory = AppDomain.CurrentDomain.DynamicDirectory;
Debug.WriteLine(plug.FullName + " to " + directory);
shadowCopiedPlug = InitializeFullTrust(plug, new DirectoryInfo(directory));
} //加载程序集
var shadowCopiedAssembly = Assembly.Load(AssemblyName.GetAssemblyName(shadowCopiedPlug.FullName)); //添加引用信息到BuildManager
Debug.WriteLine("添加到BuildManager: '{0}'", shadowCopiedAssembly.FullName);
BuildManager.AddReferencedAssembly(shadowCopiedAssembly); return shadowCopiedAssembly;
} /// <summary>
/// FullTrust级别下的插件初始化
/// </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 + " 文件已被锁, 尝试重命名");
//可能被 devenv锁住,可以通过重命名来解锁
try
{
var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
File.Move(shadowCopiedPlug.FullName, oldFile);
}
catch (IOException exc)
{
throw new IOException(shadowCopiedPlug.FullName + " 重命名失败, 无法初始化插件", exc);
}
//重新尝试复制
File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
}
return shadowCopiedPlug;
} /// <summary>
/// MediumTrust级别下的插件初始化
/// </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)); //检查插件是否存在,如果存在,判断是否需要更新
if (shadowCopiedPlug.Exists)
{
var areFilesIdentical = shadowCopiedPlug.CreationTimeUtc.Ticks >= plug.CreationTimeUtc.Ticks;
if (areFilesIdentical)
{
Debug.WriteLine("插件已经存在,不需要更新: '{0}'", shadowCopiedPlug.Name);
shouldCopy = false;
}
else
{
//删除现有插件
Debug.WriteLine("有新插件; 删除现有插件: '{0}'", shadowCopiedPlug.Name);
File.Delete(shadowCopiedPlug.FullName);
}
} if (shouldCopy)
{
try
{
File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
}
catch (IOException)
{
Debug.WriteLine(shadowCopiedPlug.FullName + " 文件已被锁, 尝试重命名");
//可能被 devenv锁住,可以通过重命名来解锁
try
{
var oldFile = shadowCopiedPlug.FullName + Guid.NewGuid().ToString("N") + ".old";
File.Move(shadowCopiedPlug.FullName, oldFile);
}
catch (IOException exc)
{
throw new IOException(shadowCopiedPlug.FullName + " 重命名失败, 无法初始化插件", exc); }
//重新尝试复制
File.Copy(plug.FullName, shadowCopiedPlug.FullName, true);
}
} return shadowCopiedPlug;
} /// <summary>
///判断文件是否属于插件目录下的文件(Plugins下)
/// </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>
/// 获取InstalledPlugins.txt文件的物理路径
/// </summary>
/// <returns></returns>
private static string GetInstalledPluginsFilePath()
{
return CommonHelper.MapPath(InstalledPluginsFilePath);
} #endregion
}
}
2、如何注册路由,正确调用插件的Controller和Action
路由我通过扩展现Mvc的RouteCollection的MapRoute方法,将插件名称作为area强行插入到DataToken中,这样在ViewEngine中可以使用area规则来发现视图。然后重写RegisterRoutes方法,通过遍历所有插件集合,添加指定的路由,并将所有插件的Controller的命名空间写入到插件匹配模式中,这样可以解决不同插件之间Controller重名的问题。
public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces,string area)
{
if (routes == null)
{
throw new ArgumentNullException("routes");
}
if (url == null)
{
throw new ArgumentNullException("url");
} Route route = new Route(url, new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(defaults),
Constraints = new RouteValueDictionary(constraints),
DataTokens = new RouteValueDictionary()
}; if ((namespaces != null) && (namespaces.Length > ))
{
route.DataTokens["Namespaces"] = namespaces;
} if (!string.IsNullOrEmpty(area))
{
route.DataTokens["area"] = area;
} routes.Add(name, route); return route;
}
public static void RegisterPluginRoutes(RouteCollection routes)
{ routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); foreach (var plugin in PluginManager.ReferencedPlugins)
{ routes.MapRoute(plugin.PluginName,
string.Concat(plugin.PluginName, "/{controller}/{action}/{id}"),
new { area= plugin.PluginName, controller = "Home", action = "Index", id = UrlParameter.Optional },
new string[]{ plugin.ControllerNamespace}, plugin.PluginName);
} routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional },
namespaces:new string[] { "GWT.Framework.Web.Controllers" }
); }
3、如何实现ViewEngine,正确的发现View
关于这个问题我发现Nop和Orchard中好多地方都是硬编码,通过VIEW(~/Plugin/XXX/views/XXX/XX.csthml)的方式来发现视图。不知他们是何用意,我觉这样耦合度过高。此处我通过前面路由中插入的area并配合实现一个继承自RazorViewEngine的视图引擎,将所有的插件请求定位到~/Plugins/{area}/Views/{controller}/{action}.cshtml。同时替换掉原有的视图引擎。代码如下:
public class PluginViewEngine : RazorViewEngine
{
public PluginViewEngine()
{ AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Plugins/{2}/Views/{1}/{0}.cshtml",
"~/Plugins/{2}/Views/Shared/{0}.cshtml"
};
AreaMasterLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Plugins/{2}/Views/{1}/{0}.cshtml",
"~/Plugins/{2}/Views/Shared/{0}.cshtml"
};
AreaPartialViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml",
"~/Plugins/{2}/Views/{1}/{0}.cshtml",
"~/Plugins/{2}/Views/Shared/{0}.cshtml"
}; FileExtensions = new[] { "cshtml" };
}
}
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new PluginViewEngine()); AreaRegistration.RegisterAllAreas();
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
ApplicationStartup.RegisterPluginRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
4、页面中的Url如何自动生成
我们知道页面中的url可以使用硬编码方式比如/Home/Index,也可以使用Html.ActionLink(“Index”,“Home”)或者Url.Action方式实现。前者硬编码的方式已经不适用于插件化,因为开发者不知道是否会被用作插件,如果强行写入/Pluin1/Home/Index,势必导致本地无法运行。在插件系统中应该使用后两者,因为他们都是用过路由系统输出URL的。MVC框架会基于当前的Controller到路由系统中找到匹配的路径返回给前台页面。
对于URL我们可以使用Html和Url帮助器生成,但是对于Script和css等内容文件MVC框架就无能为力了。为了解决内容文件的加载,我扩展了UrlHelper帮助器,根据当前的请求中是否有area来生成相对路径。代码如下
public static string PluginContent(this UrlHelper urlHelper, string url)
{
if (urlHelper.RequestContext.RouteData.Values.Keys.Contains("area"))
{
var area = urlHelper.RequestContext.RouteData.Values["area"].ToString();
if (!string.IsNullOrEmpty(area))
{
url = url.Substring(url.IndexOf("/") + );
return string.Format("~/Plugins/{0}/{1}", area, url);
}
}
return url; }
在页面中可以如下调用: @Url.PluginContent("/Views/Shared/_Layout.cshtml")
参考文档:
https://shazwazza.com/post/Developing-a-plugin-framework-in-ASPNET-with-medium-trust.aspx
http://www.cnblogs.com/longyunshiye/p/5786446.html
MVC插件实现的更多相关文章
- MVC 插件化框架支持原生MVC的Area和路由特性
.NET MVC 插件化框架支持原生MVC的Area和路由特性 前面开放的源码只是简单的Plugin的实现,支持了插件的热插拔,最近晚上偶然想到,原生的MVC提供Areas和RouteAtrribut ...
- ASP.NET MVC 插件化
ASP.NET MVC 插件化机制 2015-03-14 22:25 by 杨康新, 1328 阅读, 15 评论, 收藏, 编辑 概述 nopCommerce的插件机制的核心是使用BuildMana ...
- MVC 插件式开发
MVC 插件式开发 在开发一个OA系统是,我们可能遇到 A模块. B模块 .C模块,这也模块组成一个完整的系统,买给客服.现在又有一个客服要我们做一个OA系统,唉我们发现,跟上一个OA系统差不多,但没 ...
- MVC插件
MVC插件 最近领导让我搞一下插件化,就是实现多个web工程通过配置文件进行组装.之前由于做过一个简单的算是有点经验,当时使用的不是area,后来通过翻看orchard源码有点启发,打算使用area改 ...
- Asp.net MVC 插件式应用框架
Asp.net MVC 插件式应用框架 2013年05月13日 10:16供稿中心: 互联网运营部 摘要:这几年来做了很多个网站系统,一直坚持使用asp.net mvc建站,每次都从头开始做Layou ...
- 零基础ASP.NET Core MVC插件式开发
零基础ASP.NET Core MVC插件式开发 一个项目随着业务模块的不断增加,系统会越来越庞大.如果参与开发的人员越多,管理起来也难度也很大.面对这样的情况,首先想到的是模块化插件式开发,根据业务 ...
- MVC插件式开发平台
---恢复内容开始--- 经过DyOS.BraveOS1.0再到BraveOS2.0,系统现在已经开发了下载. 我们的目标是,网页版操作系统,可以在线安装更新软件,并提供二次开发平台,提供基础的逻辑和 ...
- ASP.NET MVC 插件化机制
概述 nopCommerce的插件机制的核心是使用BuildManager.AddReferencedAssembly将使用Assembly.Load加载的插件程序集添加到应用程序域的引用中.具 体实 ...
- .NET MVC 插件化框架支持原生MVC的Area和路由特性
前面开放的源码只是简单的Plugin的实现,支持了插件的热插拔,最近晚上偶然想到,原生的MVC提供Areas和RouteAtrribute等路由特性标签,按照先前的做法,无法解析插件的路由特性和Are ...
- [转]NopCommerce MVC 插件机制分析
原文地址:http://www.cnblogs.com/haoxinyue/archive/2013/06/06/3105541.html 基本原理 插件话的应用程序一般都是先定义插件接口,然后把插件 ...
随机推荐
- We Chall-Training: Crypto - Caesar I-Writeup
MarkdownPad Document html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,ab ...
- PS切图篇(一)---界面设置
#工作区设置 四大主要面板:信息 字符 图层 历史记录 打开必要属性: 选择工具设置 选择图层的方式:ctrl+鼠标左击想选择的图层
- cmake的两个命令: option 和 configure_file
本节要讨论的是cmake的两个命令: option 和 configure_file option 选项,让你可以根据选项值进行条件编译. configure_file 配置文件,让你可以在代码文件中 ...
- java 文件操作 读取字节级数据(读取)
package com.swust; import java.io.*; /* * 功能:按照双精度浮点型.整型.布尔型.字符型.和字符串型的顺序从名为sample.dat文件读取数据 * 分析:用F ...
- PHP 中使用 Composer
在线安装版本: http://www.phpcomposer.com/ 这个是国内的composer网站 thinkphp5自带了composer.phar组件,如果没有安装,则需要进行安装 以下命令 ...
- C#中的foreach语句与枚举器接口(IEnumerator)及其泛型 相关问题
这个问题从<C#高级编程>数组一节中的foreach语句(6.7.2)发现的. 因为示例代码与之前的章节连贯,所以我修改了一下,把自定义类型改为了int int[] bs = { 2, 3 ...
- IOS缓存管理之PINCache使用
前言: 今年重点在于公司iOS架构的梳理工作,上周整理了http请求接口管理与解耦,接下来准备整理一下项目中的缓存处理,目前项目中使用的是PINCache,去年加入这个开源框架时并没有对这个框架进行了 ...
- Linux下ifort的安装记录
首先进入网址https://software.intel.com/en-us/qualify-for-free-software/student 下载Intel Parallel Studio XE ...
- C语言常见错误中英文对照表
C语言常见错误中英文对照表(网络搜索及经验积累不断更新中) 常见错误中英文对照表 fatal error C1003: error count exceeds number; stopping co ...
- vs2017 .net core WebApp 去掉ApplicationInsights
vs2017新建的 .net core WebApp都内置了这个遥测中间件进去,嗯,用AZURE的话是不错能无缝支持.但不用AZURE就没什么用了. 为了不占地方和提高一点点初始启动的速度,对新建的项 ...