[转载]服务器端执行JavaScript代码

mikel阅读(1121)

[转载]服务器端执行JavaScript代码 – 老赵点滴 – 追求编程之美.

话说,如今不在客户端使用JavaScript代码才是稀奇事儿。由于Web应用的体验越来越丰富,客户端用JavaScript 实现的逻辑也越来越多,这造成的结果就是某些几乎一致的逻辑需要在客户端和服务器端各实现一遍。这违反了DRY原则,不容易维护。幸运的是,我们可以在服 务器端执行JavaScript代码,谁让JavaScript傍上了这无比霸道的浏览器平台呢?

例如,如今在客户端使用JavaScript进行验证已经是个标准,它可以有效避免用户在正常情况下提交错误的数据,增强用户体验。当然,服务器端 的验证是必不可少的,因为这才是“安全性”的体现。有些解决方案,会在服务器端提供有限的验证种类,然后在客户端生成JavaScript代码,并辅以服 务器端的验证框架。这种做法可以追溯到ASP.NET 1.x上的Validator控件,但这显然会有扩展性,灵活性上的限制,因此我更倾向于在服务器端执行JavaScript代码。

例如,要检查用户名是否合法,我们可能会写这样的JavaScript代码:

var checkName = function (name) { return /^\w{3,10}$/.test(name); }

这在客户端验证自然没有任何问题,服务器端就要借助一些JavaScript执行引擎了。在.NET平台上有例如比较新的IronJS项目,这是个基于DLR的JavaScript执行引擎,十分重视性能,从作者博客上的评测结果来看,甚至领先于以速度见长的V8。可惜的是,IronJS还没有完整实现ECMAScript 3.0,还缺少一些重要功能,例如正则表达式。

Jint是一个.NET平台上较早的JavaScript执行引擎,因此与DLR关系不大,因此可能不太容易与IronPython,IronRuby等语言进行互操作。用它来执行一些简单的JavaScript脚本不成问题,例如上面的代码:

var jint = new Jint.JintEngine();
jint.Run(@"var checkName = function(name) { return /^\w{3,10}$/.test(name); }");

Console.WriteLine(jint.CallFunction("checkName", "jeffz")); // True
Console.WriteLine(jint.CallFunction("checkName", "hello world")); // False

只可惜,在实际使用中,Jint不支持多线程的环境,即我们无法在多个线程下同时调用jint的CallFunction方法,但是如果每次都重新 Run一遍JavaScript代码,也会带来较多的性能开销。其实要解决这个问题也并不困难,构造一个对象池即可,.NET 4中提供了并行容器(如ConcurrentStack,ConcurrentQueue),实现一个简单的对象池可谓不费吹灰之力。

这方面Jurassic的表现要好的多,这是一个构建于.NET 4.0的JavaScript执行引擎:

var engine = new Jurassic.ScriptEngine();
engine.Evaluate(@"var checkName = function(name) { return /^\w{3,10}$/.test(name); }");

Console.WriteLine(engine.CallGlobalFunction<bool>("checkName", "jeffz"));
Console.WriteLine(engine.CallGlobalFunction<bool>("checkName", "hello world"));

此外,从Benchmark上来看,Jurassic性能也比Jint有所提高,但还是远远落后于V8,甚至IE 8里的JavaScript引擎。而且,它还提供了一个基于Silverlight控制台,您可以在浏览器里把玩一番。

令人感到意外的是,Jint和Jurassic作为JavaScript执行引擎都有一些严重的问题,那便是不能正确运行showdown.js(JavaScript 实现的Markdown转化器)——虽然我并没有发现showdown.js中有过于复杂的内容,基本就是些字符串操作吧。原本我还想把它们用在mono 中,既然如此也就不做进一步尝试了。不过,经过简单的实验,Jurassic似乎使用了mono 2.8中尚不支持的接口,但也有可能只是Jurassic控制台中的问题。

有趣的是,.NET平台下最靠谱的JavaScript执行引擎居然是Rhino JavaScript,最近一次发布是在2009年3月,不过实现的十分完整。要说缺点,可能就是使用起来比较麻烦,还有,这是个Java项目。

嗯,我没有开玩笑,我们完全可以在.NET平台下使用Rhino JavaScript:

var cx = Context.enter();
try
{
    var scope = cx.initStandardObjects();
    cx.evaluateString(scope, @"var checkName = function(name) { return /^\w{3,10}$/.test(name); }", "checkName.js", 1, null);
    var func = (Function)scope.get("checkName", scope);

    Console.WriteLine(Context.toString(func.call(cx, scope, scope, "jeffz")));
    Console.WriteLine(Context.toString(func.call(cx, scope, scope, "hello world"));
}
finally 
{
    Context.exit();
}

因为我们有IKVM.NET。mono等.NET开源社区上有大量宝藏, 就看您能利用多少了。我用ikvmc把js.jar转化为RhinoJs.dll之后就可以直接使用,效果很好,对调试也有很好的支持(如果 JavaScript执行时出现了错误,则VS会直接带您至出错的那行)。性能也是比较令人满意的,在我的Mac OSX上安装的Ubuntu Server 10.10虚拟机,单线程转化并过滤博客上最近的3800条评论,大约耗时20秒。试验时Host上还开着一个Windows 7虚拟机,还有大量浏览器等应用程序,并不十分空闲。

您可能知道,我的博客目前是基于mono 2.6的,其中比较有特色的地方便是评论功能了,我使用Markdown标记,并提供了实时的预览功能,这自然需要在客户端解释Markdown标记,并 进行过滤。目前,我还在服务器使用了C#实现的Markdown转化器及过滤逻辑,但在某些特殊情况下结果会有所不同,且需要维护两套代码。不久以后,我 会将把博客升级为ASP.NET 4.0及mono 2.8(C# 4.0的dynamic特性在某些情况下的确比较方便),并且在服务器端使用IKVM.NET + Rhino JavaScript执行相同转化代码。从效果上来看还是十分令人满意的。

值得一提的是,其实在.NET平台上还有一个基于DLR的JavaScript执行引擎,是为RemObjects Script for .NET,据称也支持mono。只可惜它并不是开源产品(不过公开了源代码),且授权协议要求我们最多在5台机器上安装代码,且只供我们自己使用,于是我就没有对它有关注太多了。

[转载]通过源代码研究ASP.NET MVC中的Controller和View(二)

mikel阅读(1183)

[转载]通过源代码研究ASP.NET MVC中的Controller和View(二) – Ivony… – 博客园.

通过源代码研究ASP.NET MVC中的Controller和View(一)

在开始之前,先来温习下上一篇文章中的结论(推论):

  • IView是所有HTML视图的抽象
  • ActionResult是Controller对View的控制的抽象,也是View的入口。
  • ViewResult用于指明呈现某个HTML视图(IView实例)。
  • ViewResult是IView实例的使用者,查找、呈现和释放IView实例。
  • IViewEngine是IView的管理者,被ViewResult调用,用于查找和释放IView实例。

三个类型已经可以得出初步的结论:

ViewResult

  • 创建者:Controller
  • 职责:呈现HTML视图
  • 公开接口:ExecuteResult( ControllerContext ) : void

IViewEngine

  • 创建者:全局
  • 职责:查找和管理IView(视图)对象
  • 公 开接口:FindView( controllerContext : ControllerContext, viewName : string, masterName : string, useCache : bool ) : ViewEngineResult,ReleaseView( IView ) : void

IView

  • 创建者:IViewEngine
  • 职责:呈现HTML
  • 公开接口:Render( TextWriter ) : void

在呈现视图这个过程中,ViewResult是主控,IView是操作执行,IViewEngine是查找映射。如果类比到其他的架构(如ASP.NET),则可以建立这样的类比:

ViewResult 类比为 HttpApplication,是呈现视图的主控对象。

IView 类比为 IHttpHandler,是呈现视图的操作执行。

IViewEngine 类比为 IHttpHandlerFactory,负责视图的查找映射。

PS:大家可以比较一下类比类型的公开接口是何其相似。

同时我发现到,ViewResult调用IViewEngine.FindView接口参数里,除了ControllerContext,没有开放类型(非sealed类型)。这意味着这个接口很难存在“私有协议”(自创名称)这种手法。

简 单的说“私有协议”这种手法就是指假设有两个类型A和B(A和B都是开放类型),A是B的某个接口的使用者,同时这个接口中存在一个开放类型C(非 sealed类型)。那么我们就可以写出两个类型A’和B’,同时写一个私有的(internal)类型C’。由于C’是私有的,非A’和B’便不能访问 其扩展成员,此时在A’和B’之间就建立了一个“私有协议”。

显然举例说明更好,由于IView类型是一个开放类型(非sealed),我们可以建立一个IViewEngine到ViewResult之间的私有协议,如下:

  public class MyViewResult : ViewResult
  {
    protected override ViewEngineResult FindView( ControllerContext context )
    {
      var result = base.FindView( context );
 
      var myView = result.View as PrivateView;
 
      if ( myView != null )
        myView.ControllerContext = context;
 
      return result;
    }
 
  }
 
 
  public class MyViewEngine : IViewEngine
  {
 
    //...
 
    public ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache )
    {
      return new ViewEngineResult( new PrivateView(), this );
    }
 
    //...
  }
 
 
  internal class PrivateView : IView
  {
 
    public ControllerContext ControllerContext
    {
      get;
      internal set;
    }
 
    #region IView 成员
 
    public void Render( ViewContext viewContext, System.IO.TextWriter writer )
    {
      throw new NotImplementedException();
    }
 
    #endregion
  }

注意这里的PrivateView 类型,这是一个internal的类型,只有我们的MyViewResultMyViewEngine才 能使用和发现。在使用中我们利用私有协议传输了更多的信息:ControllerContext,而这些信息对于这个程序集之外的代码是不可见的(因为携 带的类型根本就不可见),这就形成了私有协议。这个例子仅为说明这种手法,并无实际意义(因为事实上IViewEngine在创建IView对象的时候就 可以把ControllerContext传进去了,而无需这么复杂)。

在IViewEngine.FindView的参数里,只有ControllerContext是开放类型,私有协议只能在这之上建立。但从设计上来说,在Context上建立私有协议并不是一个好的idea。

或者说这种接口的设计透露出来设计者的思想:我不太愿意ViewResult和IViewEngine之间存在强耦合关系。

最后来看看ViewEngineResult的设计:

  public class ViewEngineResult
  {
 
    public ViewEngineResult( IEnumerable<string> searchedLocations )
    {
      if ( searchedLocations == null )
      {
        throw new ArgumentNullException( "searchedLocations" );
      }
 
      SearchedLocations = searchedLocations;
    }
 
    public ViewEngineResult( IView view, IViewEngine viewEngine )
    {
      if ( view == null )
      {
        throw new ArgumentNullException( "view" );
      }
      if ( viewEngine == null )
      {
        throw new ArgumentNullException( "viewEngine" );
      }
 
      View = view;
      ViewEngine = viewEngine;
    }
 
    public IEnumerable<string> SearchedLocations
    {
      get;
      private set;
    }
 
    public IView View
    {
      get;
      private set;
    }
 
    public IViewEngine ViewEngine
    {
      get;
      private set;
    }
  }

这个类型除了属性啥都没有,难道就是传说中的贫血对象?

顺带提一句我对某些胡乱发明(至少我认为是)的诸如“贫血对象”这样的词汇相当不满。尽管这里的设计明显有一些坏味道。

OK,那么很显然的这个对象只是为了IViewEngine的FindView方法不至于多一个out IEnumerable<string> searchedLocations而已,这种设计存在的意义更多的在于改善编码体验,同时,避免在公开接口中定义out参数。

结合ViewResult.FindView和ViewEngineCollection.FindView的实现,我能够搞清楚这个类型对我们来说唯一有价值的东西就是View属性,如果这个属性为null,则表示没有找到视图。

那么对于架构设计的研究就到此为止了。接下来研究下ASP.NET MVC的默认视图模型实现。

由于IViewEngine是IView对象的管理者,所以先用Reflector查找一下IViewEngine的实现类有哪些:

image

OK,只有一个类型,看来问题变得很简单了。

FindView是IViewEngine的主要方法,这个方法在VirtualPathProviderViewEngine里面实现:

    public virtual ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache )
    {
      if ( controllerContext == null )
      {
        throw new ArgumentNullException( "controllerContext" );
      }
      if ( String.IsNullOrEmpty( viewName ) )
      {
        throw new ArgumentException( MvcResources.Common_NullOrEmpty, "viewName" );
      }
 
      string[] viewLocationsSearched;
      string[] masterLocationsSearched;
 
      string controllerName = controllerContext.RouteData.GetRequiredString( "controller" );
      string viewPath = GetPath( controllerContext, ViewLocationFormats, AreaViewLocationFormats, "ViewLocationFormats", viewName, controllerName, _cacheKeyPrefix_View, useCache, out viewLocationsSearched );
      string masterPath = GetPath( controllerContext, MasterLocationFormats, AreaMasterLocationFormats, "MasterLocationFormats", masterName, controllerName, _cacheKeyPrefix_Master, useCache, out masterLocationsSearched );
 
      if ( String.IsNullOrEmpty( viewPath ) || (String.IsNullOrEmpty( masterPath ) && !String.IsNullOrEmpty( masterName )) )
      {
        return new ViewEngineResult( viewLocationsSearched.Union( masterLocationsSearched ) );
      }
 
      return new ViewEngineResult( CreateView( controllerContext, viewPath, masterPath ), this );
    }

逻辑很简单,首先是得到controllerName,然后调用GetPath方法,这个方法的参数数量,呃,,,那个,,,,(private的方法也不能写成这样啊!怒!)

GetPath方法会返回一个字符串,看起来这个字符串应该是path,然后是一个if,他的逻辑是判断GetPath返回的结果是不是空。如果是空,则返回一个没有View的ViewEngineResult,否则调用CreateView方法来创建视图返回。

简单说:

  • 获取路径
  • 路径为空则返回没有视图的ViewEngineResult
  • 否则创建视图返回

那么我发现CreateView方法是一个抽象的方法,这意味着我大体上能推测出VirtualPathProviderViewEngine类型的主要职责便是将ViewResult通过FindView方法传递来的信息转换成路径信息交由派生类创建视图对象:

viewName -> viewPath

masterName -> masterPath

这就是主要工作,这个工作是由GetPath方法完成的:

    private string GetPath( ControllerContext controllerContext, string[] locations, string[] areaLocations, string locationsPropertyName, string name, string controllerName, string cacheKeyPrefix, bool useCache, out string[] searchedLocations )
    {
      searchedLocations = _emptyLocations;
 
      if ( String.IsNullOrEmpty( name ) )
      {
        return String.Empty;
      }
 
      string areaName = AreaHelpers.GetAreaName( controllerContext.RouteData );
      bool usingAreas = !String.IsNullOrEmpty( areaName );
      List<ViewLocation> viewLocations = GetViewLocations( locations, (usingAreas) ? areaLocations : null );
 
      if ( viewLocations.Count == 0 )
      {
        throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture,
            MvcResources.Common_PropertyCannotBeNullOrEmpty, locationsPropertyName ) );
      }
 
      bool nameRepresentsPath = IsSpecificPath( name );
      string cacheKey = CreateCacheKey( cacheKeyPrefix, name, (nameRepresentsPath) ? String.Empty : controllerName, areaName );
 
      if ( useCache )
      {
        return ViewLocationCache.GetViewLocation( controllerContext.HttpContext, cacheKey );
      }
 
      return (nameRepresentsPath) ?
          GetPathFromSpecificName( controllerContext, name, cacheKey, ref searchedLocations ) :
          GetPathFromGeneralName( controllerContext, viewLocations, name, controllerName, areaName, cacheKey, ref searchedLocations );
    }

我的神,这个方法有够复杂,我们慢慢看。

首先是入口检查和out变量初始化,尽管那个_emptyLocations明白无误的告诉了我们他是一个空的数组,我还是去瞄了一眼:

    private static readonly string[] _emptyLocations = new string[0];

然后获取了areaName,useAreas标识是不是使用了区域,然后获取了viewLocations,大体上这些变量从名称就能推测其意义。

如果viewLocations没有,那么抛了个异常,这个异常大体上的意思是属性不能为空,属性名则是locationsPropertyName,这是个参数,对应到调用的地方的那个字符串:“ViewLocationFormats”

显然这个字符串和GetViewLocations方法存在某种微妙的联系,这个联系并不难找,我们发现调用的地方是这样的:

string viewPath = GetPath( controllerContext, ViewLocationFormats, AreaViewLocationFormats, ViewLocationFormats, viewName, controllerName, _cacheKeyPrefix_View, useCache, out viewLocationsSearched );
string masterPath = GetPath( controllerContext, MasterLocationFormats, AreaMasterLocationFormats, MasterLocationFormats, masterName, controllerName, _cacheKeyPrefix_Master, useCache, out masterLocationsSearched );

这个字符串的值和第二个参数是一样的,第二个参数就是locations,看看locations用来干啥了?唔,,,GetViewLocations的参数,所以,,,,

好吧,这个设计有点坏味道了,,,,

继续我们的探索,回头我会帮大家把这些方法全掰碎了看清楚。

然后有一个判断,IsSpecificPath,下面的cacheKey和if ( useCache )是缓存用的,暂且不管。然后我们看到下面是return了。

先来摸清楚那个判断干啥的,nameRepresentsPath的意思应该是:“名称代表路径”,IsSpecificPath的意思是:“是特定的路径”,传入的参数是name,结合起来分析。

IsSpecificPath方法的参数应该是一个path,那么这意味着name和path在某些时候是一个东西(nameRepresentsPath)。来看看IsSpecificPath的实现:

    private static bool IsSpecificPath( string name )
    {
      char c = name[0];
      return (c == '~' || c == '/');
    }

极为简洁。在这里顺带稍微提一下,写成return name.StartsWith( “~” ) || name.StartsWith( “/” );语义岂不更为明确。
这个方法大体上来说可以描述成,判断name是不是一个绝对路径。结合参数名,我大体上可以这样猜测,如果name是以”/”或者”~”开 头,则VirtualPathProviderViewEngine则会当作路径来看待,否则当作名称来看待。向上追溯,我们就能发现这个name其实就 是viewName或matserName。

现在我们大体上理清了VirtualPathProviderViewEngine.GetPath的逻辑:

  1. 获取区域名(areaName)和视图位置(viewLocations)
  2. 检查名称是不是一个绝对路径
  3. 如果useCache为true,尝试从缓存中获取路径
  4. 否则,根据名称是不是一个绝对路径调用相应的方法获取路径。

修剪和归纳一下大体是这样:

  1. 获取视图位置
  2. 缓存检索
  3. 获取路径

接下来探索GetPathFromSpecificName和GetPathFromGeneralName两个方法,直觉告诉我GetPathFromSpecificName应该会比较简单:

    private string GetPathFromSpecificName( ControllerContext controllerContext, string name, string cacheKey, ref string[] searchedLocations )
    {
      string result = name;
 
      if ( !FileExists( controllerContext, name ) )
      {
        result = String.Empty;
        searchedLocations = new[] { name };
      }
 
      ViewLocationCache.InsertViewLocation( controllerContext.HttpContext, cacheKey, result );
      return result;
    }

的确很简单,简单的判断了一下文件是否存在(FileExists),然后就是插入缓存和返回结果了,而这个结果(result),就是name。哈,还真是nameRepresentsPath。

那么看看GetPathFromGeneralName:

    private string GetPathFromGeneralName( ControllerContext controllerContext, List<ViewLocation> locations, string name, string controllerName, string areaName, string cacheKey, ref string[] searchedLocations )
    {
      string result = String.Empty;
      searchedLocations = new string[locations.Count];
 
      for ( int i = 0; i < locations.Count; i++ )
      {
        ViewLocation location = locations[i];
        string virtualPath = location.Format( name, controllerName, areaName );
 
        if ( FileExists( controllerContext, virtualPath ) )
        {
          searchedLocations = _emptyLocations;
          result = virtualPath;
          ViewLocationCache.InsertViewLocation( controllerContext.HttpContext, cacheKey, result );
          break;
        }
 
        searchedLocations[i] = virtualPath;
      }
 
      return result;
    }
  • 循环获取locations(就是viewLocations)里面的所有项,进行了一个Format的操作(注意这里调用的是ViewLocation.Format方法)得到virtualPath(虚拟路径)。
  • 如果某个虚拟路径的文件是存在的(FileExists),则返回这个虚拟路径(同时会清空searchedLocations)。
  • 否则,会返回一个空字符串(最上面初始化的结果)。

在循环过程中,如果虚拟路径不存在,那么他会被添加到searchedLocations(查找过的位置)。唔,,,,这里又有坏味道了?

简单解释下,因为如果在循环过程中找到了任何一个正确的结果,searchedLocations就会被置为空数组,所以最终searchedLocations只可能有两种结果:空,或者所有循环过的virtualPath。

OK,现在获取路径的逻辑已经基本搞清,唯一不明白的是ViewLocation.Format方法的实现,而ViewLocation这个对象则是从这里来的:

      List<ViewLocation> viewLocations = GetViewLocations( locations, (usingAreas) ? areaLocations : null );

这个viewLocations后来成为了GetPathFromGeneralName的locations参数,如果注意观察的话,事实上这个东西仅用在了GetPathFromGeneralName方法:

image

先来看看ViewLocation这个类型:

    private class ViewLocation
    {
 
      protected string _virtualPathFormatString;
 
      public ViewLocation( string virtualPathFormatString )
      {
        _virtualPathFormatString = virtualPathFormatString;
      }
 
      public virtual string Format( string viewName, string controllerName, string areaName )
      {
        return String.Format( CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName );
      }
 
    }

呃,简单的说,这个类型就是对string的一个包装,提供一个特定的Format方法。。。。

不过,好像areaName参数没有被用到,,,,

还好在这个类型定义的下面我很快发现了另一个东西:

    private class AreaAwareViewLocation : ViewLocation
    {
 
      public AreaAwareViewLocation( string virtualPathFormatString )
        : base( virtualPathFormatString )
      {
      }
 
      public override string Format( string viewName, string controllerName, string areaName )
      {
        return String.Format( CultureInfo.InvariantCulture, _virtualPathFormatString, viewName, controllerName, areaName );
      }
 
    }

这一对父子都是string的一个包装。他们包装了类似于:”abc{0}shy{1}uin{2}”这样的字符串,然后提供一个Format方法映射到String.Format。

没有更多信息了。去创建这些类型的GetViewLocations方法去看看:

    private static List<ViewLocation> GetViewLocations( string[] viewLocationFormats, string[] areaViewLocationFormats )
    {
      List<ViewLocation> allLocations = new List<ViewLocation>();
 
      if ( areaViewLocationFormats != null )
      {
        foreach ( string areaViewLocationFormat in areaViewLocationFormats )
        {
          allLocations.Add( new AreaAwareViewLocation( areaViewLocationFormat ) );
        }
      }
 
      if ( viewLocationFormats != null )
      {
        foreach ( string viewLocationFormat in viewLocationFormats )
        {
          allLocations.Add( new ViewLocation( viewLocationFormat ) );
        }
      }
 
      return allLocations;
    }

神,这个方法也是如此的简洁,简单说就是把两个string[]类型的参数包装成ViewLocation然后再合并输出了。

好吧,我大体上可以弄明白了,这些GetViewLocations方法和ViewLocation类型全部都是障眼法。真正的逻辑可以简单的描述 成,有若干个字符串模版,然后在GetPathFromGeneralName对这些模版调用了String.Format方法来产生 virtualPath。

结合之前的研究可以得到结论了:

GetPath方法干的事情:

  1. 获取视图位置(GetViewLocations)
    • 检查是否使用了区域(Area)
    • 如果使用了区域,则把areaLocations传入
    • GetViewLocations方法会将locations和areaLocations这两个字符串数组包装和合并成一个ViewLocation的集合
    • 如果集合没有东西,那么抛异常
  2. 缓存检索
  3. 获取路径
    • 如果名称像是一个绝对路径(”/”或”~”开头)
      • 检查虚拟路径所指向的文件是否存在(FileExists)
      • 存在则返回名称(当作路径)。
      • 否则返回空字符串。
    • 如果名称不像是一个绝对路径
      • 遍历所有的视图位置生成虚拟路径
      • 如果虚拟路径所指向的文件存在,则返回这个虚拟路径。
      • 如果所有生成的虚拟路径所指向的文件都不存在,则返回空字符串。

缓存处理部分我并不关心,现在从外部来看GetPath方法,那么它的参数分为三大部分:

  • 缓存部分
    • controllerContext(主要利用里面的HttpContext.Cache模块)
    • cacheKeyPrefix
    • useCache
  • 位置部分:
    • locations和areaLocations,这是虚拟路径的模版,使用的值是VirtualPathProviderViewEngine的公开属性。
    • locationsPropertyName,这个用于抛异常的时候指示使用的哪个Property。
  • 名称部分:
    • name,这个参数会是viewName或者masterName
    • controllerName,这个参数标识了控制器的名称
    • areaName,没有出现在参数中,但利用controllerContext提取了出来,事实上controllerName也是从controllerContext中提取的,性质一样。

那么这里弥漫着一股很浓烈的坏味道了。

GetPath方法的参数真是一团糟。不应当传入ControllerContext而是传入areaName和Cache,locations、areaLocations和locationsPropertyName应该绑成一个对象。换言之,这样岂不更好:

    private string GetPath( Cache cache, string cacheKeyPrefix, bool useCache, LocationsInfo locations, string name, string controllerName, string areaName );

最后用一句话来总结GetPath方法干的破事儿:

利用提供的虚拟路径模版和名称产生虚拟路径,并检查虚拟路径文件是否存在(FileExists),如果存在则返回虚拟路径。

那么,默认虚拟路径模版到底有哪些呢?这些东西是在派生类(WebFormViewEngine)中定义的。

[转载]通过源代码研究ASP.NET MVC中的Controller和View(一)

mikel阅读(1008)

[转载]通过源代码研究ASP.NET MVC中的Controller和View(一) – Ivony… – 博客园.

因为Jumony计划提供ASP.NET MVC的视图模型支持,以取代ASP.NET MVC默认的视图模型(事实上,我觉得很糟糕)。所以,需要先对ASP.NET MVC的视图模型有一个了解。

ASP.NET MVC是一个很年轻的项目,代码并不多,这很好,但麻烦的是文档和资料也不够多,看来要想了解其视图模型只能靠自己。

不 过幸运的是,MVC模型已经决定了其大体的框架,所以我打算直接用Reflector来看看里面的结构(不直接用源代码的原因仅仅是因为 Reflector导航功能实在是太强了,但当我贴代码的时候,贴的是MVC源代码),配合名称和设计思想反析就能够很轻松的了解其架构。

那么这一次研究的对象是.NET Framework 4中的ASP.NET MVC 2。

我先来看一下ASP.NET MVC中的类型,那么我很轻松的发现,这两个类型会是我的切入点:

IView

IController

这两个接口再明白不过的告诉了我它们就是View和Controller的抽象。那么他们俩都只有一个方法,所以职责也很容易就能推导出来:

IView只有一个方法:void Render(ViewContext viewContext, TextWriter writer);

依据视图上下文(ViewContext)呈现HTML。

IController也只有一个方法:void Execute(RequestContext requestContext);

根据请求上下文执行相应的操作。

简单的来说,视图的职责就是呈现(Render),控制器的职责就是执行(Execute),还真简单,哈。

下面我想了解一下视图和控制器是怎么联系到一起的,先来看看MVC的范例网站,我发现Controller里面所有的方法的最后都是这样的:

return View();

而所有的方法的返回值类型都是ActionResult,通过源代码我发现View方法的返回结果是ViewResult,一个ActionResult的子类:

    protected internal ViewResult View()

看来先得弄明白这俩东西是干什么的,从名称我不能发现更多的信息,那么先从抽象的ActionResult来研究。

这个类型只有一个方法:

  public abstract class ActionResult
  {
 
    public abstract void ExecuteResult( ControllerContext context );
 
  }

根据控制器上下文来执行结果,这个方法是抽象的,应该是由具体的类型(如ViewResult)来决定到底要执行什么操作了。

看来这个东西的职责就是执行结果了。

根据上面的观察可以得到推论:

Controller里面的方法都需要返回一个ActionResult(猜测?),这个ActionResult负责下一步的执行操作。不过我有 点奇怪为什么要把操作放在这个ActionResult而不是直接在方法里面执行,那么看看ActionResult有些什么子类型:

image

我不知道您看出来了什么,不过通过子类我已经找到刚才问题的答案了。

ActionResult执行的操作都是与用户交互相关的,例如JavaScriptResult,又或者是FileResult,当然还有我们的 ViewResult。利用ActionResult可以隔离在Controller里面的的业务逻辑和在ActionResult里面的交互逻辑。这是 一个很经典的设计。呃,顺带说说我为什么讨厌《设计模式》,你经常会发现一些很经典的设计手法,然后,你没办法从那个天杀的《设计模式》中找到一个很NB 的名词来描述(我一会儿思考下这是不是那啥模式),所以。。。。

为了证实我的观点,来看看ActionResult.Execute方法的那个参数ControllerContext里面有什么。是的,里面有一 大堆与用户界面相关的东西,例如HttpContext、RequestContext(尽管我现在还不太清楚这个干什么用的,但单从名称看就知道与业务 没关系)、RouteData等。

那么我们可以得到ActionResult的定义,这是一个用户界面交互动作的抽象,用户界面交互包括,呈现一个视图(ViewResult)、或 是执行一段脚本(JavaScriptResult)、或是下载一个文件(FileResult)。如果我们将用户交互全部定义为MVC中的View部分 的话,那么ActionResult就是Controller通向View的入口。也就是MVC结构图中Controller指向View的那个箭头。

或者说,ActionResult的职责就是,改变和呈现View(广义的View,泛指用户界面,非IView实例)。

或者说我现在可以得到结论,ViewResult的职责就是呈现一个HTML视图(因为还有FileResult、JavaScriptResult,所以ViewResult多半只负责呈现一个动态的HTML视图)?

所以ActionResult之前的事情在这一次研究中我就不太关心了,因为ActionResult是视图的控制入口,我现在只关心视图模型。

来看看ViewResult(ViewResultBase)对于ExecuteResult的实现:

    public override void ExecuteResult( ControllerContext context )
    {
      if ( context == null )
      {
        throw new ArgumentNullException( "context" );
      }
      if ( String.IsNullOrEmpty( ViewName ) )
      {
        ViewName = context.RouteData.GetRequiredString( "action" );
      }
 
      ViewEngineResult result = null;
 
      if ( View == null )
      {
        result = FindView( context );
        View = result.View;
      }
 
      TextWriter writer = context.HttpContext.Response.Output;
      ViewContext viewContext = new ViewContext( context, View, ViewData, TempData, writer );
      View.Render( viewContext, writer );
 
      if ( result != null )
      {
        result.ViewEngine.ReleaseView( context, View );
      }
    }

这是ViewResultBase的源代码,前面两个if是例行的入口检查,从第三个if开始干活。首先判断自己的View属性(IView类型) 是不是为空,为空的话执行FindView方法,得到一个result(类型是ViewEngineResult),再把result.View赋给自己 的View属性。我将这些步骤称为查找视图。

然后创建一个writer和一个ViewContext,调用View的Render方法来呈现视图。最后如果result不为空,则调用ViewEngine的ReleaseView方法。简单的说就是:

  • 查找视图(this.View : IView)
  • 呈现视图(IView.Render)
  • 释放试图(IViewEngine.ReleaseView)

查找视图的主要方法是 FindView,这是ViewResultBase唯一的一个抽象方法,由子类ViewResult来实现。那么ViewResultBase和 ViewResult的分工也很明确了。ViewResult封装了查找视图的逻辑(因为只有这么一个抽象方法),呈现视图和其他工作则是由 ViewResultBase来完成。

看看ViewResult.FindView的实现:

    protected override ViewEngineResult FindView( ControllerContext context )
    {
      ViewEngineResult result = ViewEngineCollection.FindView( context, ViewName, MasterName );
      if ( result.View != null )
      {
        return result;
      }
 
      // we need to generate an exception containing all the locations we searched
      StringBuilder locationsText = new StringBuilder();
      foreach ( string location in result.SearchedLocations )
      {
        locationsText.AppendLine();
        locationsText.Append( location );
      }
      throw new InvalidOperationException( String.Format( CultureInfo.CurrentUICulture,
          MvcResources.Common_ViewNotFound, ViewName, locationsText ) );
    }

首先调用了ViewResultBase.ViewEngineCollection.FindView方法。先不管这个方法的实现,我们看到这个方法的返回值result在后面被原封不动的return了。所以,我们得出结论,ViewResult的FindView方法干了什么事:

  1. 调用ViewResultBase.ViewEngineCollection.FindView方法
  2. 如果刚才的结果里面存在一个View(result.View != null),那么返回这个结果。
  3. 否则抛个InvalidOperationException的异常。

那么这里没有查找视图的逻辑,我需要进一步研究ViewResultBase.ViewEngineCollection.FindView方法。 先看看ViewResultBase.ViewEngineCollection这个属性,其值默认就是ViewEngines.Engines:

    public ViewEngineCollection ViewEngineCollection
    {
      get
      {
        return _viewEngineCollection ?? ViewEngines.Engines;
      }
      set
      {
        _viewEngineCollection = value;
      }
    }

换言之ViewResultBase.ViewEngineCollection.FindView等于ViewEngines.Engines.FindView,多按几下F12找到这个方法的实现:

    public virtual ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName )
    {
      if ( controllerContext == null )
      {
        throw new ArgumentNullException( "controllerContext" );
      }
      if ( string.IsNullOrEmpty( viewName ) )
      {
        throw new ArgumentException( MvcResources.Common_NullOrEmpty, "viewName" );
      }
      Func<IViewEngine, ViewEngineResult> cacheLocator = e => e.FindView( controllerContext, viewName, masterName, true );
      Func<IViewEngine, ViewEngineResult> locator = e => e.FindView( controllerContext, viewName, masterName, false );
      return Find( cacheLocator, locator );
    }
    private ViewEngineResult Find( Func<IViewEngine, ViewEngineResult> cacheLocator, Func<IViewEngine, ViewEngineResult> locator )
    {
      ViewEngineResult result;
 
      foreach ( IViewEngine engine in Items )
      {
        if ( engine != null )
        {
          result = cacheLocator( engine );
 
          if ( result.View != null )
          {
            return result;
          }
        }
      }
 
      List<string> searched = new List<string>();
 
      foreach ( IViewEngine engine in Items )
      {
        if ( engine != null )
        {
          result = locator( engine );
 
          if ( result.View != null )
          {
            return result;
          }
 
          searched.AddRange( result.SearchedLocations );
        }
      }
 
      return new ViewEngineResult( searched );
    }

这里的cacheLocator和locator是两个匿名方法,其实就是封装了IViewEngine.FindView的调用,利用闭包把controllerContext、viewName和masterName包了进去。

这个手法很好玩,但实际上如果把Find方法代入展开就是这样:

    public virtual ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName )
    {
      if ( controllerContext == null )
      {
        throw new ArgumentNullException( "controllerContext" );
      }
      if ( string.IsNullOrEmpty( viewName ) )
      {
        throw new ArgumentException( MvcResources.Common_NullOrEmpty, "viewName" );
      }
 
      ViewEngineResult result;
 
      foreach ( IViewEngine engine in Items )
      {
        if ( engine != null )
        {
          result = engine.FindView( controllerContext, viewName, masterName, true );//Func<IViewEngine, ViewEngineResult> cacheLocator = e => e.FindView( controllerContext, viewName, masterName, true );
 
          if ( result.View != null )
          {
            return result;
          }
        }
      }
 
      List<string> searched = new List<string>();
 
      foreach ( IViewEngine engine in Items )
      {
        if ( engine != null )
        {
          result = engine.FindView( controllerContext, viewName, masterName, false );//Func<IViewEngine, ViewEngineResult> locator = e => e.FindView( controllerContext, viewName, masterName, false );
 
          if ( result.View != null )
          {
            return result;
          }
 
          searched.AddRange( result.SearchedLocations );
        }
      }
 
      return new ViewEngineResult( searched );
    }

方法虽然复杂点,但逻辑很简单,首先遍历Items里面所有的IViewEngine,调用FindView方法(最后一个参数为true),如果有任何一个IViewEngine返回的结果的View不是null,就返回。

如果遍历了一次没有找到任何View,那么就进行二次遍历(最后一个参数为false),同样的,一旦发现有一个IViewEngine返回的结果存在有View,就返回。两次遍历都没结果的话,就返回一个new ViewEngineResult( searched )。searched是已经查找过的位置(根据result.SearchedLocations这个名字直接就知道了)。

最后查查MSDN可知,ViewEngines.Engines是所有可用的视图引擎(IViewEngine的实例)的集合,这个Items是其基类Collection<IViewEngine>的属性,简单的说就是一个容器,存放所有视图引擎。

结合之前的结论可以得到ViewResult的ExecuteResult全部执行过程:

  • 查找视图(IView)
    • 调用ViewResultBase.ViewEngineCollection.FindView
      • 调用ViewEngines.Engines.FindView
        1. 遍历所有的视图引擎(IViewEngine实例),调用FindView方法(最后一个参数为true)
        2. 若找到任何一个视图引擎返回的结果中View不为null,则返回。
        3. 第二次遍历,最后一个参数用false来调用FindView
        4. 若找到View则返回
        5. 否则返回一个没有View的ViewEngineResult,在ViewResult.FindView中将会抛出异常。
  • 呈现视图(IView.Render)
  • 释放试图(IViewEngine.ReleaseView)

最后来看看IViewEngine接口长啥样:

  public interface IViewEngine
  {
    ViewEngineResult FindPartialView( ControllerContext controllerContext, string partialViewName, bool useCache );
    ViewEngineResult FindView( ControllerContext controllerContext, string viewName, string masterName, bool useCache );
    void ReleaseView( ControllerContext controllerContext, IView view );
  }

我们发现这个IViewEngine的实例主要就干两件事情:FindView和ReleaseView。

综合上面的结论,我们可以搞清楚ViewResult、IView和IViewEngine的分工和职责了:

ViewResult:提供ActionResult的实现,查找视图(FindView)并且呈现它(IView.Render),最后释放 (IViewEngine.Release)。很明显,ViewResult是视图(IView)的使用者。几乎也是唯一的使用者:

image

IViewEngine:提供查找视图(FindView)和释放视图(ReleaseView)的功能,从FindView的最后一个参数(useCache)来看,它还可以缓存视图实例以便下次使用。所以,他是视图(IView)的管理者。

IView:负责呈现HTML视图。

好了,今天的研究就到此为止了,要煮饭给老婆吃了。

[转载]CMS系统模板引擎设计(4):Parameter类设计

mikel阅读(993)

[转载]CMS系统模板引擎设计(4):Parameter类设计 – 氣如蘭兮長不改,心若蘭兮終不移。 – 博客园.

紧接上回,说到Parameter和Field了。
在 Label初始化的时候,同时也要对ParameterCollection和FiledCollection初始化。在上节有个属性是这样写的 ArticleId=Url(articleid),意思是ArticleId 的值是url的query里的articleid的参数的值。而且还有个 DateFormat=”yyyy年MM月dd日”。所以可以看出Parameter的多样化,我个人认为Parameter是最难设计的!以至于我现在 写博文都心虚,我之前的系统里对Parameter处理的也比较糟糕,有不少hardcode。
我们说下Parameter的麻烦之处:
1、我们具体Label(Article的List)需要获取Parameter的值,有int string bool等,所以Parameter需要把本身的value可转成任意基础类型
2、有的Parameter的value不是具体值,而是一个方法调用,我们需要反射这个方法,这个方法存在哪里?Core还是具体的某个模块(比如文章)实现的
3、像Format之类的Parameter显然是用来处理“后事”的,他不会用到前期取值,而是得到后的format工作,也就是需要传一个未知的值。
4、如何做到Label也可以用,Field也可以用。前者主要用做参数,后者主要用来format。当然,前者有时也format。
带着这几个问题,我们想想Parameter应该有什么样的内容?

/// <summary>
/// Label参数类
/// </summary>
public class Parameter
{
/// <summary>
/// 参数名
/// </summary>
public string Name { get; set; }
/// <summary>
/// 参数初始值
/// </summary>
public string Value { get; set; }

public Parameter() { }
public Parameter(string name, string value)
{
Name
= name;
Value
= value;
}
/// <summary>
/// 获取参数最终值(默认为初始值)
/// </summary>
/// <param name=”parameters”></param>
/// <returns></returns>
public virtual string GetValue(params string[] parameters)
{
return Value;
}
}

这是Parameter基类,那么Url等特殊的参数我设计成了子类!

public class Url : Parameter
{
public override string GetValue(params string[] parameters)
{
return PageCollection.GetCurrentPage().UrlPattern.GetValue(RequestUtility.Rawurl(), Name);
}
}

public class Format : Parameter
{
public override string GetValue(params string[] parameters)
{
if (parameters == null) return string.Empty;
var val
= parameters[0];
return Value.Replace(@me, val);
}
}

public class DateFormat : Parameter
{
public override string GetValue(params string[] parameters)
{
if (parameters == null) return string.Empty;
DateTime t;
if (DateTime.TryParse(parameters[0], out t))
{
return t.ToString(Value);
}
return parameters[0];
}
}

呵呵,GetValue貌似不是很漂亮,但确实解决了传值不定的情况。那我们如何实例化ParameterCollection的呢?(其实就是看怎么实例化这些Parameter的)

/// <summary>
/// Parameter集合
/// </summary>
public class ParameterCollection : IEnumerable<Parameter>
{
private static readonly Regex FindPattern = new Regex(@”(?<name>\w+)=(?<value>(“”([^””]+)””)|(‘[^’]+’)|([^\s\}]+)), RegexOptions.Compiled);

private readonly IDictionary<string, Parameter> _dict;

public ParameterCollection(string parameterString)
{
//两个return都会造成_dict为null,枚举此类的时候会抛异常,所以把dict实现实例化了
_dict = new Dictionary<string, Parameter>();

if (parameterString == string.Empty) return;
var matches
= FindPattern.Matches(parameterString);
if (matches.Count == 0) return;

//开始初始化所有Parameter
foreach (Match m in matches)
{
var name
= m.Groups[name].Value;
var value
= m.Groups[value].Value;

_dict.AddValue(name, ParameterFactory.Create(name, value));
}
}

public Parameter this[string key]
{
get { return _dict[key]; }
}

public IEnumerator<Parameter> GetEnumerator()
{
foreach (var item in _dict)
{
yield return item.Value;
}
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

其中的AddValue是我写的扩展方法:
代码

public static void AddValue<TKey, TValue>(this IDictionary<TKey, TValue> dict, TKey key, TValue value)
{
if (dict.ContainsKey(key))
{
dict[key]
= value;
}
else
{
dict.Add(key, value);
}
}

代码最会说话,我就不废话了,可以看到这是一个还没完工的Collection,而创建Parameter部分不在这,在Factory 哈哈。

public static class ParameterFactory
{
private static readonly Regex FuncPattern = new Regex(@”(?<func>\w+)\((?<parameter>[^)(]+?)\), RegexOptions.Compiled);
/// <summary>
/// 获取一个Parameter
/// </summary>
/// <param name=”name”></param>
/// <param name=”value”></param>
/// <returns></returns>
public static Parameter Create(string name, string value)
{
Parameter parameter;
if (IsSpecialParameter(name))
{
parameter
= GetParameterByName(name);
}
else if (FuncPattern.IsMatch(value))
{
parameter
= GetParameterByName(name);
}
else
{
parameter
= new Parameter(name, value);
}
return parameter;
}
/// <summary>
/// 是否为特殊名称的Parameter
/// </summary>
/// <param name=”name”></param>
/// <returns></returns>
private static bool IsSpecialParameter(string name)
{
return false;
}
/// <summary>
/// 根据参数名获取Parameter(例如format=”e.g@me”)
/// </summary>
/// <param name=”name”></param>
/// <returns></returns>
private static Parameter GetParameterByName(string name)
{
//通过反射创建Parameter类
return null;
}
/// <summary>
/// 根据参数值获取Parameter(例如”Url(articleid)”)
/// </summary>
/// <param name=”value”></param>
/// <returns></returns>
private static Parameter GetParameterByValue(string value)
{
return null;
}

}

方法内部我没写如何实现,无外乎就是反射,所以大家理解思路即可。先判断是否有特殊的name,然后再判断是否有特殊的value,最后再是最普通的。

这样感觉就灵活的很多,而且如果用户想自定义一些function扩展,自需要在这几的程序集的特定名字空间下实现Parameter的继承,系统会自动find到这个特殊name或value。 不过实际应用中貌似这种需求不是很多,一般系统提供的足够用了。

写了好几个小时,才刚写好Parameter,后面再说Field的吧,Field还是比较复杂的。

[转载]CMS系统模版引擎设计(3):Label基类的设计

mikel阅读(863)

[转载]CMS系统模版引擎设计(3):Label基类的设计 – 氣如蘭兮長不改,心若蘭兮終不移。 – 博客园.

上节讲了页面的整个生产流程,大家都期待第三篇,也就是生产的核心内容——Label的替换。说实话,我很有压力啊:)一个人一个实现思路,所以…可能你不能接受。
我的标签分为2种,一种是配置变量标签(就是站点和系统的Config),用 %变量名%表示,在初始化Labels之前是要执行替换的。另外一种就是数据调用的Label咯。看下风格:
//简单的循环列表
{Article:List Top=”10″ CategoryId=”5″}
<a href =”/details/[field:FileName/]” target=”_blank”>[field:Title/]</a>
{/Article:List}
//引用用户控件模版,CategoryId是需要传递的参数
{System:Include TemplateId=”5″ CategoryId=”7″/}
//详情页模版
{Article:Model ArticleId=”Url(articleid)“}
<h1>[field:Title/]</h1>
{/Article:Model}
{Artcile:Model name=”PostTime” dateformat=”yyyy年MM月dd日/}

大家可以看出点端倪了吧,格式都是统一的。我来说下:
Article:List:是Article模块下的List标签
Top :调用条数
CategoryId:分类ID

当然还支持其他的属性比如Skip,Class,Cache等等,这些属性关键是看List标签的支持度。
下面的<a>…</a>当然是循环部分,而[field:FieldName/]则是具体的字段,接着是关闭标签。
但例如System模块的Include标签却没有内容部分。
而详情页的字段展示和列表不同,他的字段可以任意位置摆放。所以可以下面的那个Model虽没有ID也可以输出:) 这些七七八八的细节比较多。
我们如何解释这些标签代码呢?
其 实这些都是文本,依靠文本执行代码就得靠反射了。所以得反射!是的,Article是程序集(或者是命名空间),而List其实就是个类。List又包含 了好多参数,还包含了循环体,所以参数其实也是类(Parameter),而循环体里有[field]其实也是类(Field)。呵呵,一切皆是类。
那么,各种的标签都是类,我们需要抽象出他们的公共部分作为基类,或许还要设计些接口?
根据我们提到的所有信息里,目前能想到的就是Id,Parameters,Fields,Cache,Html和GetHtml()方法。
从 上面的标签里我们有看到include会给子模版里的标签传参,所以Parameters应该是可变的,Fields也最好可变的,所以数组都不合适。另 外循环的时候要替换Field,所以Fields最好是键值对集合(k/v)。Parameters也存成K/V合适吗?暂时也这么存吧。
每 个标签在网页里出现的目的是什么?转换成Html,哪怕他是空(或许是在某些条件下输出的是空),那么我们设计成为virtual函数还是抽象成接口呢? 首先说虚函数的意义,就是子类可以去覆盖,但也可以直接使用,而接口则是必须实现。如果设计成接口,就算不输出的标签也要多去实现,那不是很烦。所以暂时 我们设计成虚函数,或许我们的决定是错的。 另外GetHtml感觉名称不够准确,因为每个Label都有原始的Html代码,所以改名为 GetRenderHtml()。
/// <summary>
/// Label基类
/// </summary>
public class Label
{
/// <summary>
/// ID,一般用于缓存的Key
/// </summary>
public string ID { get; set; }
/// <summary>
/// 原始的HTML代码
/// </summary>
public string Html { get; set; }
/// <summary>
/// 标签的参数
/// </summary>
public IDictionary<string,Parameter> Parameters { get; set; }
/// <summary>
/// 标签的字段
/// </summary>
public IDictionary<string, Field> Fields { get; set; }
/// <summary>
/// 缓存
/// </summary>
public Cache Cache { get; set; }
/// <summary>
/// 获取需要呈现的HTML
/// </summary>
/// <returns></returns>
public virtual string GetRenderHtml()
{
return string.Empty;
}
}
大家是否觉得Parameters和Fields很难看呢?因为关于他们的操作(获取某个parameter,删除,增加,枚举等)还很多,所以应该单独封装,而且万一哪天发现IDictionary不合适,所以封装是合适的。所以改成了,
public ParameterCollection Parameters { get; set; }
public FieldCollection Fields { get; set; }
那么怎么在页面里发现这些Label,并实例化他们呢? 当然是强大的正则了。
{((?<a>\w+):(?<c>\w+))(?<p>[^}]*)((/})|(}(?<t>(?>(?<o>{\1[^}]*})|(?<-o>{/\1})|(?:(?!{/?\1)[\s\S]))*)(?(o)(?!)){/\1}))
懂正则的朋友我想说:你懂的:)。字符串被分为了4个组分别是assembly,class,parameters,template。
而Label的ParameterCollection和FiledCollection则需要从<parameters>组和<template>组再次使用正则获取。
Parameter的正则:(?<name>\w+)=(?<value>(“([^”]+)”)|(‘[^’]+’)|([^\s\}]+))
Field的正则:\[field:(?<name>[\w\.]+)(?<parameters>[^]]+)?/\]
我说下嵌套的实现思路:
1、递归Template找到所有的Label,被嵌套的必须有ID号
2、当替换外层Label每行数据时,需要把当前行的数据DataItem传递给里层的Label,里层的Label实例可以通过FindLabel(id)来找到。是不是觉得有点像Repeater啊?哈哈。
3、外层Label的Template是需要Replace掉内层Label的Html的。不然Field就乱了。
说了这么多不如看代码明白,那就创建个LabelFactory类,负责Label的生产。
public class LabelFactory
{
/// <summary>
/// 匹配Label的正则
/// </summary>
private static readonly Regex LabelRegex = new Regex(@”{((?<a>\w+):(?<c>\w+))(?<p>[^}]*)((/})|(}(?<t>(?>(?<o>{\1[^}]*})|(?<-o>{/\1})|(?:(?!{/?\1)[\s\S]))*)(?(o)(?!)){/\1})));

/// <summary>
/// 根据模版获取其包含的所有Label
/// </summary>
/// <param name=”template”>模版</param>
/// <param name=”preInit”>Label初始化前需要的工作</param>
/// <returns></returns>
public static IList<Label> Find(string template, Action<Label> preInit)
{
var ms
= LabelRegex.Matches(template);
if (ms.Count == 0) return null;

var list = new List<Label>();
foreach (Match m in ms)
{
var label
= Create(m.Groups[0].Value, m.Groups[a].Value, m.Groups[c].Value, m.Groups[p].Value, m.Groups[t].Value);
//订阅事件
if (preInit != null)
{
label.PreInit
+= preInit;
}
//查找Label的子Label,如果存在则会替换Label的TemplateString
var labels = Find(label.TemplateString);
if (labels != null)
{
label.TemplateString
= label.TemplateString.Replace(labels[0].TemplateString, string.Empty);
}

//label.Init();
list.Add(label);

if (labels != null)
list.AddRange(labels);
}
return list;
}

/// <summary>
/// 重载上面的Find,一般情况下使用该方法,除非需要特殊处理某些标签
/// </summary>
/// <param name=”template”></param>
/// <returns></returns>
public static IList<Label> Find(string template)
{
return Find(template, null);
}

/// <summary>
/// 反射创建一个Label
/// </summary>
/// <param name=”template”>标签的原始HTML,用于替换使用</param>
/// <param name=”a”>程序集名称</param>
/// <param name=”c”>标签类名称</param>
/// <param name=”p”>标签参数</param>
/// <param name=”t”>标签的模版</param>
/// <returns></returns>
private static Label Create(string template, string a, string c, string p, string t)
{
var assembly
= Assembly.Load(a);
var label
= assembly.CreateInstance(c, true) as Label;
label.Html
= template;
label.TemplateString
= t;
label.ParameterString
= p;
return label;
}
}

这代码只是比较简单的,异常肯定是有的,我只是写思路:)
细 心的朋友会发现Label又增加了些新内容,是的,这是在设计过程中的填充和修改。没有人一开始就考虑的十分周全,这是一个正常的设计过程。看看 Label的改动,增加了几个属性,一个preinit事件,和一个初始化方法init给定一段html代码,里面会包含若干个label,所以find 会返回一个list,另外我们还需要一个Create方法类反射每一个label。
在 实例化一个label后,还需要继续看这个label是否嵌套了label,所以要对该label的template继续find,如此递归。。如果能找 到label,则把父亲的template里最先发的label的template替换掉。不然初始化Fields的时候会出问题。
为什么设计了一个事件?
因为Include标签是需要传参给里面的label的,所以在label初始化之前可能会改动label的parameterString和templateString:) 希望您能理解。
/// <summary>
/// 原始的HTML代码
/// </summary>
public string Html { get; set; }
/// <summary>
/// Label的Parameter字符串
/// </summary>
public string ParameterString { get; set; }
/// <summary>
/// Label的模版
/// </summary>
public string TemplateString { get; set; }
/// <summary>
/// 初始化之前的事件
/// </summary>
public event Action<Label> PreInit;
/// <summary>
/// 初始化Label
/// </summary>
public virtual void Init()
{
if (PreInit != null)
{
PreInit(
this);
}
//初始化所有参数
Parameters = new ParameterCollection(ParameterString);
//初始化所有字段
Fields = new FieldCollection(TemplateString);
}

好了,写了太久了,大家和我都消化消化,休息下:)后面继续讲Parameters和Fields的设计。

[转载]ASP.NET MVC:自定义 Route 以生成小写的 Url

mikel阅读(966)

转载ASP.NET MVC:自定义 Route 以生成小写的 Url – 鹤冲天 – 博客园.

先给出本文中测试用的 controller:

public class PersonsController : Controller
{
    public ActionResult Query(string name)
    {
        return View();
    }
}

ASP.NET 中 Url 大小写

不严格来讲,ASP.NET MVC 对 Url 是不敏感的,以下 Url 都是相同的,都可以访问到 PersonController 的 Query 方法:

  1. ~/Persons/Query
  2. ~/PERSONS/QUERY
  3. ~/persons/query

但对 MVC 的数据绑定来说,大小写似乎还是有区别的:

  1. ~/Persons/Query?Name=Bob
  2. ~/Persons/Query?Name=bob

对以上两个 Url,Query 中 name 参数会接收到两个不同值:Bobbob。Action 中的参数只是原样接收,并没有作任何处理。至于name 字符串的大小写是否敏感要看具体的应用了。

再回头看前面的三个 Url:

  1. ~/Persons/Query: 是 MVC 中默认生成的,因为在 .Net 中方法命名通常采用 PascalCase;
  2. ~/PERSONS/QUERY: 全部大写,这种写法很不友好,很难读,应该杜绝采用这种方式;
  3. ~/persons/query:这种方式比较好,易读,也是大多数人选择的方式。

本文探讨如何在 MVC 中使用第三种方式,也就是小写(但不完全小写),目标如下:

在不影响程序正常运行的前提下,将所有能小写的都小写,如:

~/persons/query?name=Bob&age=18

~/persons/query/name/Bob/age/18

MVC 中 Url 的生成

在 View 中生成超级链接有多种方式:

<%: Html.ActionLink("人员查询", "Query", "Persons", new { name = "Bob" }, null) %>
<%: Html.RouteLink("人员查询", new { controller = "Persons", action = "Query", name = "Bob" })%>
<a href="<%:Url.Action("Query", "Persons", new { name="Bob" }) %>">人员查询</a>

在 Action 中,可以使用 RedirectTo 来调转至新的页面:

return RedirectToAction("Query", "Persons", new { name = "Bob" });
return RedirectToRoute(new { controller = "Persons", action = "Query", name = "Bob" });

ActionLink、RouteLink、RedirectToAction 和 RedirectToRouter 都会生成 Url,并最终显示在浏览器的地址栏中。

这四个方法都有很多重载,想从这里下手控制 Url 小写实在是太麻烦了。当然也不可行,因为可能还有其它方式来生成 Url。

MVC 是一个非常优秀的框架,但凡优秀的框架都会遵循 DRY(Don’t repeat yourself) 原则,MVC 也不例外。MVC 中 RouteBase 负责 Url 的解析和生成:

public abstract class RouteBase
{
    public abstract RouteData GetRouteData(HttpContextBase httpContext);
    public abstract VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values);
}

GetRouteData 用来解析 Url,GetVirtualPath 用来生成 Url。ActionLink、RouteLink、RedirectToAction 和 RedirectToRouter 内部都会调用 GetVirtualPath 方法来生成 Url。

因此我们的入手点就是 GetVirtualPath 方法。

自定义 Route 以生成小写的 Url

MVC 中 RouteBase 的具体实现类是 Route,我们经常在 Global.asax 中经常使用:

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            "Default", // Route name
            "{controller}/{action}/{id}", // URL with parameters
            new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
        );
    }
    //...
}

MapRoute 返回 Route,MapRoute 有很多重载,用来简化我们构建 Route 的过程。

Route 类没有给我们提供可直接扩展的地方,因此我们只能自定义一个新的 Route 来实现我们的小写 Url。但处理路由的细节也是相当麻烦的,因此我们最简单的方式就是写一个继承自 Route 的类,然后重写它的 GetVirtualPath 方法:

public class LowerCaseUrlRoute : Route
{

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        //在此处进行小写处理
        return base.GetVirtualPath(requestContext, values);
    }
}

再来看下我们的目标:

~/persons/query?name=Bob&age=18

~/persons/query/name/Bob/age/18

其实我们只需要进行两步操作:

  1. 将路由中的 area、controller、action 的值都变成小写;
  2. 将路由中其它键值对的键变成小写,如:Name=Bob 中的 Name。

那我们先来完成这个功能吧:

private static readonly string[] requiredKeys = new [] { "area", "controller", "action" };

private void LowerRouteValues(RouteValueDictionary values)
{
    foreach (var key in requiredKeys)
    {
        if (values.ContainsKey(key) == false) continue;

        var value = values[key];
        if (value == null) continue;

        var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
        if (valueString == null) continue;

        values[key] = valueString.ToLower();
    }

    var otherKyes = values.Keys
        .Except(requiredKeys, StringComparer.InvariantCultureIgnoreCase)
        .ToArray();

    foreach (var key in otherKyes)
    {
        var value = values[key];
        values.Remove(key);
        values.Add(key.ToLower(), value);
    }
}

GetVirtualPath 生成 Url 时,会将 requestContext.RouteData.Values、values(第二个参数) 以及 Defaults(当前 Router 的默认值)三个 RouteValueDictionary 进行合并,如在 View 写了如下的一个 ActionLinks:

<%: Html.ActionLink("查看") %>

生成的 Html 代码可能是:

<a href="/Home/Details">查看</a>

因为没有指定 Controller,MVC 会自动使用当前的,即从 requestContext.RouteData.Values 中获取 Controller,得到 ”Home“;”Details“来自 values;如果连 ActionLink 中 Action 也不指定,那将会从 Defaults 中取值。

因此我们必须将这三个 RouteValueDictionary 都进行处理才能达到我们的目标:

public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
    LowerRouteValues(requestContext.RouteData.Values);
    LowerRouteValues(values);
    LowerRouteValues(Defaults);
    return base.GetVirtualPath(requestContext, values);
}

再加上几个构造函数,完整的 LowerCaseUrlRoute 如下:

public class LowerCaseUrlRoute : Route
{
    private static readonly string[] requiredKeys = new [] { "area", "controller", "action" };

    public LowerCaseUrlRoute(string url, IRouteHandler routeHandler)
        : base(url, routeHandler) { }
    
    public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
        : base(url, defaults, routeHandler){ }

    public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, IRouteHandler routeHandler)
        : base(url, defaults, constraints, routeHandler) { }
    public LowerCaseUrlRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints, RouteValueDictionary dataTokens, IRouteHandler routeHandler)
        : base(url, defaults, constraints, dataTokens, routeHandler) { }    

    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        LowerRouteValues(requestContext.RouteData.Values);
        LowerRouteValues(values);
        LowerRouteValues(Defaults);
        return base.GetVirtualPath(requestContext, values);
    }

    private void LowerRouteValues(RouteValueDictionary values)
    {
        foreach (var key in requiredKeys)
        {
            if (values.ContainsKey(key) == false) continue;

            var value = values[key];
            if (value == null) continue;

            var valueString = Convert.ToString(value, CultureInfo.InvariantCulture);
            if (valueString == null) continue;

            values[key] = valueString.ToLower();
        }

        var otherKyes = values.Keys
            .Except(requiredKeys, StringComparer.InvariantCultureIgnoreCase)
            .ToArray();

        foreach (var key in otherKyes)
        {
            var value = values[key];
            values.Remove(key);
            values.Add(key.ToLower(), value);
        }
    }
}

有了 LowerCaseUrlRoute,我们就可以修改 Global.asax 文件中的路由了。

创建 LowerCaseUrlRouteMapHelper

这一步不是必须的,但有了这个 MapHelper 我们在修改 Global.asax 文件中的路由时可以非常方便:

public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapLowerCaseUrlRoute( //routes.MapRoute(
        "Default", // Route name
        "{controller}/{action}/{id}", // URL with parameters
        new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
    );
}

尤其是已经配置了很多路由的情况下,其代码如下:

public static class LowerCaseUrlRouteMapHelper
{
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url){
        return routes.MapLowerCaseUrlRoute(name, url, null, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults){
        return routes.MapLowerCaseUrlRoute(name, url, defaults, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, string[] namespaces){
        return routes.MapLowerCaseUrlRoute(name, url, null, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, object constraints){
        return routes.MapLowerCaseUrlRoute(name, url, defaults, constraints, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces){
        return routes.MapLowerCaseUrlRoute(name, url, defaults, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces){
        if (routes == null) throw new ArgumentNullException("routes");
        if (url == null) throw new ArgumentNullException("url");
        LowerCaseUrlRoute route2 = new LowerCaseUrlRoute(url, new MvcRouteHandler());
        route2.Defaults = new RouteValueDictionary(defaults);
        route2.Constraints = new RouteValueDictionary(constraints);
        route2.DataTokens = new RouteValueDictionary();
        LowerCaseUrlRoute item = route2;
        if ((namespaces != null) && (namespaces.Length > 0))
            item.DataTokens["Namespaces"] = namespaces;
        routes.Add(name, item);
        return item;
    }

    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url){
        return context.MapLowerCaseUrlRoute(name, url, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults){
        return context.MapLowerCaseUrlRoute(name, url, defaults, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, string[] namespaces){
        return context.MapLowerCaseUrlRoute(name, url, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, object constraints)        {
        return context.MapLowerCaseUrlRoute(name, url, defaults, constraints, null);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, string[] namespaces){
        return context.MapLowerCaseUrlRoute(name, url, defaults, null, namespaces);
    }
    public static LowerCaseUrlRoute MapLowerCaseUrlRoute(this AreaRegistrationContext context, string name, string url, object defaults, object constraints, string[] namespaces)
    {
        if ((namespaces == null) && (context.Namespaces != null))
            namespaces = context.Namespaces.ToArray<string>();
        LowerCaseUrlRoute route = context.Routes.MapLowerCaseUrlRoute(name, url, defaults, constraints, namespaces);
        route.DataTokens["area"] = context.AreaName;
        bool flag = (namespaces == null) || (namespaces.Length == 0);
        route.DataTokens["UseNamespaceFallback"] = flag;
        return route;
    }
}

总结

大功告成,如果你感兴趣,不妨尝试下!

写到这里吧,如果需要,请下载本文中的示例代码:MvcLowerCaseUrlRouteDemo.rar(209KB)

如果你有其它办法,欢迎交流!

[转载]CMS系统模版引擎设计(2):页面生产流程

mikel阅读(1027)

[转载]CMS系统模版引擎设计(2):页面生产流程 – 氣如蘭兮長不改,心若蘭兮終不移。 – 博客园.

上节我们介绍了下基本的几个类。下面我们来讲下本CMS系统的运行机制。
一个Url请求过来,我们的CMS系统就要对URL进行分析,知道其Domain属于哪个Site,Url符合哪个Page的访问规则,然后再把Page的Template里的Label进行行替换。
如何拦截Url请求呢? 那就是HttpModule。
自定义一个CMSHttpModule:IHttpModule,在config里增加HttpModule的配置即可。

我画了一张图,先看一下生产流程。

步骤如下:

获取匹配当前domain的Site,如果匹配不到,说明数据库中不存在这个Site。那就只能输出空信息了。
【注意】如果你想让你的URL访问没有后缀,那必须添加“通配符映射”,并且不能“检查文件是否存在”,什么是通配符?即使所有的URL格式,不管是什 么后最,有没有后缀,都会走ASP.NET的 ISAPI筛选器。也就是都会走我们定义的HttpModule,包括页面内的图片、js等静态资源。所以我们必须过滤掉不想处理的后缀,这些后缀可以是 针对每个Site配置,也可以针对全局配置,所以我们还需要给Site类增加一个Config属性,并且定义一个Config类。

代码

public class Config
{
private IDictionary<string, string> _configs;

public string this[string key]
{
get
{
return _configs != null && _configs.ContainsKey(key) ? _configs[key] : string.Empty;
}
set
{
if (_configs == null)
{
_configs
= new Dictionary<string, string>();
}

if (_configs.ContainsKey(key))
_configs[key]
= value;
else
_configs.Add(key, value);
}
}
}

Site获取ConfigValue的逻辑是先从自己的Config获取,如果没有再去Global的config获取。为什么需要Config呢?因为不同的站点可能需要自定义一些变量到前台使用。

好了,过滤说完了,就该获取Page了。

根据URL拿Page实例,Page也需要用hashtable来缓存,因为这个获取实例的请求实在是太大了,复杂度也要降到O(1)。

如果page是null,说明数据库不存在这个Page,所以要跳出HttpModule,让IIS接手继续处理。

如果存在,则需要判断Page是否缓存,我们这里缓存其实就是生成静态页,如果缓存了,则根据URL生成静态页文件名,再去查找是否存在这个文件,如果 存在还要判断文件的创建时间是否过期。如果过期了,我们就得重新写入一个新文件,通过返回旧文件,等新文件创建成功后覆盖掉旧文件(读写分离)。

如果不是缓存,则获取Page.Template.Content,然后循环Labels,对Content进行替换。

var html = page.Template.Content;
foreach (var label in page.Labels)
{
html
= html.Replace(label.Template, label.GetHtml());
}

最后输出被替换掉标签的html,则页面生产完成。

我讲的都是粗略的框架,开发过程中会出现很多细节性的东西,我就不提了,只提一些比较重要的。下节我们该讲如何设计Label了!

[转载]CMS系统模版引擎设计(1):基础类型

mikel阅读(1004)

[转载]CMS系统模版引擎设计(1):基础类型 – 氣如蘭兮長不改,心若蘭兮終不移。 – 博客园.

写博文写 教程性质的内容,大家比较喜欢,具体的项目设计方案关注的人比较少,而且思路也不一定说的清楚,本身写博文就比较辛苦,作者再偷点懒,那基本上就是一篇废 文。尽管如此,我还是想写一下我做过的一个项目的模块设计——CMS的模版引擎。呵呵,叫成“引擎”就是夺人眼球而已。其实就是一个标签解释的过程模块。
做过网站 的朋友都对CMS很熟悉,有的朋友也接触过N多CMS系统,国内比较流行的有dedeCMS,phpCMS,帝国 CMS,KingCMS,PowerEasyCMS等等,他们都有个共同的特点,就是前台的实现是模版标签机制。标签的好处就是可以让非专业开发人员通过 特定的标签实现数据调用。一段标签表示一种数据的调用。那我的这个CMS也是干这事的,不过是用.NET实现的。实现思路也是自己琢磨出来的,如有雷同, 那真是太巧合了:)
从哪里开始讲呢?还是从业务开始吧!
当公司接 到一个网站的单子后,就要估算价格。估算价格一般都要问开发经理这个网站的开发周期和难度(方便忽悠好要价),开发经理也就是我们技术leader,他会 估算该网站大概需要多少页面,需要什么子模块,需要干多久(假如没有CMS的话)。呐,这里就有个重要的信息,就是需要多少页面和什么模块。
做一个网站我们肯定要考虑他的功能模块,也要考虑多少页面。
功能模块一般有 文章、图片(相册)、视频、投票、留言、评论、下载、单页、自定义表单等等。我们介绍模版,这些功能模块就不多提了。
页面的概 念便是需要多少去制作的Page.aspx。一般有首页(子站首页)、封面页(可以理解为栏目封面)、列表页、详情页、独立页(关于我们之类的),不同的 数据类型还不一定一致,比如新闻和下载都是不一样的页。而模版的话不可能是要制作人员创建aspx的,他们只会用模版。所以通过这些信息我们能想到需要设 计哪些类呢?
Page类、Template类、Label类,页面、模版和标签。
Label涉及到具体的标签系列,咱暂且不说,先看Template如何设计?
一个模版有何设计的?有的CMS系统就是直接读取静态文件,但是我说这样不好,我们要在代码里有他的具体类型才能更方便的处理。
代码

/// <summary>
/// 模版类
/// </summary>
public class Template
{
/// <summary>
/// 模版ID
/// </summary>
public Guid TemplateId { get; set; }
/// <summary>
/// 模版名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 模版内容
/// </summary>
public string Content { get; set; }
/// <summary>
/// 是否为部分视图
/// </summary>
public bool IsPartial { get; set; }
}

初步设计就是如此,增加了一个IsPartial属性,因为我们的模版或许会作为部分视图在其他模版里调用,就像UserControl一样。

那 么Page类的设计就略显复杂了。我们想访问一个Page起码要有访问路径吧,所以就要有一个UrlPattern属性,也就是访问规则,因为像详细页一 般只是参数的变化,所以URL不能写死,只能是一个规则。既然有规则,也会有一些参数,而且参数不一定是?name=value形式,可能是/value /value1形式,所以我们还得设计一个UrlPattern类。

代码

/// <summary>
/// Url访问规则
/// </summary>
public class UrlPattern
{
/// <summary>
/// 具体规则
/// </summary>
public string Pattern { get; set; }
/// <summary>
/// 正则引擎
/// </summary>
public Regex Regex { get; set; }
/// <summary>
/// 参数列表
/// </summary>
public string[] Parameters { get; set; }
/// <summary>
/// 获取某个参数的值
/// </summary>
/// <param name=”rawurl”>当前访问的URL</param>
/// <param name=”name”>参数名</param>
/// <returns></returns>
public string GetValue(string rawurl, string name)
{
throw new System.NotImplementedException();
}
}

是的,你没看错,我们要用正则表达式,这可能是对制作人员难度最大的部分。:)不过可以教他们初级的写法,可以应付绝大多数需求。

比如我写一个规则如下  /details/(?<articleid>\d+),这个表明参数名为articleid,访问规则就是 “/details/数字”

Page除了访问URL外还要有缓存的概念,不然我们如何提升性能,你说是不?!缓存可能还会用标签里去,因为如果Page不缓存只是缓存了某个标签,所以Label也要有缓存,那么我们是否需要设计一个缓存类呢?

代码

/// <summary>
/// Page/Template/Label的缓存
/// </summary>
public class Cache
{
/// <summary>
/// 缓存名
/// </summary>
public string Key { get; set; }
/// <summary>
/// 缓存秒数
/// </summary>
public int CacheSeconds { get; set; }
/// <summary>
/// 获取缓存数据
/// </summary>
/// <returns></returns>
public object GetData()
{
throw new System.NotImplementedException();
}
/// <summary>
/// 移除缓存
/// </summary>
public void Remove()
{
throw new System.NotImplementedException();
}
/// <summary>
/// 更新缓存
/// </summary>
/// <param name=”data”></param>
public void SetData(object data)
{
throw new System.NotImplementedException();
}
}

那么Page类还应该有什么?Labels!是的,我们不可能每次都去解释模版来获取所有的Label,而是Page被缓存后我们只需要访问他的LabelCollection即可。那么我们来看下Page的设计雏形吧。

代码

/// <summary>
/// Page类
/// </summary>
public class Page
{
/// <summary>
/// ID
/// </summary>
public Guid PageId { get; set; }
/// <summary>
/// 名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 标题
/// </summary>
public string Title { get; set; }
/// <summary>
/// 关键字
/// </summary>
public string Keywords { get; set; }
/// <summary>
/// 描述
/// </summary>
public string Description { get; set; }
/// <summary>
/// 模版
/// </summary>
public Template Template { get; set; }
/// <summary>
/// 访问路径规则
/// </summary>
public UrlPattern UrlPattern { get; set; }
/// <summary>
/// 标签
/// </summary>
public Label[] Labels { get; set; }
/// <summary>
/// 缓存
/// </summary>
public Cache Cache { get; set; }
/// <summary>
/// 显示HTML代码
/// </summary>
public void Render()
{
throw new System.NotImplementedException();
}
}

不错哦,袄哟,不错哦。

其实对于大型的站点,子站的概念是不可缺少的。或者我们的CMS需要支持多站点,那么还需要一个Site类。

代码

/// <summary>
/// 站点
/// </summary>
public class Site
{
/// <summary>
/// 站点ID
/// </summary>
public Guid SiteId { get; set; }
/// <summary>
/// 站点名称
/// </summary>
public string Name { get; set; }
/// <summary>
/// 站点备注
/// </summary>
public string Note { get; set; }
/// <summary>
/// 站点域名
/// </summary>
public string[] Domains { get; set; }
/// <summary>
/// 站点状态
/// </summary>
public Status Status { get; set; }
/// <summary>
/// 站点的页面
/// </summary>
public Page[] Pages { get; set; }
}

站点一般包含多个可访问的域名,所以有个Domains。当然站点包含N个Page。

好啦,初步的设计就到这了,下节我们讲怎么让这些类运作起来。

[转载]面向对象:宽接口、窄接口和访问方法(上)

mikel阅读(882)

[转载]【面向对象】宽接口、窄接口和访问方法(上) – 横刀天笑的技术空间 – 博客园.

封装

封装、继承和多态是面向对象“三大金刚”。这其中封装可谓三大金刚之首。封装(或称信息隐藏)亦即不对使用者公开类型的内部实现手段,只对外提供一些接口,使用者只能通过这些公开的接口与类型进行交谈。

封装不好实际上继承和多态也是无稽之谈,即使不无稽也会风雨飘摇,使用者可能绕过你精心构造的对象层次,直接访问对象的数据,因为直接访问一切看起来那么自然而然,很简单,很直观也很容易,不需要经过大脑。

面向对象

面 向对象是一种将数据和行为绑定在一起的编程方法,虽然在面向过程的时代,也可以使用模块化设计将数据以及使用这些数据的行为绑定在一起,但是毕竟那是靠程 序员的个人自律。使用者还是可以轻松的无视这些约定,这样就导致很难发现这块数据有多少地方使用了,如何使用,带来一个问题就是我如果修改这块数据将会带 来多大的影响也将是未可知的。面向对象第一次使用强制的手段将数据和行为绑定在一起,但这一切是建立在封装的基础之上的。如果你随意的公开你的数据,那么 使用者也就可以随意的使用你的数据,没有人会觉得心里愧疚。因为那毕竟是最直接的手段。这也就是为什么很多人在使用着面向对象的语言干着面向过程的事情的 原因。

访问方法

还有一点需要指出的是封装并不是叫你将所有的内部数据都通过getter和setter的方法来 访问,套一个简简单单,全裸的方法,就说你是在封装,你说你没有让使用者直接访问数据,你骗谁呢。但是,一些著名的规范或者框架却直接无视三大金刚之首, 比如Java Bean,比如像Hibernate之类的ORM。将setter和getter作为规范或标准来执行。不过,没有办法,人家毕竟要通过一种手段来访问你 的数据,但是我觉得这种“随意”的要求你将内部敞开的做法不是什么好主意,即使你要访问内部数据,你也要将门槛设高点。还有一点是,大部分时候我们需要在 界面上显示数据,收集用户填充的数据,如是我们还是需要一堆的getter和setter。看来getter和setter还是避免不了,但观察上面的问 题我们发现,需要公开所有getter和setter的地方是在一些特定的上下文内,并不是所有地方我们都应该热情地敞开胸怀。这样我们就可以根据不同的 上下文公开不同的接口来获得更好的封装性。

比如在界面上需要显示或收集数据时,在ORM需要这种getter和setter方法时,我 们提供一种宽接口,而在业务逻辑部分我们采用窄接口,因为我不想在业务逻辑计算的时候别的类窥探我的隐私。因为,一旦我能很容易窥探到你的隐私,就总是有 这么一种诱惑:根据你的隐私我做出一些决策,而这些决策本应该是你自己做出的,因为毕竟这是你的隐私,你对它最熟悉。比如经常看到如下的代码:

   1: //if user loged in
   2: if(String.IsNullOrEmpty(user.Username) && String.IsNullOrEmpty(user.Password))
   3: {
   4:     //do something
   5: }

写出这样的代码的原因是我访问User对象的内部数据太容易了,轻而易举,如是我就帮User一个忙,我自己检查一下它的用户名和密码是不是为空, 这样就能知道这个User是不是已经登录了。可是用户名和密码都应该是用户的私有数据,本不应该暴露出来,而且验证用户是否登录的方法是否真的是如此呢? 即使今天是这样明天也不一定是这样啊。如果User类没有暴露出它的用户名和密码,那么User类的使用者也就无法使用上面的代码判断用户是否登录了,那 么他要么自己去给User类添加一个IsLogedIn的方法,要么祈求User类的开发人员添加一个。这样我们能获得什么样的好处呢?

1、我们用方法名(IsLogedIn)就能描述我们要干的事儿,代码的可读性也就更佳了,所以上面代码的第一行的注释可以问心无愧的删除。

2、如果有一天验证用户是否登录的逻辑改变了,我们只需要修改User类里面的逻辑就够了,其他地方都无需更改。

宽接口、窄接口

其实造成上面那段代码的原因责任并不在于编写那段代码的人,责任应该归咎于编写User类的人,你太随意了。

不过现在带来另外一个问题,刚才我们刚刚大谈特谈不应该随意的使用setter和getter方法将类型内部的数据暴露出去,但是我们现在需要做一 个用户登录页面,需要用户输入账号密码,然后验证,或者我们在后台管理页面需要显示本系统所有用户列表。看来我们还是躲不过setter和getter的 魔咒。这里的用户界面部分以及上面的那段代码也就是系统的不同上下文。我们可以对界面上下文公开宽接口,而对业务逻辑等部分公开窄接口。给不同的上下文看 不同的接口有很多种方法,不同的语言里也有不同的实践:

1、在C++里我们有友元(friend),如果我们有一个LoginView类表示登录窗口,User表示用户类,我们可以将LoginView 作为User的友元,这样LoginView就可以访问User的私有数据。不过使用我个人觉得使用friend是一种非常不好的实践。首 先,friend关系是不能被继承的,这在构建一些对象层次时是会出现问题的。再次,这样在一个领域类里引入一个界面类实在是一件非常奇怪的事情,说出去 都有点不好意思见人。

2、.NET里的友元程序集。.NET虽然没有友元类这个概念,但却是有友元程序集的,我们可以将LoginView所属的程序集设为User所属 程序集的友元,然后将setter和getter方法设为internal的。不过,还是一样,领域对象所在的程序集居然要知道一个界面所在的程序集,这 很荒谬。

3、我们创建一个IUser接口,然后User实现该接口。IUser是一个窄接口,在业务逻辑部分使用,而User就是宽接口,会通过setter和getter暴露内部数据。

那么我们还是来看一个案例吧。

案例

我们要开发一个选课系统,这里有这样三个对象:科目[Course](像数学啊,物理啊等,要学这个科目还必须学完该科目的预修科目,所以有个预修 科目列表)、课程[CourSEOffering](课程里面包括这是哪个科目的课程,讲师是谁,最多可以有多少个学生,现在有多少个学生等信息),还有 一个对象就是学生[Student]了(学生知道自己已经修了哪些科目了)。

现在有个问题,要选课的话,实际上就是往课程的学生列表里添加学生,那么我们该怎么做呢?

代码1:

   1: public class CourseService
   2: {
   3:     public void Choose(Student student,CourseOffering courseOffering)
   4:     {
   5:         if(student.Courses.Contains(courseOffering.Course.PreRequiredCourses) && courseOffering.LimitStudents > courseOffering.Students.Size)
   6:         {
   7:             courseOffering.Students.Add(student);
   8:         }
   9:     }
  10: }

大部分人看了上面这部分代码都会摇头,这完全就是披着class的外衣,写着过程式的代码。我们写了一个服务,里面有个Choose方法,传个学 生,传个课程,然后看看学生是不是修完了该课程对应科目的预修课程,而且看看这个课程的学生是不是已经满了,如果条件符合的话我们就将这个学生收了。经过 这么一解释,嘿嘿,这逻辑貌似很自然啊。面向过程就是这样,完全不饶弯弯,很直白的将逻辑表现出来(但这往往是表象,因为代码一多,逻辑一复杂,面向过程 的代码就会像面条一样纠缠不清,而且因为抽象层次低,需求一改变什么都玩了)。
其实我们可以思考一下为什么会写出上面的代码。实际上我想的是写Student、CourSEOffering和Course这三个类的人太随意了,将所有的数据都公开出来,因此我在这里很容易访问,也就很容易写出这种方法了。

实际上,经过思考我们觉得这个Choose方法更应该放在CourSEOffering类里,这样我们就可以不暴露Students了:

代码2:

   1: public class CourseOffering
   2: {
   3:     private readonly Course course;
   4:
   5:     private IList<Student> students = new List<Student>();
   6:
   7:     private readonly int limitStudents;
   8:
   9:     public CourseOffering(int limitStudents,Course course)
  10:     {
  11:         this.limitStudents = limitStudents;
  12:         this.course = course;
  13:     }
  14:
  15:     public void AddStudent(Student student)
  16:     {
  17:         if(student.Courses.Contains(course.PreRequiredCourses) && limitStudents > students.Count)
  18:         {
  19:             students.Add(student);
  20:         }
  21:     }
  22: }

那么选课服务也许就像下面这样了:

代码3:

   1: public class CourseService
   2: {
   3:     public void Choose(Student student,CourseOffering courseOffering)
   4:     {
   5:         courseOffering.AddStudent(student);
   6:     }
   7: }

因为CourseOffering不再公开students属性了,所以我们写这个选课服务的时候我们没办法了,我们只有求助CourseOffering。

但是在CourseOffering类的内部,还是有信息的泄露,Student将它已修的课程透露出来了(其实我是个差生,经常逃课,我真的不想 将我的已修课程透露出去)。再思考一下这里的逻辑,你不觉得检查自己是不是可以修某个科目不应该是学生自己的职责么,因为学生知道他自己修了哪些课程了。 那么我们可以进一步封装:

   1: public class Student
   2: {
   3:     private IList<Course> alreadyCourses = new List<Course>();
   4:
   5:     public bool CanAttend(Course course)
   6:     {
   7:         return alreadyCourses.Contains(course.PreRequiredCourses);
   8:     }
   9: }
  10: public class CourseOffering
  11: {
  12:     private readonly Course course;
  13:
  14:     private IList<Student> students = new List<Student>();
  15:
  16:     private readonly int limitStudents;
  17:
  18:     public CourseOffering(int limitStudents,Course course)
  19:     {
  20:         this.limitStudents = limitStudents;
  21:         this.course = course;
  22:     }
  23:
  24:     public void AddStudent(Student student)
  25:     {
  26:         if(studnet.CanAttend(this.course) && limitStudents > students.Count)
  27:         {
  28:             students.Add(student);
  29:         }
  30:     }
  31: }

这里不仅将Student应该有的职责分离出去了,还提升了 student.Courses.Contains(course.PreRequiredCourses)这条语句的抽象层次(其实面向对象的成功之一 就是能不断的提高抽象层次,抽象出领域的各种概念,促进团队对整个系统的认识)。

不过在Student里还是存在对比的对象内部数据的知悉:Student知道了课程的预修课程。嘿嘿,其实我这门课程啊,虽然预修课程有108 门,但实际上你只要修了那么五门也就可以了,但是这个事情可不能透露给那些学生哦,如果他们听到了那其余103门的补考费我找谁收去啊,呵呵。所以检查这 个学生能不能修还是我自己操刀吧,而且我还想内部的动态改变这个是不是能修的策略呢(当然,这是笑谈,不过这也透露了一点,用户的需求经常是变化的,怎么 应对这种变化?):

   1: public class Course
   2: {
   3:     private IList<Course> preRequireds = new List<Course>();
   4:
   5:     public bool Acceptable(IList<Course> courses)
   6:     {
   7:         return courses.Contains(preRequireds);
   8:     }
   9: }
  10: public class Student
  11: {
  12:     private IList<Course> alreadyCourses = new List<Course>();
  13:
  14:     public bool CanAttend(Course course)
  15:     {
  16:         return !IsAlreadyAttend(course) && course.Acceptable(alreadyCourses);
  17:     }
  18:
  19:     private bool IsAlreadyAtteded(Course course)
  20:    {
  21:         return alreadyCourses.Contains(course);
  22:    }
  23: }
  24: public class CourseOffering
  25: {
  26:     private readonly Course course;
  27:
  28:     private IList<Student> students = new List<Student>();
  29:
  30:     private readonly int limitStudents;
  31:
  32:     public CourseOffering(int limitStudents,Course course)
  33:     {
  34:         this.limitStudents = limitStudents;
  35:         this.course = course;
  36:     }
  37:
  38:     public void AddStudent(Student student)
  39:     {
  40:         if(studnet.CanAttend(this.course) && limitStudents > students.Count)
  41:         {
  42:             students.Add(student);
  43:         }
  44:     }
  45: }

至此,我们的三个领域类都不了解对方内部到底藏有什么花花肠子,我们可以任意更改我们每个类的内部实现,只需要我们的公开接口不变就行了,我们每个类都有清晰的职责,我们还通过具有描述性的名称来提升了概念的抽象层次。

但是我们的问题依然没有解决,如果这些内部的数据都不公开,我们要做一个界面显示这些对象的信息该怎么办?

【注】:如果你觉得本文的案例部分的例子有点熟悉,那么恭喜你,你的感觉是对的。本文的案例示例采用了《OOD沉思录》里的一个讲述,不过本文只采用了原文的“创意”部分(不过这年头,最缺的就是创意)。

[转载]在MVC2.0 中 进行 LINQTOSQL 实体统一验证方法(上)

mikel阅读(963)

[转载]在MVC2.0 中 进行 LINQTOSQL 实体统一验证方法(上) – RyanDing – 博客园.

场景

当我把项目从 MVC1.0 升级到 MVC2.0 时,原以为可以方便的使用 System.ComponentModel.DataAnnotations 结合 MVC2.O 的

ModelState.IsValid 进行数据有效验证。比如以下验证:

1 public class SystemUserMetaData 2 { 3 [Required(ErrorMessage = "不能为空!")] 4 [StringLength(6, ErrorMessage = "用户名长度不能超过6!")] 5 public string UserName { get; set; } 6 [Required(ErrorMessage = "IsValid is required.")] 7 public string ChineseName { get; set; } 8 [Required(ErrorMessage = "IsValid is required.")] 9 public bool IsValid { get; set; } 10 [Required(ErrorMessage = "Department is required.")] 11 public int DepartmentID { get; set; } 12 [Required(ErrorMessage = "Password is required.")] 13 public string Password { get; set; } 14 [Required(ErrorMessage = "Rank is required.")] 15 public int RankID { get; set; } 16 [PhoneAttribute(ErrorMessage = "电话号码不正确")] 17 public string MobilePhone { get; set; } 18 public int UserID { get; set; } 19 }
代码1

这些Annotation特性验证可以很轻松通过 mvc2.0  ViewData.ModelState.Values 获取到验证错误的提示信息。但是当我们的验证条件变得更加

复杂时,比如在修改一个LinqToSQL 实体时需通过该实体的主键和唯一索引进行验证实体是否唯一性时,此时需要两个字段同时验证,当这种验证出现时我

发现无法简单的使用 DataAnnotaion 进行同一实体的多字段验证。自定义 ValidationAttribute 特性重写 IsValid 时 无法根据当前的属性获取到其他属性

的值。因为ValidationAttribute 特性是附加在一个类的属性上的。可能聪明的你此刻已想到了将验证特性直接加载 LinqToSQL 的 类上。当你为这个特性

编写验证方法时就可以通过反射得到 LinqToSQL 实体的所有属性的值,或许单一的 ValidationAttribute 属性验证特性不能完成的任务就可以得到解决。

当我把LINQTOSQL 类的验证特性写完后附加到 LinqTOSQL partial 类上代码如下:

[UniqueName("UserID", "UserName", typeof(SystemUser), ErrorMessage = "该用户已存在。")] [MetadataType(typeof(SystemUserMetaData))] public partial class SystemUser { }

在MVC2.0 中当我们使用 TryUpdateModel 方法时 发现 UniqueName 的 IsValid 方法始终没有被调用。但是当 MetadataType 移除除掉,我们再调用

TyUpdateaModel方法时UniqueName 特性的 IsValid 验证方法就被正常调用了。此时我明白了问题应该是由 MVC  TryUpdateModel 方法引起,将该方

法换成 UpdateModel 后问题依旧。MetadataType 特性覆盖了 UniqueName 特性,当然了如果想知道具体的原因,可以 Reflect 出 TryUpdateModel

的方法找到到答案。为了解决这个问题,我决定使用自定义的方法进行实体验证,代码如下:

代码3

public class Validation { public static void ValidateAttributes<TEntity>(TEntity entity) { var validationInstance = new Validation(); validationInstance.ValidateAttributesInternal(entity); } public virtual void ValidateAttributesInternal<TEntity>(TEntity entity) { var validationIssues = new List<ValidationIssue>(); var props = typeof(TEntity).GetProperties(); var metatype = typeof(TEntity).GetCustomAttributes(typeof(MetadataTypeAttribute), false).FirstOrDefault(); var type = ((System.ComponentModel.DataAnnotations.MetadataTypeAttribute)(metatype)).MetadataClassType; var s = type.GetProperties(); var customAttrs = typeof(TEntity).GetCustomAttributes(true).Where(t => t.GetType().Namespace.Contains("ValidationMeta")); foreach (var attr in customAttrs) { var validate = (ValidationAttribute)attr; //执行 附加在 linqtosql partial 类 上的 ValidationAttribute 验证方法 bool valid = validate.IsValid(entity); if (!valid) { validationIssues.Add(new ValidationIssue(null, null, validate.ErrorMessage)); } } //执行附加在 linqtosql partial 类 属性上的 ValidationAttribute 验证方法 foreach (var prop in s) ValidateProperty(validationIssues, entity, prop); // throw exception? if (validationIssues.Count > 0) throw new ValidationIssueException(validationIssues); } protected virtual void ValidateProperty<TEntity>(List<ValidationIssue> validationIssues, TEntity entity, PropertyInfo property) { //得到验证特性的集合 var validators = property.GetCustomAttributes(typeof(ValidationAttribute), false); foreach (ValidationAttribute validator in validators) ValidateValidator(validationIssues, entity, property, validator); } protected virtual void ValidateValidator<TEntity>(List<ValidationIssue> validationIssues, TEntity entity, PropertyInfo property, ValidationAttribute validator) { var dataEntityProperty = typeof(TEntity).GetProperties().FirstOrDefault(p => p.Name == property.Name); var value = dataEntityProperty.GetValue(entity, null); if (!validator.IsValid(value)) { validationIssues.Add(new ValidationIssue(property.Name, value, validator.ErrorMessage)); } } }

大家留意一下代码3 中的注释,这样 Validation 这个类就就可以替代MVC TryUpdateModel 的验证功能同时让代码1的 UniqueName 和 MetaDataType 两个特性 “共存”。

MetadataType 的职责:验证实体的单一属性值的有效性。

LINQ实体类上的其他的自定义特性:如代码1中的 UniqueName 则可以进行复杂的属性验证如多属性值同时验证等。

这样我们就彻底的解决了开发过程中验证代码统一的编码规范。而不是同一个数据有效性验证的代码满天飞的局面。

小结

当我完成了以上代码似乎已经达到了预期的目的,但测试代码时候发现如果使用TryUpdateModel 更新另外一个LINQTOSQL 模型(Order表),这个被

更新的模型从数据库上来看它属于 SystemUser 的外键表。通过Order表中的UserID 字段关联到 SystemUser。当Order实体被MVC TryUpdateModel 时会同时把SystemUser 的 自定义的 [UniqueName] 特性的方法 IsValid() 也调用了,很显然这不是我们想要的。该问题我会在下一篇文章提出解决方案。