Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET MVC Server Explorer: Part 3 - Plug-in Architecture

0.00/5 (No votes)
3 May 2011 1  
How to use the plug-in architecture in ASP.NET MVC.

Introduction

This article demonstrate how to use the plug-in architecture in ASP.NET MVC. We will create a Search-Box plug-in and see how the host (server explorer) calls it.

Plug-in Architecture

The plug-in architecture is a software architecture that creates an object instance of an interface at run time. The plug-in architecture extends the behaviour of an existing class so that it can be used for a more specific purpose. It differs from using class inheritance, where behaviour is altered or overwritten, or configuration, where behaviour modification is limited to the capabilities of the defined configuration options.

With the plug-in architecture, the modified behaviour (the plug-in) connects to an abstract partial class, which in turn connects to the core class. The plug-in uses this interface to implement methods called by the core class and can also call new methods in the core class.

All plug-in models require two basic entities—the plug-in host and the plug-in itself. The plug-in host’s code is structured such that certain well-defined areas of functionality can be provided by an external module of code—a plug-in. Plug-ins are written and compiled entirely separately from the host, typically by another developer. When the host code is executed, it uses whatever mechanism is provided by the plug-in architecture to locate compatible plug-ins and load them, thus adding capabilities to the host that were not previously available. The plug-in architecture is loosely coupled. It gives you great flexibility.

Using the Plug-in Architecture in ASP.NET MVC

You can read these excellent articles from Veeb, wynia and fzysqr. They explain how to make ASP.NET MVC work on a plug-in framework. The plug-in assembly should have its own Model, View, and Controller, and store a plug-in’s views within the plug-in assembly as an embedded resource. This way, plug-ins can be just a DLL that can be added to the host’s bin directory.

In the plug-in host, we use the VirtualPathProvider class to intercept the calls MVC makes to retrieve files from the file system. Then we need the subclass VirtualFile and override Open() to read the requested resource out of the plug-in assembly.

Create Search-Box Plug-in

Create a new ASP.NET MVC empty project in the ServerExplorer solution and name it “SeachBox Plugin”. Because the SearchBox plug-in project needs to share the FileModel class, create a new DataModel project and move the FileModel class to the DataModel. Add the DataModel reference in both the SearchBox project and the ServerExplorer project.

Add a Search Controller in the Controller folder. Then create a SearchBox template view.

SearchBox view template

That search box is composed of a label, a text input, and image links. It’s typical Windows 7 style. When the input gets focus, the label text “Search” will disappear.

This is done by a script:

$(function () {

    $('#searchbox input')
           .focus(function () {
               $('#searchbox label').hide();
           })
            .blur(function () {
                if (this.value == "")
                    $('#searchbox label').show();
            });
});

After doing a search, a clear link will replace the search link.

Click the clear link, and the Search Box will be cleared.

searchbox.ascx looks like:

<%@ 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>
Search result view template

A recursive file search will be done under the current folder. All files which match the search criteria will be fetched. The normal file list view for the top level sub-files and the sub-directories are not enough. We need to get a new type of view to display the result.

Use the Telerik grid to implement this view. Shown below is the code of the View 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();
%>

Use the Telerik grid to implement this view. Shown below is the code of the View 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
        });
    }
}

File recursive search

The easiest way to search files and folders under a directory is using the .NET built-in functions.

string[] dirs = Directory.GetDirectories(path, "*.*", SearchOption.AllDirectories);
string[] files = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);

But there is a known issue: if there is a sub-folder that is not accessible, the GetDirectories and GetFiles functions will fail. To overcome this side-effect, we need to implement our own recursive search with SearchOption.TopDirectoryOnly rather than SearchOptions.AllDirectories. Listed below is the code.

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)
    {
    }
}

Make SearchBox pluggable

Change all the View files’ build action to Embedded Resource. Thus we can get the actual View from the assembly resource. In the host application, any lookup for resources prefixed with “~/Plugins/” are assumed to be located in a DLL. The format of the path is assumed to be: ~/Plugins/{DLL file name}/{Resource Name inside 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"]);
    }
}

Calling the Search-Box plug-in

The host project (ServerExplorer) doesn’t have to reference the plug-in assembly. At runtime, the host application will check if the plug-in DLL exists in the bin folder. If the plug-in exists, the plug-in View will be loaded, otherwise an empty View is loaded.

If there is no SearchBoxPlugin.dll in the bin folder, no search box gets loaded.

If SearchBoxPlugin.dll exists in the bin folder, a search box gets loaded.

Using the plug-in View

In index.aspx, we add the line below:

<% Html.RenderPartial("~/Plugins/SearchBoxPlugin.dll/
          SearchBoxPlugin.Views.Search.SearchBox.ascx","");%>

The naming convention is important. The path which starts with “~/Plugins” is the plug-in resource path. The next part is the file name of the plug-in assembly. Then follows the embedded resource name.

The VirtualPathProvider class provides developers a way to intercept the calls MVC makes to retrieve files from the file system and provide these files however we see fit (in our case, directly from an assembly). The AssemblyResourceProvider subclasses the VirtualPathProvider class to embed plug-in controllers and views into separate assemblies and then load them on demand.

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;
}

In the FileExists() method, if the path is a plug-in resource path, we’ll check if the assembly and the assembly resource exist rather than the base FileExists method.

First, we get the full path of the assembly and check that the file exists.

assemblyName = Path.Combine(HttpRuntime.BinDirectory, assemblyName);

if (!File.Exists(assemblyName))
    return false;

We get the assembly using Reflection.

byte[] assemblyBytes = File.ReadAllBytes(assemblyName);
Assembly assembly = Assembly.Load(assemblyBytes);

Then we check that the resource exists.

string[] resourceList = assembly.GetManifestResourceNames();
bool found = Array.Exists(resourceList, r=> r.Equals(resourceName));

If the plug-in resource does not exist, we use an empty View.

if (IsAppResourceExisted(virtualPath))
    return true;
else
{
    if (ConfigurationManager.AppSettings["EmptyViewVirtualPath"] != null)
        return base.FileExists(
          ConfigurationManager.AppSettings["EmptyViewVirtualPath"]);
    return false;
}

In web.config, we add:

<appSettings>
    <add key="EmptyViewVirtualPath" value="~/Views/Shared/Empty.ascx" />
</appSettings>

In the GetFile() method, we get the resource rather than the physical file.

if (IsAppResourcePath(virtualPath))
{
    return new AssemblyResourceVirtualFile(virtualPath);
}
else
    return base.GetFile(virtualPath);

Now we add the AssemblyResourceVirtualFile class to subclass VirtualFile and override Open() to read the stream of resource.

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;
}

The interesting part is what is returned if the assembly resource does not exist. We said before that an empty view will be used rather than returning null. How?

if (!File.Exists(assemblyName))
{
    if (ConfigurationManager.AppSettings["EmptyViewVirtualPath"] != null)
    {
        string emptyViewPath = HttpContext.Current.Server.MapPath(
           ConfigurationManager.AppSettings["EmptyViewVirtualPath"]);
        return new FileStream(emptyViewPath, FileMode.Open);
    }
}

Switch Views

So far there are two Views: the file list, and search results. In the search box, “return” key or “search” button will show the search results.

Then use the “Clear” button or change the folder by selecting a different folder on the folder navigation tree or by typing a different location in the location combobox to switch to the normal file list.

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!");
        }
    });
}

Shown below is the controller method:

public ActionResult FileList(string folderPath)
{
    folderPath = FileModel.Encode(folderPath);
    DirectoryInfo di = new DirectoryInfo(folderPath);
    return PartialView(new FileModel(di));
}

Conclusion

This article describes how to implement a plug-in architecture in ASP.NET MVC. The example application is intended to be as thin as possible to help accentuate the integration of plug-ins from the ASP.NET MVC host application. For a production environment, many improvements can be made to make a more useful solution.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here