[转载]通过源代码研究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阅读(992)

[转载]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阅读(862)

[转载]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阅读(965)

转载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阅读(1024)

[转载]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阅读(1002)

[转载]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阅读(962)

[转载]在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() 也调用了,很显然这不是我们想要的。该问题我会在下一篇文章提出解决方案。

[转载]C#调试心经续

mikel阅读(1297)

[转载]C#调试心经续 – Alexis – 博客园.

由于上篇文章漏了一些比较重要的知识,在此文中补充。

断点篇

命中次数(Hit Counts)

右击断点,可以设置Hit Counts(命中次数),会弹出如下的对话框

当条件满足的时候断点会被命中(即即将被执行),这个命中次数是断点被命中的次数。默认是始终break,选项有如下的几种:始终break;当命中次数达到多少次时break;当命中次数是多少的倍数时break;当命中次数大于等于多少的时候break

于是在上篇中的条件也可以这样实现,设置命中次数等于50的时候break,按F5后,断点被触发,此时i=50

点过滤器

我们可以限制断点在特定的处理器和进程中。可以设置机器名、进程id、进程名、线程id、线程名中的某些条件来过滤一些断点。

注意:ThreadId需要特别说明一下,ThreadId并不是托管程序中,.NET 框架中System.Threading.Thread.ManagedThreadId,两者不能等同。简单来说,ManagedThreadId是线程在CLR中的标识符,而ThreadId却是线程在操作系统中的标识符。因此ThreadId需要从调试器中的“Threads”窗口中获取。

断点条件

我们可以设置断点达到的条件,如下图,我们设置表达式为i==5注意是判相等,而不是赋值的等于),按F5,断点再次被触发,此时i=50

还有一个选项是已经被改变,则里面条件是具体的变量,如我们的代码如下

private void ConditionDebug()
{
int hitCount = 0;
for (int i = 0; i < 100; i++)
{
if (i==49)
{
hitCount = 1;
}
}
Console.Write(“Hit Count={0}”, hitCount);
}

我们在代码里如果i==49,就将hitCount的值改变,同时设置断点的条件为

则当断点再次被触发的时候此时i=50。这个通常被用在找变量的时在什么时候发生改变。

断点的位置

可以设置断点的位置,如下图,设置程序到达那个文件的第几行第几个字符时触发断点。

断点触发时

我们可以设置断点到达时做一些其他的事情,如打印消息,运行一个宏。

自定义调用堆栈

堆栈跟踪时vs一步步执行你的程序是对当前的方法调用继承关系的直观显示。在调试程序时,我们会经过一个又一个方法,包括方法的嵌套调用。堆栈跟踪会对这当中的每一层方法作出记录。选择“调试–>窗口–>调用堆栈”,或者是快捷键Ctrl+Alt+C就可以看到当前的堆栈跟踪状态。这里会将每个方法单独显示为一行,并且带有行号和参数值。每一个新的方法调用被称为堆栈帧。

堆栈跟踪是广为人知的调试工具,它的优点在于你可以双击任意一行跳转到程序中该层调用方法的代码。于是你可以看到程序是如何执行到这一位置的,同时可以看到方法接受的参数值。并且可以使用Ctrl+C将一个或者全部堆栈帧复制到剪贴板,并将这个方法的调用信息发送给工作伙伴。

项目属性中的Debug选项卡

如果你的项目是Console项目(控制台应用程序)或者是WinForm项目,则右击项目解决方案,选择属性,会出来如下的项目属性窗体。

我们可以设置“启动动作”、“启动选项”和“是否启用调试”。

Start Action有三个选择项:

Start Project:默认选项,设置为启动项目

Start external program:调试的时候启动内部程序

Start browser with URL:调试的时候打开URL地址

使用Trace.axd调试ASP.NET

在以前asp时候,我们为了查看某个变量的值,通常会使用Response.Write方法。可能现在许多ASP.NET程序员也习惯在后台使用Response.Write方法将变量的值写出来,其实微软提供了很好的调试工具,即Trace.axd。它的功能主要是:配置 ASP.NET 代码跟踪服务以控制如何收集、存储和显示跟踪结果。

关键的几个选项:

1localOnly 默认为false。这个很好理解。如果为true,只在本地输出跟踪信息。

2enabled 是否启用跟踪。

3pageOutput 指定在每一页的结尾是否呈现跟踪输出。如果是 false ,则只能通过跟踪实用工具访问跟踪输出。

4requestLimit 指定在服务器上存储的跟踪请求的数目。最大为10000,默认为10

5traceMode 指定显示跟踪信息的顺序。SortByCategory SortByTime(默认)

关于更多可以参考

http://msdn.microsoft.com/zh-cn/library/6915t83k%28VS.80%29.aspx

下面以一个小Demo来说明怎么使用Trace.axd来调试ASP.NET

1. 建立一个Web项目,取名为WebTraceTest

2. 编辑web.config文件,添加trace节点(在)

内容如下:

<trace enabled=”true” localOnly=”true”

pageOutput=”true”

requestLimit=”15″

mostRecent=”true” />

3. 新建一个页面,取名为Test.aspx,在里面增加一个文本框和一个按钮(都是服务器端的控件)

按下F5,开始调试,会发现出现如下界面

5. 在文本框中输入文字,如Alexis,点击按钮,会发现Form Collection中会有详细的信息,如下:

说明:使用Trace.axd我们可以获得以下信息:

Request Details:请求的详细信息

Trace Information:跟踪信息

Control Tree:控件树

Session State:会话状态

Application State:应用程序状态

Request Cookies Collection:请求Cookie集合

Response Cookies Collection:响应Cookie集合

Headers Collection:标头集合

Response Headers Collection:响应标头集合

Form Collection:窗体集合

Querystring CollectionQueryString集合(即Url中?后面的字符串的信息)

Server Variables:服务器变量

Visual Studio与一个运行中的进程连接

当你按下F5对程序开始调试时,VS.NET会对项目进行生成(如果有必要的话)并以调试模式启动程序。也就是说,只要项目位于debug版本的程序集中,VS.NET就与运行得程序之间建立了连接,以便对断点等与调试相关的方法作出反应。

不过有些时候,我们需要或者想要对正在运行得Visual Studio之外启动的进程进行调试。当进程位于debug版本的程序集中,这是可以做到的。

1. 选择“工具—>调试进程”列出所有正在运行得程序,如下图

2. 选择自己感兴趣的进程,点击连接,此时Visual Studio自动切换到了调试模式。

3. 打开Progress窗口,发现我们刚刚选择的进程在列表中,如下图

这一技巧可以让你对Windows服务进程进行调试。编写Windows服务进程时,你无法按F5启动调试,因为它们必须先通过管理工具安装后启动才能运行。如果你在调试模式下生成并安装服务程序,就可以使用这一技巧进行调试。

而且你可以对SQL存储过程使用同样的方式进行调试。如果你安装了SQL Server调试组件,并且有足够的权限,就可以连接到SQL Server的进程,并在服务器中为存储过程设置断点来一步步执行。

调试Visual Studio中的多个项目

在实际开发中,我们往往分了许多层,有许多的项目集合在一个解决方案下。我们可以右击要调试的项目选择“调试–> 运行新实例”来实现调试这个项目。我也可以右击解决方案,选择多项目调试,如下图

我们还可以设置项目的期待顺序。在客户端/服务器(CS结构)程序中,我们可以使用这一方法来确保服务器端程序在客户端程序之前运行。

只在特定类型的异常时中断

一个健壮的程序会在运行时处理所有可能出现的异常。不过开发者在调试复杂的程序时会觉得这样有些麻烦。因为所有的异常都被处理掉了。在出现任何异常时,Visual Studio不会再进行处理,或者中断代码来对用户作出提示。

幸运的是Visual Studio有个选项可以让开发者指定他们关心的异常类型。选择菜单栏à调试à异常,或者使用快捷键Ctrl+Alt+E。如下图

我们可以看到一个树状结构列出所有VS可以监视到的异常。

后面的两个勾选框的意思分别为是否被抛出和用户是否不处理。

参考:

Visual Studio.NET使用技巧手册》

http://msdn.microsoft.com/

[转载]C#调试心经

mikel阅读(1233)

[转载]C#调试心经 – Alexis – 博客园.

我们在做程序开发时,难免会遇到错误异常。如何快速地找到出错的地方、分析错误的原因以及找到解决问题的方案,是许多初级程序员困扰的问题,这也正是经验的宝贵之处。 下面我将简单介绍在Visual Studio中调试以及一些高级的调试和常见的错误。

PS:如无特别说明Visual Studio均指Dev10Visual Studio 2010

入门篇

假设你是有着.Net平台的程序员,并且使用Visual Studio 做为开发工具。

断点:最简单的一种,设置一个断点,程序执行到那一句就自动中断进入调试状态。设置断点,在你觉得有问题的代码行,左侧单击,会出现红色的红点即断点。

启动调式:按F5,或者菜单栏调式开始调试,或者工具栏的图标

快速监视:快速查看变量或者表达式的值,也可以自定义表达式进行计算

单步执行

有三种,一种是每次执行一行(F10);一种是每次执行一行,但遇到函数调用就会跳到被调用的函数里(F11);一种是直接执行当前函数里剩下的指令,返回上一级函数(Shift+F11)。

还有一种后悔药,设为下一句(Set Next Statement),即下一句会被执行的语句(右击设置或者快捷键:Ctrl+Shift+F10),但要注意在调试与数据有关的时候,设置下一句有可能会报异常。如在调试向DataTable中添加行的时候,已经存在的行不能重复被添加到DataTable中。

监视

调试器可能会自动列出一些相关变量的值,但是你可能还关心其它变量的值,可以添加对这些变量的监视。还可以监视一个表达式的值,比如a+b。但是,这个表达式最好不要修改变量的值,比如监视a++都会导致监视时修改了a的值,影响了程序的运行结果。

调试技巧篇

使用快捷键会大大提升我们的调试效率,常用的调试快捷键:

F5 启动调试

F10 执行下一行代码,但不执行任何函数调用。

F11 在执行进入函数调用后,逐条语句执行代码。

Shift + F11 执行当前执行点所处函数的剩余行。

Shift + F5 停止运行程序中的当前应用程序。可用于“中断”模式和“运行”模式。

拖动断点(感谢 圣殿骑士的提醒)

在调试中,我们可以拖动断点,使得程序运行到我们想要运行的地方。通常是用来验证这段代码对程序的运行结果有没有影响的。因为我们拖动代码,则被过滤的代码就不会执行,将它跟原来的相比,可以看出去掉这段代码有什么影响

条件中断

假如你写了个for循环,而且循环的次数比较多,如下代码,现在我们知道在i=50的时候会有异常,那我们不可能按50F5去调试这代码,不然这效率….

private void ConditionDebug()
{
for
(int i = 0; i < 100; i++)
{
if
(i==50)
{
//some error code here
Console
.WriteLine(“i=50 here”);
}
}
}

我们可以直接利用vs提供的功能修改变量i的值,一开i=0,即刚进入for循环中,我们设置将i改为49并回车,再调试一次,会发现i=50 如下图

当然我们也可以直接在代码里写代码以达到这个目的,代码如下

private void ConditionDebug()
{
for (int i = 0; i < 100; i++)
{
System.Diagnostics.Debug.Assert(i != 50);
if (i==50)
{
//some error code here
Console.WriteLine(“i=50 here”);
}
}
}

使用了调试中的Assert(断言),当执行程序后会弹出如下的提示框,点击Ingore(忽略)即可,会发现此时i已经为50了,有兴趣的朋友可以看看Assert的其他用法。

Immediate Window

Immediate window在调试的时候计算表达式的值、执行语句、打印变量的值等。我们输入命令(注意一定要以“>”开头),会有智能提示,而且命名都是自解释型。

如,我们现在想要知道i的值,可以输入命名>Debug.Print i(也可以简单的使用>? i),如下图

Immediate window还有更强大的用法,计算方法的返回值(如果有的话)

如果有这个的函数

int MethodValue(int a)

{

if (a==1)

{

return 1;

}

else

{

return 0;

}

}

我们可以使用Immediate命令 >? class.Method(args) 去调用这个方法,如下图

其中p是当前类的实例(因为MethodValue是类的方法,注意?和表达式之间要有空格)


对于一些实时性很高的程序(如socket)使用 Debug.Write()把错误写到日志文件中,.Net可以将Debug信息写到你指定的文件中,记住,写进出的信息不一定是出错的信息,也可以是你的程序的运行的一些重要信息,当你调试过程中发现某个模块出了问题,但是不能决定位置,那你就可以使用这个方法,如果是一天才出一个错误,那你就更要使用这个方法。

实例篇

涉及到WSWebServices)的调试

在基于WinForm的实际开始开发中,我们往往采用WS用做数据的传递,我们在前台获取收集数据,通过WS将数据传递给后台,后台做相应的业务逻辑处理后,会持久到数据库中。而往往我们又会在WS中写一些相关的代码,如身份验证、日志记录、提示信息等,怎样去调试这些代码呢。

涉及到JavaScript的调试

许多程序员为调试JavaScript感到困惑不已,因为没有一款很好的调试工具。一些人喜欢使用FireBug来调试JavaScript,确实是一个不错的选择,Firebug提供了许多的JavaScript信息,是一款不错的调试JavaScript的工具。下面我将会介绍如何使用Visual Studio调试JavaScript,Visual Studio中调试JS跟调试C#差不多,都是设置断点,不同的是我们在查看元素值的时候需要注意点。

涉及到Ajax的调试

现在ajax已经十分的流行,但是随之而来的即调试困难,大部分初级程序员不知道如何有效地从前台调试到后台代码,以至出了很多不完善的ajax应用。

下面以一个简单的实例来介绍如何使用Visual Studio调试JavaScript。实例是使用Ajax验证用户登录,如果验证通过,则提示“登录成功”,否则提示“登录失败”。

下面是主要的代码,我们使用JQuery来实现ajax,并且在后台文件中故意出错。

正确的用户名和密码是admin1

调试方法如下,在后台入口处设置断点,然后在前台js中调用后台的方法处设置断点,然后按F5启动调试,当我们输入用户名、密码后,点击登录后会发现,前台断点被触发了。

F5继续调试,有时候会跳到JQuery的源码中,不管他,继续F5,会发现执行到后台中的断点中,如下图

而后台代码的调试是十分简单的。(PS:有时候无需在前台设置断点也可直接进入后台的调试,如何不行的话,在前台html文件或者aspx文件中认为有可能出错的地方设置断点,一步步调试)

一些调试中出现的常见错误(会陆续更新):

1. 我们调试到某一句代码的时候,突然莫名奇妙的跳出来了,其实是刚刚执行的这一句话有异常,我们可以使用try…catch进行异常捕获,看看异常原因是什么,然后做相应的处理

2. ADO.NET,我们会使用ds.Merge()方法进行合并内存表,如果有异常的话,一般有以下三种情况:

A.其中一张表中有两行一模一样的数据,包括主键

B.这两张表的结构不一致

C.两张表中某个字段的类型不匹配,如字段ageA表中式string,而在B表中确是Decimal