ASP.NET MVC 服务器浏览器:第三部分 - 插件架构






4.80/5 (7投票s)
如何在 ASP.NET MVC 中使用插件架构。
引言
本文演示了如何在 ASP.NET MVC 中使用插件架构。我们将创建一个搜索框插件,并展示宿主(服务器浏览器)如何调用它。
插件架构
插件架构是一种在运行时创建接口对象实例的软件架构。插件架构扩展了现有类的行为,使其可用于更具体的目的。它不同于类继承,后者会修改或覆盖行为;也不同于配置,后者的行为修改仅限于定义的配置选项的能力。
通过插件架构,修改后的行为(插件)连接到一个抽象的部分类,该部分类又连接到核心类。插件使用此接口来实现核心类调用的方法,也可以调用核心类中的新方法。
所有插件模型都需要两个基本实体——插件宿主和插件本身。插件宿主的代码结构使得一部分明确定义的功能可以由外部代码模块——插件来提供。插件是独立于宿主编写和编译的,通常由其他开发人员完成。当宿主代码执行时,它会使用插件架构提供的任何机制来查找兼容的插件并加载它们,从而为宿主添加以前不存在的功能。插件架构是松散耦合的,它为您提供了极大的灵活性。
在 ASP.NET MVC 中使用插件架构
您可以阅读来自 Veeb、wynia 和 fzysqr 的这些优秀文章。它们解释了如何让 ASP.NET MVC 在插件框架上运行。插件程序集应拥有自己的模型、视图和控制器,并将插件的视图存储在插件程序集中作为嵌入式资源。这样,插件可以仅仅是一个 DLL 文件,可以添加到宿主的 bin 目录中。
在插件宿主中,我们使用 VirtualPathProvider
类来拦截 MVC 从文件系统中检索文件的调用。然后,我们需要继承 VirtualFile
类并重写 Open()
方法,以便从插件程序集中读取请求的资源。
创建搜索框插件
在 ServerExplorer 解决方案中创建一个新的 ASP.NET MVC 空项目,并将其命名为“SeachBox Plugin”。由于 SearchBox 插件项目需要共享 FileModel
类,因此创建一个新的 DataModel 项目,并将 FileModel
类移至 DataModel。在 SearchBox 项目和 ServerExplorer 项目中都添加 DataModel 引用。
在 *Controller* 文件夹中添加一个 Search Controller。然后创建一个 SearchBox 模板视图。
该搜索框由一个标签、一个文本输入框和图片链接组成。它是典型的 Windows 7 风格。当输入框获得焦点时,“Search”标签文本将消失。
这是通过脚本完成的
$(function () {
$('#searchbox input')
.focus(function () {
$('#searchbox label').hide();
})
.blur(function () {
if (this.value == "")
$('#searchbox label').show();
});
});
执行搜索后,一个清除链接将替换搜索链接。
单击清除链接,搜索框将被清除。
searchbox.ascx 如下所示
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<div id="searchbox">
<label for="search-label">
Search</label>
<%= Html.TextBox("searchinput", "", new {onchange="search()" })%>
<a id="searchlink" href="javascript:void(0);">
<img height="14" width="14" runat="server"
alt="search icon" style="border: none"
src="~/Content/Images/Search.png" />
</a> <a id="clearlink" href="javascript:clear();">
<img height="14" width="14" runat="server"
alt="search icon" style="border: none"
src="~/Content/Images/Clear.png" /></a>
</div>
<script type="text/javascript">
$(document).ready(function () {
var searchinput = $('#searchinput');
var searchText = '<%=Model %>';
if (searchText != "") {
if (searchText != searchinput.val())
searchinput.val(searchText);
$('#grid-search label').hide();
$('#searchlink').hide();
} else {
$('#clearlink').hide();
}
});
function search() {
var text = $('#searchinput').val();
var combobox = $("#location").data('tComboBox');
var path = combobox.value();
if (text == "")
showListView(path);
else
showSearchView(path, text);
}
function showSearchView(path, searchtext) {
$.ajax({
type: "POST",
url: getApplicationPath() + "Search/SearchView",
data: { folderPath: path, searchString: searchtext },
cache: false,
dataType: "html",
success: function (data) {
$('#searchlink').hide();
$('#clearlink').show();
$('#listpanel').html(data);
$('#searchinput').blur();
$('#searchinput').focus();
},
error: function (req, status, error) {
alert("switch to search view failed!");
}
});
}
function showListView(path) {
$.ajax({
type: "POST",
url: getApplicationPath() + "Explorer/FileList",
data: { folderPath: path },
cache: false,
dataType: "html",
success: function (data) {
$('#searchlink').show();
$('#clearlink').hide();
$('#listpanel').html(data);
$('#searchinput').blur();
$('#searchinput').focus();
},
error: function (req, status, error) {
alert("switch to normal view failed!");
}
});
}
function clear() {
$("#searchinput").val("");
search();
}
$('#searchbox input')
.keydown(function (e) {
if ((e.which && e.which == 13) || (e.keyCode && e.keyCode == 13)) {
$('#searchinput').blur();
$('#searchinput').focus();
return false;
}
else
if ((e.which && e.which == 27) || (e.keyCode && e.keyCode == 27)) {
clear();
}
return true;
});
</script>
将在当前文件夹下进行递归文件搜索。将获取所有匹配搜索条件的文件。对于顶层子文件和子目录的正常文件列表视图已不足够。我们需要一种新的视图类型来显示结果。
使用 Telerik 网格实现此视图。下面是视图 SearchView.ascx 的代码。
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<%
Html.Telerik().Grid<FileModel>()
.Name("searchResult")
.DataKeys(key => key.Add(x => x.FullPath))
.Columns(columns =>
{
columns.Bound(x => x.Name).Title("")
.ClientTemplate("<table cellpadding='0' cellspacing='0' class='content'>"
+ "<tr><td width='24' rowspan='2'>"
+ "<img width='24' height='24' "
+ "alt='<#= CategoryText #>' src='"
+ Url.Content("~/Content/Images/")
+ "<#= CategoryText #>.png' "
+ "style= 'vertical-align:middle;'/></td>"
+ "<td><#= Name #></td></tr>"
+ "<td><#= DirectoryName #></td>"
+ "<tr></tr></table>");
columns.Bound(x => x.Size).Format("Size: {0}").Title("");
columns.Bound(x => x.Accessed).Format("Date Modified: {0:g}").Title("");
columns.Bound(x => x.IsFolder).Hidden(true);
columns.Bound(x => x.FullPath).Hidden(true);
})
.DataBinding(dataBinding => dataBinding.Ajax()
.Select("SearchResult", "Search",
new { searchFolder = ViewBag.SearchFolder,
searchString = ViewBag.SearchString })
)
.Pageable(pager => pager.PageSize(Int32.MaxValue).Style(GridPagerStyles.Status))
.HtmlAttributes(new { style = "text-align:left; border:none;" })
.Render();
%>
使用 Telerik 网格实现此视图。下面是视图 SearchView.ascx 的代码。
public class SearchController : Controller
{
public string EmbeddedViewPath
{
get
{
return string.Format(
"~/Plugins/SearchBoxPlugin.dll/SearchBoxPlugin.Views.{0}.{1}.ascx",
this.ControllerContext.RouteData.Values["controller"],
this.ControllerContext.RouteData.Values["action"]);
}
}
//
// GET: /Search/
public ActionResult SearchView(string folderPath, string searchString)
{
ViewBag.SearchFolder = folderPath;
ViewBag.SearchString = searchString;
return PartialView(this.EmbeddedViewPath);
}
[GridAction]
public ActionResult SearchResult(string searchFolder, string searchString)
{
IList<FileModel> result = FileModel.Search(searchFolder, searchString);
return View(new GridModel<FileModel>
{
Total = result.Count,
Data = result
});
}
}
文件递归搜索
搜索目录下的文件和文件夹的最简单方法是使用 .NET 内置函数。
string[] dirs = Directory.GetDirectories(path, "*.*", SearchOption.AllDirectories);
string[] files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
但是有一个已知问题:如果存在无法访问的子文件夹,GetDirectories
和 GetFiles
函数将失败。为了克服这种副作用,我们需要实现自己的递归搜索,使用 SearchOption.TopDirectoryOnly
而不是 SearchOptions.AllDirectories
。下面是代码。
public static IList<FileModel> Search(string folderPath, string search)
{
string path = Decode(folderPath);
List<FileModel> result = new List<FileModel>();
GetFiles(path, search, result);
return result;
}
public static void GetFiles(string path, string search, List<FileModel> result)
{
try
{
string[] files = Directory.GetFiles(path, search, SearchOption.TopDirectoryOnly);
foreach (string file in files)
{
try
{
FileInfo fi = new FileInfo(file);
result.Add(new FileModel(fi));
}
catch
{
}
}
}
catch (Exception)
{
}
try
{
string[] dirs = Directory.GetDirectories(path, search, SearchOption.TopDirectoryOnly);
foreach (string dir in dirs)
{
try
{
DirectoryInfo di = new DirectoryInfo(dir);
result.Add(new FileModel(di));
}
catch
{
}
}
foreach (string dir in Directory.GetDirectories(path))
GetFiles(dir, search, result);
}
catch (Exception)
{
}
}
使搜索框可插件化
将所有视图文件的生成操作更改为嵌入式资源。这样我们就可以从程序集资源中获取实际的视图。在宿主应用程序中,任何以“~/Plugins/”为前缀的资源查找都被假定位于 DLL 中。路径的格式假定为: ~/Plugins/{DLL 文件名}/{DLL 中的资源名称}。
public string EmbeddedViewPath
{
get
{
return string.Format("~/Plugins/SearchBoxPlugin.dll/" +
"SearchBoxPlugin.Views.{0}.{1}.ascx",
this.ControllerContext.RouteData.Values["controller"],
this.ControllerContext.RouteData.Values["action"]);
}
}
调用搜索框插件
宿主项目(ServerExplorer)不必引用插件程序集。在运行时,宿主应用程序将检查插件 DLL 是否存在于 bin 文件夹中。如果插件存在,将加载插件视图;否则,将加载一个空视图。
如果 bin 文件夹中没有 SearchBoxPlugin.dll,则不会加载搜索框。
如果 SearchBoxPlugin.dll 存在于 bin 文件夹中,则会加载搜索框。
使用插件视图
在 index.aspx 中,我们添加以下行
<% Html.RenderPartial("~/Plugins/SearchBoxPlugin.dll/
SearchBoxPlugin.Views.Search.SearchBox.ascx","");%>
命名约定很重要。以“~/Plugins”开头的路径是插件资源路径。接下来的部分是插件程序集的文件名。然后是嵌入式资源名称。
VirtualPathProvider
类为开发人员提供了一种拦截 MVC 从文件系统中检索文件调用的方法,并以我们认为合适的方式(在我们的例子中,直接从程序集中)提供这些文件。AssemblyResourceProvider
继承了 VirtualPathProvider
类,以将插件控制器和视图嵌入到单独的程序集中,然后在需要时加载它们。
public override bool FileExists(string virtualPath)
{
if (IsAppResourcePath(virtualPath))
{
if (IsAppResourceExisted(virtualPath))
return true;
else
{
if (ConfigurationManager.AppSettings["EmptyViewVirtualPath"] != null)
return base.FileExists(
ConfigurationManager.AppSettings["EmptyViewVirtualPath"]);
return false;
}
}
else
return base.FileExists(virtualPath);
}
public override VirtualFile GetFile(string virtualPath)
{
if (IsAppResourcePath(virtualPath))
{
return new AssemblyResourceVirtualFile(virtualPath);
}
else
return base.GetFile(virtualPath);
}
private bool IsAppResourceExisted(string virtualPath)
{
string path = VirtualPathUtility.ToAppRelative(virtualPath);
string[] parts = path.Split('/');
string assemblyName = parts[2];
string resourceName = parts[3];
assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName);
if (!File.Exists(assemblyName))
return false;
byte[] assemblyBytes = File.ReadAllBytes(assemblyName);
Assembly assembly = Assembly.Load(assemblyBytes);
if (assembly != null)
{
string[] resourceList = assembly.GetManifestResourceNames();
bool found = Array.Exists(resourceList, r=> r.Equals(resourceName));
return found;
}
return false;
}
在 FileExists()
方法中,如果路径是插件资源路径,我们将检查程序集和程序集资源是否存在,而不是调用基类的 FileExists
方法。
首先,我们获取程序集的完整路径并检查文件是否存在。
assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName);
if (!File.Exists(assemblyName))
return false;
我们使用反射获取程序集。
byte[] assemblyBytes = File.ReadAllBytes(assemblyName);
Assembly assembly = Assembly.Load(assemblyBytes);
然后我们检查资源是否存在。
string[] resourceList = assembly.GetManifestResourceNames();
bool found = Array.Exists(resourceList, r=> r.Equals(resourceName));
如果插件资源不存在,我们将使用一个空视图。
if (IsAppResourceExisted(virtualPath))
return true;
else
{
if (ConfigurationManager.AppSettings["EmptyViewVirtualPath"] != null)
return base.FileExists(
ConfigurationManager.AppSettings["EmptyViewVirtualPath"]);
return false;
}
在 web.config 中,我们添加
<appSettings>
<add key="EmptyViewVirtualPath" value="~/Views/Shared/Empty.ascx" />
</appSettings>
在 GetFile()
方法中,我们获取资源而不是物理文件。
if (IsAppResourcePath(virtualPath))
{
return new AssemblyResourceVirtualFile(virtualPath);
}
else
return base.GetFile(virtualPath);
现在我们将 AssemblyResourceVirtualFile
类添加到 VirtualFile
的子类中,并重写 Open()
方法以读取资源流。
public override Stream Open() {
string[] parts = path.Split('/');
string assemblyName = parts[2];
string resourceName = parts[3];
assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName);
if (!File.Exists(assemblyName))
{
if (ConfigurationManager.AppSettings["EmptyViewVirtualPath"] != null)
{
string emptyViewPath = HttpContext.Current.Server.MapPath(
ConfigurationManager.AppSettings["EmptyViewVirtualPath"]);
return new FileStream(emptyViewPath, FileMode.Open);
}
}
byte[] assemblyBytes = File.ReadAllBytes(assemblyName);
Assembly assembly = Assembly.Load(assemblyBytes);
if (assembly != null)
{
return assembly.GetManifestResourceStream(resourceName);
}
return null;
}
有趣的是,如果程序集资源不存在,会返回什么。我们之前说过,将使用空视图而不是返回 null。怎么做?
if (!File.Exists(assemblyName))
{
if (ConfigurationManager.AppSettings["EmptyViewVirtualPath"] != null)
{
string emptyViewPath = HttpContext.Current.Server.MapPath(
ConfigurationManager.AppSettings["EmptyViewVirtualPath"]);
return new FileStream(emptyViewPath, FileMode.Open);
}
}
切换视图
到目前为止,有两个视图:文件列表和搜索结果。在搜索框中,“返回”键或“搜索”按钮将显示搜索结果。
然后使用“清除”按钮,或者通过在文件夹导航树中选择另一个文件夹,或者在位置组合框中输入另一个位置来更改文件夹,以切换到正常的文件列表。
if ($("#filelist").length == 0) {
showListView(folder);
}
else {
var grid = $("#filelist").data('tGrid');
grid.rebind({ filePath: folder });
}
loadActionLinks(folder);
}
function showListView(path) {
$.ajax({
type: "POST",
url: getApplicationPath() + "Explorer/FileList",
data: { folderPath: path },
cache: false,
dataType: "html",
success: function (data) {
$('#searchlink').show();
$('#clearlink').hide();
$('#listpanel').html(data);
$('#searchinput').val("");
$('#searchinput').blur();
$('#searchinput').focus();
},
error: function (req, status, error) {
alert("switch to normal view failed!");
}
});
}
下面是控制器方法
public ActionResult FileList(string folderPath)
{
folderPath = FileModel.Encode(folderPath);
DirectoryInfo di = new DirectoryInfo(folderPath);
return PartialView(new FileModel(di));
}
结论
本文介绍了如何在 ASP.NET MVC 中实现插件架构。示例应用程序的目的是尽可能精简,以帮助突出插件与 ASP.NET MVC 宿主应用程序的集成。对于生产环境,可以进行许多改进,使其成为一个更有用的解决方案。