65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET AJAX 驱动的源代码浏览器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (12投票s)

2009年6月11日

CPOL

8分钟阅读

viewsIcon

45626

ASP.NET AJAX 驱动的源代码浏览器

这个项目始于我开始寻找一种快速的方式将源代码放在我的网站上,以便访客能够轻松地浏览项目。时间非常宝贵,所以我并不总是喜欢下载一个 zip 压缩包然后解压来查看它——我更希望有一个在线的空间。

(注意:在自己安装此应用程序时,您应该将其发布为 Web 应用程序,然后更新 web.config 文件,将 "root" appSetting 指向您的源代码的根目录。Web 应用程序应该具有读取指定文件夹的权限。)

<appSettings><add key="root" value="c:\dev\"/></appSettings>

那时我意识到这是一个完美的迷你项目。它将展示一个稳健架构的灵活性,提供了大量的重构机会,并解决了我在显示源代码方面的问题,同时展示了一些基本的编程范例!

我的最初要求是,我可以在几个小时内完成 "第一版" 的开发。我本身就花了很多时间在编码上,不希望一个失控的项目会占用我主要目标的任何时间和精力。所以,需求相当简单:

  • 显示一个树形视图,可以向下浏览到各种节点、目录或源文件
  • 以某种方式显示源代码,并进行语法高亮
  • 不要显示所有内容,只显示相关内容

不算差,那么从哪里开始呢?

首先,我想创建一个灵活的模型。我第一版迭代的想法是解析文件系统并显示节点,但考虑到将来,我可能最终需要链接到第三方存储库,如 SVN。因此,我决定对我的模型进行接口化。

Code Browser Model Interfaces

我有了结构中最通用的部分,即 IFileSystemNode,它基本上包含一个名称和一个路径。这可以进一步解析为 IDirectory,它包含其他 IFileSystemNode 的集合,或者一个由扩展名(也许我应该将其更改为类型)定义的 IFile,并且可能包含要渲染的内容。

下一步是实现这些模型。我为节点创建了一个基类 abstract,然后创建了一个 DirectoryModel 和一个 FileModel

Code Browser File and Directory Models

关键在于,我可以有一个保存 Uri 并将路径公开为 URL 等实现的。遵循敏捷原则,我不会想得太远,暂时坚持我目前所拥有的。

接下来是数据访问层。第一步是一个通用的 IDataAccess,它简单地允许 "Load" 一个实体。

namespace Interface.DataAccess
{
    /// <summary>
    ///     Data access method
    /// </summary>
    /// <typeparam name="T">The type of data access</typeparam>
    public interface IDataAccess<T> where T: IFileSystemNode 
    {
        /// <summary>
        ///     Loads the selected node
        /// </summary>
        /// <param name="path">The path to the node</param>
        /// <returns>The node</returns>
        T Load(string path);
    }
}

由于我不需要在请求时获取文件内容,因此我创建了一个 IFileDataAccess,它专门接收一个文件对象的引用,然后加载内容。

实现相对简单。DirectoryDataAccess 递归地遍历结构,为每个目录构建一个集合,或者调用文件数据访问来加载文件内容。

public DirectoryModel Load(string path)
{
    if (string.IsNullOrEmpty(path))
    {
        throw new ArgumentNullException("path");
    }

    DirectoryModel retVal = ModelFactory.GetInstance<directorymodel>();

    if (Directory.Exists(path))
    {
        retVal.Path = path; 
        DirectoryInfo dir = new DirectoryInfo(path);
        retVal.Name = dir.Name;
        
        // recursively add subdirectories
        foreach(DirectoryInfo subDir in dir.GetDirectories())
        {
            DirectoryModel subDirectory = Load(subDir.FullName);
            subDirectory.ParentDirectory = retVal; 
            retVal.Contents.Add(subDirectory);
        }

        foreach(FileInfo file in dir.GetFiles())
        {
            FileModel fileModel = _fileAccess.Load(file.FullName);
            fileModel.ParentDirectory = retVal; 
            retVal.Contents.Add(fileModel);
        }
    }

    return retVal; 
}</directorymodel>

这是我给自己的第一个提示…与其递归,不如我轻松地只加载顶级目录项的名称,并设置一个 "isLoaded" 标志。然后,只有当用户深入到较低级别时,我才需要开始在那里进行递归。这将是稍后进行的重构……现在,我将整个东西一次性加载到内存中,但这显然无法扩展到大量项目,也无法通过网络连接到源代码存储库。我们会实现的!

FileDataAccess 提取文件名和扩展名信息。它通过读取文件的所有字节来实现 "LoadContents"。

...
if (File.Exists(file.Path))
{
   file.Content = File.ReadAllBytes(file.Path);
}
...

我喜欢在数据访问之上有一个服务层。该层可以协调多个数据访问(例如,将来当我添加 SVN 实现时,服务层可以实例化一个文件系统和一个 SVN 数据访问处理程序,然后聚合结果传递给表示层)。在此项目中,服务层主要充当数据访问的传递者。但是,它还包含一个我们将在用户界面中显示的有效扩展名列表,因此它根据扩展名是否符合条件来管理对 "LoadContents" 的调用。这可以防止我们尝试加载 DLL 等文件。

现在我们已经把这些都整理好了,让我们转向控件。我可以利用 AJAX 提供的广泛的控件库,但我觉得自己动手创建控件会更有趣。我确实决定使用 JQuery,因为它使客户端操作非常容易。

首先是控件的架构。我想要一个 "树" ,它有分支(目录或文件),然后是一个 "叶子",用于显示代码。这很容易可视化和规划。

我决定将分支作为一个基于 Panel 的服务器控件。然后,分支可以控制子分支的可见性。分支包含一个 IFileSystemNode,并为其节点生成一个锚点标签。然后,根据它是 IDirectory 还是 IFile,它会生成一个点击事件来展开子节点或加载代码内容。我决定让分支递归地加载自身。在此阶段,整个树结构都被输出,然后第二级分支默认折叠。同样,这是第一个迭代:UI 的下一个重构将是延迟加载子节点。目前的状态无法处理大量项目。

您会在分支的 JavaScript 代码中发现一些有趣的代码。例如,我发现最初隐藏 DIV 元素在某种程度上与内部 DOM 状态不同步,迫使用户点击两次才能展开一个节点。为了解决这个问题,我创建了一个附加到节点的自定义属性,并调用了一个冗余的 "hide" 操作来使 DOM 同步。

// for some reason the visibility isn't sorted out, so
// the first time we'll hide it again to get the flags set
// and then the toggle works fine
if (!$(child).attr('init')) {
   $(child).hide();
   $(child).attr('init','true'); 
}

其余的只是一个简单的切换子节点及其同级节点的可见性。整个架构最终看起来是这样的:

Code Browser Control Architecture

分支是一个服务器控件,树是相关分支的容器。分支暴露一个事件,其他客户端控件可以在用户点击节点查看时监听。这就是 "叶子" 的作用。它基本上是一个标题和一个 DIV,并注册监听点击事件。当发生这种情况时,它会进行回调以获取代码内容,然后将其渲染在 DIV 中。

对于语法高亮,我使用了 Alex Gorbatchev 的 Syntax Highlighter。它做得很好,也是我在我的 C#er : IMage 博客 中使用的。一旦 div 被渲染,并在 Leaf 的帮助下(这是表示逻辑,不是服务逻辑)确定了基于文件扩展名的正确画笔,就会调用 Syntax 对象,它会进行高亮显示代码的工作。

我在 Leaf 中 "按预期" 使用了 Callback 函数,即通过调用 GetCallbackEventReference 来生成代码,它会生成适当的 JavaScript。为了防止用户在节点仍在加载时频繁点击,我们设置了一个全局标志,并在当前叶子完全加载之前显示一个错误。最终效果如下:

function leafCallback(path,name) {
        if (window.loading) {
            alert('An existing request is currently loading. 
                   Please wait until the code is loaded before clicking again.');
            return;
        }
        window.loading = true;
        $("#_codeName").html(name); 
        $("#_divCode").html("Loading..."); 
        var arg = path + '|' + name; 
        ;
    }

我们将路径和名称传递下去,以便在服务器上找到正确的节点,然后服务器返回要渲染的字节。回调函数只需注入源代码,然后调用语法高亮器。

function leafCallbackComplete(args, ctx) {
    $("#_divCode").html(args);
    window.loading = false;
    window.scrollTo(0, 0);
    setTimeout('SyntaxHighlighter.highlight()', 1);    
}

我决定构建一个静态缓存,并将整个结构的副本保留在内存中。每小时刷新一次,因此当添加新项目时,它会获取新的源代码。Cache 只是使用 ASP.NET Cache 对象,然后从缓存中拉取树,或者调用服务来递归加载树。显然,当重构以支持延迟加载时,此策略需要更改。

namespace CodeBrowser.Cache
{
    /// <summary>
    ///     Caches a global copy of the tree and refreshes it every hour
    /// </summary>
    public static class TreeCache
    {
        /// <summary>
        ///     Key for storing in the cache
        /// </summary>
        private const string CACHE_KEY = "Master.Node.Key";

        /// <summary>
        ///     The expiration (1 hour)
        /// </summary>
        private static readonly TimeSpan _expiration = new TimeSpan(0,1,0,0);

        /// <summary>
        ///     Gets the master tree node
        /// </summary>
        /// <returns>The root <seealso cref="IDirectory"/></returns>
        public static IDirectory GetMasterNode()
        {
            IDirectory retVal = (IDirectory) HttpContext.Current.Cache.Get(CACHE_KEY);

            if (retVal == null)
            {
                IService<DirectoryModel> service = ServiceFactory.GetService<DirectoryModel>();
                retVal = service.Load(ConfigurationManager.AppSettings["root"]);
                HttpContext.Current.Cache.Insert
                  (CACHE_KEY, retVal, null, DateTime.Now.Add(_expiration), TimeSpan.Zero);
            }

            return retVal; 
        }
    }
}

请注意,主 Web 项目仅处理接口和工厂(以及具体的模型),而不处理服务的实际实现(它甚至不知道服务背后的数据访问)。这为我们将来进行扩展提供了很大的灵活性。

我做的唯一其他事情是创建一个基页,该页面注册了项目所需的所有包含文件和 CSS。这使得默认页面整洁干净,因为所有设置都在后台完成。我使用了我 MVC for WebForms 架构的一个松散版本,但没有进行接口和控件/控制器工厂的范围界定。这是另一个可以让我将来重构并进一步抽象的地方。目前,控制器只是注入控件并使用缓存来拉取文件系统结构。

您可以通过点击这里查看浏览器的一个实时副本(并使用它来浏览代码)。要下载 zip 文件中的源代码,请点击这里。我希望您喜欢并从这个项目中有所收获,我期待在未来的帖子中进行一些重构并扩展它。

Jeremy Likness

© . All rights reserved.