[转载]ASP.NET MVC3 On Razor使用(2):自定义Helper方法

mikel阅读(993)

[转载]MVC3 On Razor使用(2):自定义Helper方法 – 信心、恒心、决心 – 博客园.

在使用自定义Helper方法时,开始我试了很多次都不成功一直报错“找不到WebMatrix.WebData.dll、WebMatrix.Data.dll”。

应该是一个bug,估计正式版不会有这个问题。解决方案是:

在使用自定义Helper时,需要引用默认C盘路径下的WebMatrix.WebData.dll、WebMatrix.Data.dll类库并Copy到本地,否则会出错。

clip_image002

如上图所示,首先在网站根目录下添加一个“App_Code”文件夹,并创建一个Razor的ViewPage为MyHelpers.cshtml。代码如下图所示:

clip_image004

通过@helper{}声明就可以定义一个Helper辅助函数,上述函数根据姓名循环输出一个列表。这种html和cs混合输出的helper相当简洁,相对于MVC2

写一个helper容易多了。

下面在页面中调用,只要一行代码就可以了:

clip_image006

同样可以在其他页面调用,输出结果如下:

image

注意的是,写法上将MyHelpers文件名加上,如果有文件夹则文件夹名字也要加上。使用规则是@FolderName.FileName.HelperName

[转载]ASP.NET MVC3 On Razor使用(1):基础部分

mikel阅读(1032)

[转载]MVC3 On Razor使用(1):基础部分 – 信心、恒心、决心 – 博客园.

微软在10号发布了ASP.NET MVC3的发布候选版(RC:Release-candidate),该版本提供了Razor视图引擎的智能感知等功能,并同样具备Online许可。

特意下载安装了下,下载地址:http://www.microsoft.com/downloads/en/details.aspx?FamilyID=a920ccee-1397-4feb-824a-2dfefee47d54&displaylang=en

试用了传说中强大的Razor引擎,实在是太棒了。下面是我的一些使用学习体会:

一、Razor基础简介

Razor采用了cshtml后缀的文件名,截图如下:

clip_image002

A、 版面布局

从图上看到,新的视图引擎已经没有了Site.Master这种MasterPage了,取而代之的是_Layout.cshtml和_ViewStart.cshtml。

_Layout.cshtml等同于MasterPage;如下图所示:

clip_image004

这个_Layout.cshtml文件中已没有了MasterPage中<asp:ContentPlaceHolder ID=”MainContent” runat=”server” />的标签语句了,取而代之的是

@RenderBody():呈现子页的主体内容

@RenderSection():呈现特别的节部分。

HelperResult RenderSection(string name, bool required = true);

required默认为true必须覆写,设为false则为可选覆写;

注意的是:该函数在RC版中参数有所改变,参数中optional改为required,据说和VB的关键字冲突

下图则为我在子页的页脚部分覆写,在子页实现时,使用@section 自定义节名{ }格式。

clip_image006

如果我们想在父页里写一个默认实现节,在子页作为可选实现该怎么做呢?

这个在MasterPage是直接支持的,但在Razor里就要稍显复杂些,个人觉得这种写法还可以改进下;

如下图所示,我在父页写了一个默认的页脚实现,如果子页有实现则使用子页实现,没有就用默认实现。

这里要先通过bool IsSectionDefined(string name)函数判断是否有子页实现,有则调用子页实现。

clip_image008

_ViewStart.cshtml:该页面可以理解为其它View的基类。它默认的代码就3行如下所示:

clip_image010

在该页面里定义了其它View的默认MasterPage是“~/Views/Shared/_Layout.cshtml”;

当然在这里还可以定义一些公用的函数及变量,以供其它页面可以直接调用。

B、与ASPX语法的区别

ASPX:<% %>

Razor:@{ }

这两种都是代码块的编写符号,只不过用Razor会让代码更加简洁和舒适,下图为对比语法:

clip_image002[4]

ASPX:<%= %>

Razor:@
在Razor中,你只需要用一个”@”字符就可以标识代码块的开始,它能够自动判断后面变量的结束位置,如下图对比看出,使用Razor语法,少写了不少代码。

clip_image004[5]

clip_image006[4]

而且,Razor解析器内置了很多语言的智能.
大部分情况下,Razor解析器都有足够的能力推导出模板里的一个“@”字符到底是在代码中用到,还是在静态内容中用到。

例如,我在下例中的邮件地址中使用了”@”字符,则scottgu@microsoft.com被解析为静态内容,而@DateTime.Now被解析代码执行了,很强大吧!
如果碰到和代码一样格式的内容(或者你想把代码当作内容看待),你可以显式地打@@来用另外一个”@”字符进行转义。

image

ASPX:<%: %>

Razor:@()

这两种语法都能实现内容的编码,只不过Razor更为方便一些,如下图所示:

clip_image008[4]

clip_image010[4]

结果输出:<span>文本编码</span>

C、其他的一些Razor语法

image
如上图所示,
@using :导入命名空间
@model:声明页面接收的Model
@section:定义要实现父页的节信息
这里列出的只是一些简单常用的,需要更多语法可以参考API文档。

二、MVC3 RC特性展示

相比较MVC2,版本3还是有很大的改进(除了Razor引擎)。

A、Partial Page Output Caching(部分页输出缓存)

ASP.NET MVC在第一版的时候就开始支持整页缓存。从ASP.NET MVC V3开始,我们也可以支持部分页缓存了。

这可以很容易的使你部分缓存或者一个response的片段。

例如我们要根据一个类别获取产品列表,当请求类别相同时,就输出缓存的产品列表。

如下图所示,Action上定义了输出缓存的过期时间1小时,缓存参数为category。

因此当发生重复请求时,就可以自动输出缓存信息避免频繁查询数据库,减轻服务器负担和提高响应速度:

image

image

B、隐式JavaScript和验证

其中一个比较出色的ASP.NET MVC3的改进是AJAX 和 Validation helpers将会默认使用unobtrusive JavaScript,ajax采用了JQuery来编写。

image

image

默认配置节里启用了unobtrusive JavaScript,故将上述js开启后,则验证就为客户端验证。

相比MVC2的ajax验证,不需要在View页里添加<%Html.EnableClientValidation(); %>这行代码了。

C、全局过滤器

ASP.NET MVC 支持通过过滤机制来描述性地应用“横切”逻辑。 你可以使用属性语法为控制器和执行函数指定过滤器,如下所示

image

但在使用中,我们常常希望将一些过滤器逻辑应用于程序中的所有控制器上,如Authorize过滤器。

现在ASP.NET MVC3 能够让你指定一个全局的过滤器,这个过滤器可以应用于程序中的所有控制器上。

如下图所示:在Global文件中将自定义过滤器加入GlobalFliterCollection中就可以了

image

在MVC3中,这个过滤器的判定逻辑非常灵活,你可以配置一个全局过滤器,使它只在某些条件符合的时候才启用。

如下图所示,我实现了一个全局权限过滤器,在任何页面如果没有登录的话则跳转到LogOn页面。因为是全局的,当跳转到LogOn页面时,

也要判断是否有权限,所以最终页面上什么都不显示。

那如何解决这个问题呢,让LogOn页面不应用该全局权限呢?

如下图所示:我们实现一个IFilterProvider,在里面过滤掉LogOn页面而应用于其他的页面。代码如下:

image

在使用Authorize过滤器时,发现了一个bug,就是当没有权限时,系统会自动跳到“Account/LogIn”页面,而不是跳转到webconfig中设置的页面。

<authentication mode=”Forms”>
<forms  loginUrl=”~/Account/LogOn” timeout=”2880″ />
</authentication>

这个设置好像不起作用了,通过查看ScottGu’s Blog,知道这是一个bug,解决方案就是在Configuration配置节下加入

<appSettings>
<add key=”autoFormsAuthentication” value=”false” />
</appSettings>

我想在正式版中不会再有这个问题了。

还有其他的一些新的特性,可以参考http://tech.it168.com/a2010/1111/1124/000001124749_1.shtml

[转载]在MVC2.0 中 遭遇无法被 Try Catch 的 “Exception”

mikel阅读(1104)

[转载]在MVC2.0 中 遭遇无法被 Try Catch 的 “Exception” – RyanDing – 博客园.

前天当我为新项目新增完日志模块后对日志模块进行测试,测试时居然发现开发人员一段非常简单的代码,而且很标准的try … catch .. 写法。代码如整理如下:

1 public JsonResult SaveTest() 2 { 3 try 4 { 5 //LinqToSql:返回IQueryable数据集合。 6   var iQueryableData = (from o in _Context.Orders//.Where(o => o.OrderID == 10248) 7 select new 8 { 9 ShipName = o.ShipName, 10 Employee = o.Employee, 11 }).ToList(); 12 13 //LINQ:返回IEnumerable集合。 14 var iEnumerableData = from d in iQueryableData 15 select new 16 { 17 ShipName = d.ShipName, 18 EmployeeName = d.Employee.LastName //空引用未处理引发程序异常。 19 }; 20 21 return Json(new { Success = true, Msg = iEnumerableData }, JsonRequestBehavior.AllowGet);   22 } 23 catch (Exception ex) 24 { 25 return Json(new { Success = false, Msg = ex.Message }, JsonRequestBehavior.AllowGet); 26 } 27 }

为方便大家阅读,我用 NORTHWIND 数据库。同时在该数据库内执行 SQL :update orders set EmployeeID =null where OrderID =10248 。这样造成上述代码第18 行Linq代码迭代时产生异常。您认为 这个异常可以被catch住么?答案当然是否定的!

当发现这个Bug后顿时让我产生了兴趣,我不知道如果微软的MVC项目开发人员看到这个bug,他是如何解释的。接下来让我就来一个非官方解释吧,欢迎拍砖!

我们可以在第18行设置好断点,然后用VS2010 Debug程序,发现执行到第 21 行 return Json 时 VS未报任何bug。过数秒后断点直接定位在第18行,提示也很简单:未将对象应用到实例。这个确实是我们预期想要的 Exception 但是它始终没有被 catch 住!通过StackTrace我发现了这么一句提示: at System.Web.Mvc.ControllerActionInvoker.InvokeActionResult(ControllerContext controllerContext, ActionResult actionResult) 。上述代码第21行return Json 被执行后返回了 JsonAction 这个 JsonAction 被 InvokeActionResult 调用?难道这个就是问题的关键所在?原因是写有try catch 函数(本例的SaveTest()函数)的外层调用函数(InvokeActionResult()函数)出现了异常。也就是外层函数出现异常我们的内层 函数SaveTest()又怎能try catch 到这个外层异常呢?于是通过Reflector进一步证实我的想法。具体的请看下图:

主要是这句action.ExecuteResult(ControllerContext);它的执行过程如下:

最后一幅图的JavaScriptSerializer serializer = new JavaScriptSerializer(); 进行 json 序列化时这个外层函数报出了异常。这个异常在我们的内层函数 SaveTest() 方法内是不可能被截获到的。此刻我已完全弄明白这个无法被catch 的exception 是怎么出现的!至于这个异常为什么会被外层函数触发,主要是.net Linq 延迟加载机制导致的,这不属于本文讨论的范畴,具体的园子内很多文章已经解释的很清楚了。

为消灭掉这个截获不到的exception现附上解决方案:

MVC虽然很年轻,但是这个架构真的很优秀我们可以非常方便对其扩展(我不是MVC的托,呵呵)。为了解决问题,我们只要自定义一个特性该特性继承 HandleErrorAttribute  特性即可。接着重写基类的 OnException 虚方法。代码如下:

CustomHandleErrorAttribute

public class CustomHandleErrorAttribute : HandleErrorAttribute { public override void OnException(ExceptionContext Context) { base.OnException(Context); dynamic ex = Context.Exception; if (!Context.ExceptionHandled) return; //TODO:将 ex 错误对象记录到系统日志模块 } }

接着我们在MVC Controller 内这样调用:

CustomHandleError调用

/* *截获InvokeActionResult 调用 actionResult参数的 ExecuteResult 方法。 ExecuteResult 方法执行时 进行 以下操作: JavaScriptSerializer serializer = new JavaScriptSerializer(); response.Write(serializer.Serialize(this.Data)); 将C# 匿名对象序列化成json数据供 jquery ajax 方法回调。 */ [CustomHandleError] public JsonResult SaveTest() { //try //{ //TestMehod(); //LinqToSql:返回IQueryable数据集合。 var iQueryableData = (from o in _Context.Orders//.Where(o => o.OrderID == 10248) select new { ShipName = o.ShipName, Employee = o.Employee, }).ToList(); //LINQ:返回IEnumerable集合。 var iEnumerableData = from d in iQueryableData select new { ShipName = d.ShipName, EmployeeName = d.Employee.LastName //空引用未处理引发不可截获的异常。 }; return Json(new { Success = true, Msg = iEnumerableData }, JsonRequestBehavior.AllowGet); //Json序列化。 //} //catch //外层错误,导致内层函数catch失效,无法有效的截取错误信息。 //{ // return Json(new { Success = false, Msg = "操作失败。" }, JsonRequestBehavior.AllowGet); //} }

代码看上去更简洁了吧?连try catch 都不用写了更不用担心啥时候这个截获不到的exception神秘人物登场,这样噩梦结束了。。。

本例代码这里下载

[转载]CMS系统模板引擎设计(5):Label应用初探

mikel阅读(924)

[转载]CMS系统模板引擎设计(5):Label应用初探 – 氣如蘭兮長不改,心若蘭兮終不移。 – 博客园.

话说上节 听的很郁闷,讲的也郁闷,整个系列没有详细的Code,所以都有点懵。其实仔细看之前的几节应该还是能够理解整个思路的。 我在这里再整理一遍: 用户访问URL后 ->根据所有Page的URL规则(urlpattern)获取当前Page,然后再根据page的Template,Find出所有的 Label(通过反射),然后遍历这些Label,把得到的数据的Html替换掉Label本身的标签代码。最后整个Template就是要生成的 HTML了。
所以我们 要明白Label是如何获取工作才能获取数据库的数据的。一个Label可以理解成一个控件,那么控件必然会支持一些属性(Parameter)和一些内 容(Field)。 我们上节就是在讲怎么来解析parameter,因为有些特殊的parameter,所以设计的时候设计了parameter的基类,特殊的则是子类。
同样,field是具体的要展现在HTML代码中的字段,比如中的[field:Title/],这就是一个字段,我们的模版引擎的工作就是把他替换掉应该展现的标题,而如何才能工作?我们就得设计Field的整个逻辑。在替换循环的过程中使用field类。
但是,我今天不讲Field了,因为这样讲大家还是糊涂依旧。今天我们就来设计一个Article:List的初级版。我觉得或许从实例讲解大家更容易理解设计的理念。OK,那就开始设计一个Article.List,我们最熟悉的文章列表。

//简单的循环列表
{Article:List Top=”10″ CategoryId=”5″}
<a href =”/details/[field:FileName/]” target=”_blank”>[field:Title/]</a>

{/Article:List}

想象一下Repeater,有个ItemTemplate,那么对于List这个Label来说,他的ItemTemplate显然就是Template属性。那么如果我们获取到数据源后直接foreach替换掉所有的field即可。代码大概如下:
/// <summary>
/// 获取要展示的HTML代码
/// </summary>
/// <returns></returns>
public override string GetRenderHtml()
{
var html
= TemplateString;
foreach (var article in GetDataSource())
{
foreach (var field in Fields)
{
html
= html.Replace(field.Html, field.GetValue(article));
}
}
return html;
}

从上面的方 法中,我们可以看到替换的机制是每一行数据都要执行一次所有字段的替换(所以之前有提过在构造嵌套的时候为了防止Field混乱要处理 TemplateString),最后返回html。我们还能看到一些未知的方法和字 段:GetDataSource(),Field.Html,Field.GetValue(),这些已经暴露了我们的Field设计的部分内容。我们先 看GetDataSource()是什么?

代码

/// <summary>
/// 获取Article列表
/// </summary>
/// <returns></returns>
private IEnumerable<Article> GetDataSource()
{
var parameter
= new ArticleQueryParameter();
//构造查询的参数

return ArticleDataHelper.GetList(parameter);
}
/// <summary>
/// 查询参数基类
/// </summary>
public class QueryParameter
{
public int PageSize { get; set; }
public int PageIndex { get; set; }
public int RecordCount { get; set; }
public string SearchKey { get; set; }
}
/// <summary>
/// 文章查询类
/// </summary>
public class ArticleQueryParameter
{
public QueryParameter PageParameter { get; set; }
public DateTime PostTime { get; set; }
public int CategoryId { get; set; }
public int Top { get; set; }
}
/// <summary>
/// 文章获取数据类
/// </summary>
public class ArticleDataHelper
{
public static IEnumerable<Article> GetList(ArticleQueryParameter parameter)
{
return null;
}
}

其 实就是获取ArticleList的数据源,具体的实现方式大家可能都不一样,但Article.List最终需要这么一个数据获取的方法,然而这个方法 都需要接受一些查询条件的参数,这些参数都来自Parameters!!现在我们来填充GetDataSource()的参数构造部分。

private IEnumerable<Article> GetDataSource()
{
var parameter
= new ArticleQueryParameter();
//构造查询的参数
parameter.CategoryId = Parameters[CategoryId].ConvertToInt(0);
parameter.Top
= Parameters[Top].ConvertToInt(Parameters[PageSize].ConvertToInt(0));
var pageIndex
= Parameters[PageIndex].ConvertToInt(1);
if (pageIndex > 1)
{
parameter.PageParameter
= new QueryParameter
{
PageIndex
= pageIndex,
PageSize
= parameter.Top
};
}
return ArticleDataHelper.GetList(parameter);
}

Parameters 是Label的ParameterCollection,他可以通过索引直接访问具体的parameter。ConvertTo<T>(T defaultValue)是可以将parameter的value转成T类型。 这就是Parameter所用到的地方之一。另外可以看到Field具体Html属性和GetValue方法,而且GetValue接受了当前 Article实体作为参数(不接受参数的话,我们怎么得到某个字段的值呢:)。

整 个List流程应该比较清楚了吧,获取数据源,然后循环数据,每行再去替换所有的Field,最后把拼接好的HTML返回。当然这是List,如果是其他 的标签可能就是另外一个处理办法。比如System.Include标签,他的工作就是嵌入一个用户控件(PartialTemplate),那么他的处 理逻辑和List就完全不一样(他是先根据templateid参数的值获取template,然后再把自己所有的Parameters传递给这个 template里的所有标签,最后再把这个template替换后的结果作为自己的结果返回,他没有循环)。所以我们的具体控件逻辑都是大相径庭的,但 最终都是要返回替换后的HTML,但所有的List却都是差别多的,无非就是不同的数据源进行循环。所以对于List我们应该进行抽象,把公共部分提取出 来,尽量让每个具体的Label更明确职责。如何抽象呢? 那就看看有没有可提取的公共部分。

所 有的List都可能会有分页,所以ListBase应该有PageParameter,所有的List都会去循环DataSoruce,所以 ListBase默认实现了DataSource循环,但是增加了一个方法那就是GetDataSource。这个方法是抽象的,所有的List必须实 现。

代码

/// <summary>
/// 循环标签基类
/// </summary>
public abstract class ListBase : Label
{
public QueryParameter PageParameter { get; set; }

public abstract IEnumerable<dynamic> GetDataSource();

public override string GetRenderHtml()
{
var dataSource
= GetDataSource();
if (dataSource == null) return string.Empty;
var html
= TemplateString;
foreach (var dataItem in dataSource)
{
foreach (var field in Fields)
{
field.Data
= dataItem;
html
= html.Replace(field.Html, field.GetValue());
}
}

return html;
}
}

foreach里我也做了点细微的调整,就是把Field的GetValue的参数拿掉了,换成了成员,这样更明白些。你可能会有一些疑点:

为什么设计为抽象而不是虚方法或接口?

所有子类的实现方法都不一致,没有可提取部分,所以虚函数没有意义,如果单独抽象成接口,则所有子类必须继承此接口,因为GetRenderHtml和该方法紧密结合,foreach里需要显式转换为接口才能调用,完全没有意义。

为什么是GetDataSource方法,而不是公开一个DataSource成员? 如果需要Set呢?还要增加一个SetDataSource?

其 实这个我考虑过,很少有Set的情况,因为标签都是自动生成的没有外部去干扰(Set),但不能否认以后完全没有,如果设为成员,则必须有一个可get的 地方,要么是abstract,那样也会把set abstract,要么就在Init里给set先,那也得有一个抽象的set方法。所以考虑现状还是使用一个方法最为合适。

另外一点就是为什么用了dynamic,而不是T。

首 先不能是T,如果是T,则GetRenderHtml调用时也需要指明T,则整个ListBase就要变成泛型类ListBase<T>,除 非base不执行GetDataSource调用。为什么不能用ListBase<T>?因为有些GetDataSource会用linq返 回匿名类型集合,子类无法确定返回的具体类型名称,所以就不能继承ListBase<T>。但我们可以用dynamic,动态类型,到真正执 行时可以确定T就行,这个不用我们操心,然而object显然略逊一筹了。

这样一来,Article的List只需要实现GetDataSource就行了。

这只是最简单的List雏形,假如说我还需要像Repeater控件那样,有headtemplate itemtemplate foottemplate altertemplate spacetemplate怎么办?

这 个就需要定义子标签类了。这里我就不多说了,其实很简单,就是再定义几个Label,他们又各自的获取Html的方法,我们最后组合起来就行。自需要注意 List的Template和Field已经没了,都属于子标签了。而且像交替执行的(Item和Alter)需要再循环里给他们隔行赋值。下面是我以前 写的代码,虽然比较难看,不太OO,但能说明实现的逻辑:

代码

public class ListLabelBase : LabelBase
{

public LabelHtmlAttrs HtmlAttrs { get; set; }

public PageParameter Page { get; set; }

public ListTemplate ItemTemplate { get; set; }
public ListTemplate AlterTemplate { get; set; }
public ListTemplate HeadTemplate { get; set; }
public ListTemplate FootTemplate { get; set; }
public ListTemplate NullTemplate { get; set; }
public SpaceTemplate SpaceTemplate { get; set; }

public int Rows { get; set; }

public override void Init()
{
HtmlAttrs
= new LabelHtmlAttrs(Attrs);
Rows
= Attrs.GetAttribute(Rows, 0);
InitTemplates();
Page
= new PageParameter
{

PageIndex = Attrs.GetParameter(PageIndex, 1),
PageSize
= Attrs.GetAttribute(PageSize, Rows),
OrderBy
= Attrs.GetAttribute(Sort, string.Empty),
IsASC
= Attrs.GetAttribute(asc, false)
};

Fields.SetAllBeginReplace((field, obj) =>
{
if (obj != null)
{
var tmp
= ((ListItem)obj).Item.GetPropertyValue(field.Name);
field.Value
= tmp == null ? string.Empty : tmp.ToString();
}
});

Fields[ItemIndex].SetBeginReplace((field, obj) =>
{
field.Value
= obj == null ? string.Empty : (((ListItem)obj).Index).ToString();
});

//实例化PageField
Fields.OverrideField(Page, page => new PageField
{
GetRecordCount
= () => Page.RecordCount,
PageSize
= Page.PageSize,
PageIndex
= Page.PageIndex,
Attrs
= page.Attrs,
Template
= page.Template
});
}

public virtual void InitTemplates()
{
var matches
= Regexs.ListTemplatePattern.Matches(Template);

foreach (Match m in matches)
{
var template
= m.Groups[template] == null ? string.Empty : m.Groups[template].Value;
var value
= m.Groups[value] == null ? string.Empty : m.Groups[value].Value;
var html
= m.Groups[0].Value;
if (string.IsNullOrEmpty(html))
continue;
switch (m.Groups[name].Value)
{
case item:
ItemTemplate
= new ListTemplate { Content = template, Html = html };
break;
case alter:
AlterTemplate
= new ListTemplate { Content = template, Html = html };
break;
case space:
SpaceTemplate
= new SpaceTemplate { Content = template, Html = html, Length = value.ToInt(1) };
break;
case head:
HeadTemplate
= new ListTemplate { Content = template, Html = html };
break;
case foot:
FootTemplate
= new ListTemplate { Content = template, Html = html };
break;
case null:
NullTemplate
= new ListTemplate { Content = template, Html = html };
break;
}
}

if (ItemTemplate == null && !String.IsNullOrEmpty(Template))
{
ItemTemplate
= AlterTemplate ?? new ListTemplate { Content = Template, Html = Template };
return;
}
}

/// <summary>
/// 获取替换后的Html
/// </summary>
public virtual string GetListContent<T>(IEnumerable<T> dataSoruce)
{
if (dataSoruce == null || dataSoruce.Count() == 0)
return string.Empty;
var list
= dataSoruce.ToList();
var firstItem
= list.Count == 0 ? default(T) : list[0];

#region HeadTemplate
if (HeadTemplate != null)
{
var headContent
= ReplaceTemplate(HeadTemplate.Content, new ListItem { Index = 0, Item = firstItem });
Template
= Template.Replace(HeadTemplate.Html, headContent);
}
#endregion

#region FootTemplate

if (FootTemplate != null)
{
var footContent
= ReplaceTemplate(FootTemplate.Content, new ListItem { Index = 0, Item = firstItem });
Template
= Template.Replace(FootTemplate.Html, footContent);
}

#endregion

#region ItemTemplate
if (ItemTemplate != null)
{
//替换循环
var listContent = string.Empty;
if (list.Count > 0)
{

var itemIndex = (Page == null ? 0 : (Page.PageIndex 1) * Page.PageSize) + 1;
if (HtmlAttrs.Cols > 1)
{

listContent += HtmlAttrs.PanelBegin;
var rowNumber
= 0;
foreach (var model in list)
{
var tmpTemplate
= ItemTemplate.Content;
if (AlterTemplate != null && rowNumber % 2 == 0)
tmpTemplate
= AlterTemplate.Content;

listContent += HtmlAttrs.RowBegin;
for (var c = 0; c < HtmlAttrs.Cols; c++)
{
listContent
+= HtmlAttrs.CellBegin;
listContent
+= ReplaceTemplate(tmpTemplate, new ListItem { Item = model, Index = itemIndex });
listContent
+= HtmlAttrs.CellEnd;
itemIndex
++;
}
listContent
+= HtmlAttrs.RowEnd;
rowNumber
++;
}
listContent
+= HtmlAttrs.PanelEnd;
}
else
{
foreach (var model in list)
{
var tmpTemplate
= ItemTemplate.Content;
if (AlterTemplate != null && itemIndex % 2 == 0)
tmpTemplate
= AlterTemplate.Content;

listContent += ReplaceTemplate(tmpTemplate, new ListItem { Item = model, Index = itemIndex });

if (SpaceTemplate != null && itemIndex % SpaceTemplate.Length == 0)
listContent
+= SpaceTemplate.Content;
itemIndex
++;
}
}
if (NullTemplate != null)
Template
= Template.Replace(NullTemplate.Html, string.Empty);
}
else
listContent
= NullTemplate == null ? string.Empty : NullTemplate.Content;

Template = Template.Replace(ItemTemplate.Html, listContent);
}

#endregion

if (NullTemplate != null)
Template
= Template.Replace(NullTemplate.Html, “”);

return Template;
}
}

public class ListItem
{
public object Item { get; set; }
public int Index { get; set; }
}

今天就讲到这了,不知道还有朋友有兴趣没有,目前还没有演示,或许某天我会放出个demo源码。 下次讲Field的设计吧,这也算是最后一个设计了。

[转载]hashtable详细介绍

mikel阅读(862)

[转载]hashtable详细介绍 – YangLei’s – 博客园.

Hashtable的定义

表示键/值对的集合,这些键/值对根据键的哈希代码进行组织。

Hashtable存储结构如下

Hashtable是非泛型的集合,所以在检索和存储值类型时通常会发生装箱与拆箱的操作。

当把某个元素添加到 Hashtable 时,将根据键的哈希代码将该元素放入存储桶中,由于是散列算法所以会出现一个哈希函数能够为两个不同的键生成相同的哈希代码,该键的后续查找将使用键的哈希代码只在一个特定存储桶中搜索,这将大大减少为查找一个元素所需的键比较的次数。

Hashtable 的加载因子确定元素与Hashtable 可拥有的元素数的最大比率。加载因子越小,平均查找速度越快,但消耗的内存也增加。默认的加载因子 0.72通常提供速度和大小之间的最佳平衡。当创建 Hashtable 时,也可以指定其他加载因子。

元素总量/ Hashtable 可拥有的元素数=加载因子

当向 Hashtable 添加元素时,Hashtable 的实际加载因子将增加。当实际加载因子达到指定的加载因子时,Hashtable 中存储桶的数目自动增加到大于当前 Hashtable 存储桶数两倍的最小素数。

扩容时所有的数据需要重新进行散列计算。虽然Hash具有O(1)的数据检索效率,但它空间开销却通常很大,是以空间换取时间。所以Hashtable适用于读取操作频繁,写入操作很少的操作类型。

代码一、

static void Main(string[] args) { Hashtable hashtb = new Hashtable(); hashtb.Add(1, "aa"); hashtb.Add(2, "bb"); hashtb.Add(3, "cc"); hashtb.Add(4, "dd"); foreach (DictionaryEntry item in hashtb) { Console.WriteLine(item.Value); item.Value = "ee"; } Console.Read(); }

编译出错:item为foreach的迭代变量,无法修改其成员。

原因:如果运行foreach处理语句试图修改迭代变量值,或将变量值作为ref参数或out参数传递,那么都会发生编译错误,迭代变量相当于一个局部只读变量。

代码二、

item.Value = “ee”;改成hashtb[item.Key] = “ee”;

运行报错:集合已修改;可能无法执行枚举操作。

原因:.NET Framework 提供枚举数作为循环访问一个集合的简单方法。枚举数只读取集合中的数据,无法用于修改基础集合。

foreach 语句用于循环访问集合,以获取您需要的信息,但不能用于在源集合中添加或移除项,否则可能产生不可预知的副作用。如果需要在源集合中添加或移除项,请使用 for 循环。

代码三、

Thread tr1 = new Thread(new ThreadStart(() => { foreach (DictionaryEntry item in hashtb) { Console.WriteLine(item.Value); } })); tr1.Start(); Thread tr2 = new Thread(new ThreadStart(() => { for (int i = 1; i < 4; i++) { hashtb[i] = "ee"; Console.WriteLine(hashtb[i]); } })); tr2.Start();

线程tr1用来读hashtable,线程tr2用来foreach来枚举修改hashtable,

运行时错误:集合已修改;可能无法执行枚举操作。

代码四、

//读取 Thread tr1 = new Thread(new ThreadStart(() => { for (int i = 1; i <= 4; i++) { Console.WriteLine(hashtb[i]); } })); tr1.Start(); Thread tr2 = new Thread(new ThreadStart(() => { for (int i = 1; i <= 4; i++) { Console.WriteLine(hashtb[i]); } })); tr2.Start(); //修改 Thread tr3 = new Thread(new ThreadStart(() => { for (int i = 1; i <= 4; i++) { hashtb[i] = "ee"; } })); tr3.Start();

运行结果:正常

说明:Hashtable 是线程安全的,可由多个读取器线程和一个写入线程使用。多线程使用时,如果只有一个线程执行写入(更新)操作,则它是线程安全的,从而允许进行无锁定的读取(若编写器序列化为 Hashtable)

代码五、

Thread tr1 = new Thread(new ThreadStart(() => { lock (hashtb) { foreach (DictionaryEntry item in hashtb) { Console.WriteLine(item.Value); } } })); tr1.Start(); Thread tr2 = new Thread(new ThreadStart(() => { lock (hashtb) { for (int i = 1; i <= 4; i++) { hashtb[i] = "ee"; } } })); tr2.Start();

运行结果:正常

说明:由于两个线程里面都加了lock (hashtb)把hashtable锁住,所以是线程安全的。

代码六、

Thread tr1 = new Thread(new ThreadStart(() => { //锁住 lock (hashtb) { foreach (DictionaryEntry item in hashtb) { Console.WriteLine(item.Value); } } })); tr1.Start(); Thread tr2 = new Thread(new ThreadStart(() => { //未锁住 for (int i = 1; i <= 4; i++) { hashtb[i] = "ee"; } })); tr2.Start();

运行错误:集合已修改;可能无法执行枚举操作。

说明:从头到尾对一个集合进行枚举本质上并不是一个线程安全的过程。即使一个集合已进行同步,其他线程仍可以修改该集合,这将导致枚举数引发异常。若要在枚举过程中保证线程安全,可以在整个枚举过程中锁定集合,或者捕捉由于其他线程进行的更改而引发的异常。

Hashtable 提供的线程安全方法

Hashtable的Synchronized静态方法提供线程安全的实例,如下:

Hashtable ht = Hashtable.Synchronized(new Hashtable());

内部实现如下:

public override void Add(object key, object value) { lock (this._table.SyncRoot) {   this._table.Add(key, value); } }

按输入方式输出

因为hashtable内部是无序的,所以输出不一定,hashtable取数据的机制没搞明白。按照下面代码可以实现先进先出。

可以通过控制ArrayList里面keys的排序来控制hashtable的输出,当然也可以用SortedDictionary和SortedList实现排序集合。

public class NoSortHashtable : Hashtable { private ArrayList keys = new ArrayList(); public NoSortHashtable() { } public override void Add(object key, object value) { base.Add (key, value); keys.Add (key); } public override ICollection Keys { get { return keys; } } public override void Clear() { base.Clear (); keys.Clear (); } public override void Remove(object key) { base.Remove (key); keys.Remove (key); } public override IDictionaryEnumerator GetEnumerator() { return base.GetEnumerator (); } }

[转载]如何将ASP.NET MVC2项目升级到MVC 3 RC

mikel阅读(832)

[转载]如何将ASP.NET MVC2项目升级到MVC 3 RC – 紫色永恒 – 博客园.

微软在10号发布了ASP.NET MVC3的发布候选版(RC:Release-candidate),该版本提供了Razor视图引擎的智能感知等功能,并同样具备Online许可。眼馋的同学是不是已经迫不及待的想将现有项目升级呢?这里我就将官方ReleaseNotes中的升级办法翻译总结一下,以方便那些不喜欢看罗里啰嗦的英文文档的广大同学。

译文

要将现有的ASP.NET MVC2项目手动升级到ASP.NET MVC3(RC),我们需要如下这些步骤:

1. 随便创建一个新的ASP.NET MVC3项目,我们在升级中将用到其中包含的一些文件。

2. 从新建项目中将如下这些文件copy到现有的ASP.NET MVC2项目中,它们是:

· /Scripts/JQuery.unobtrusive-ajax.js

· /Scripts/JQuery.unobtrusive-ajax.min.js

· /Scripts/JQuery.validate.unobtrusive.js

· /Scripts/jQuery.validate.unobtrusive.min.js

· /Views/Web.config

3. 如果你现有的ASP.NET MVC2项目中包含若干area,则需要将/Views/Web.config文件copy到所有的area下替换原文件。

4 在项目的Web.config文件中(根目录下的Web.config中有三处,Views文件夹下的Web.config文件中有4处)分别搜索和替换如下内容:

System.Web.Mvc, Version=2.0.0.0 → System.Web.Mvc, Version=3.0.0.0

5. 在解决方案浏览器中删除System.Web.Mvc的引用(因为这里引用的是ASP.NET MVC2中大的DLL)然后添加一个System.Web.Mvc (v3.0.0.0)的引用。接下来再分别引用System.WebPages.dll和System.Web.Helpers.dll。

6. 在解决方案浏览器中鼠标右击项目名称,并选择卸载项目。接着再次右击项目名称然后选择编辑ProjectName.csproj。

7. 在ProjectName.csproj中找到ProjectTypeGuids节点,将 {F85E285D-A4E0-4152-9332-AB1D724D3325}替换为{E53F8FEA-EAE0-44A6-8774-FFD645390401}。

8. 保存第7步中的更改后右击项目,重新加载它。

9. 在应用程序根目录下的Web.config中的assemblies节点添加如下两条

image

10. 如果项目中引用了其他使用到ASP.NET MVC2中的dll协同编译的第三方类库,则需要做如下改动

image

大功告成,尽情享受ASP.NET MVC的魅力吧。

[转载]Java实现导入Excel

mikel阅读(1103)

[转载]Java实现导入Excel – JAVA之恋 – 博客园.

上班的时候公司要求做一个从网页上导入excel,研究了半天后,开始着手去实现它。

思路很简单:

1、做一个jsp页面,页面包括浏览文件,提交文件

2、将excel文件上传到服务器

3、  服务器对该excel文件进行读出

4、  将excel文件内容显示到页面上

环境搭建:

需要准备的包:commons-fileupload-1.2.1.jar & commons-io-1.3.2.jar 这两个包是上传用的

jxl.jar 这个包是读取excel用的 下载地址 :http://sourceforge.net/projects/jexcelapi/ 建议不要用新版本,因为新版本会出现与jdk版本兼容问题,如果运行程序出现问题的时候请切换旧版本。

一、Jsp页面

注意:1、在jsp页面的form要使用html本身的<form>标记,而不要使用第三方视图开源框架的form标记,例如不要使用strut的<htm:form>。

2、在<form>的属性里必须加上  ENCTYPE=”multipart/form-data”

1 <h1>导入Excel</h1> 2 <hr> 3 <form action="importExcel" method="post" enctype="multipart/form-data"> 4 <input type="file" name="importExcel" id="importExcel"> 5 <input type="submit" value="导入"> 6 </form>

二、上传excel的Servlet

注意:1、导入的excel最好用后缀为.xls,如果用.xlsx可能会导不进去。

2、在调用FileItem的write方法前必须保证文件的存放路径存在否则出现异常。commons fileupload不会自动为你建立不存在的目录。

3、上传后会对文件进行重命名,以时间为文件名进行命名

1 public class ImportExcelServlet extends HttpServlet { 2 //缓冲区域 3   File tempPathFile; 4 //默认路径 5   String uploadTo = "D:\\"; 6 // 支持的文件类型 7   String[] errorType = { ".xls" }; 8 //格式化日期 9   SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS"); 10 11 @Override 12 protected void doGet(HttpServletRequest req, HttpServletResponse resp) 13 throws ServletException, IOException { 14 req.setCharacterEncoding("utf-8"); 15 resp.setCharacterEncoding("utf-8"); 16  //取得服务器真实路径 17   uploadTo = req.getSession().getServletContext().getRealPath("\\") + "upload\\"; 18 // Create a factory for disk-based file items 19   DiskFileItemFactory factory = new DiskFileItemFactory(); 20 // 设置缓冲区大小,这里是4kb 21   factory.setSizeThreshold(4096); 22 // 设置缓冲区目录 23   factory.setRepository(tempPathFile); 24 // Create a new file upload handler 25   ServletFileUpload upload = new ServletFileUpload(factory); 26 // Set overall request size constraint 27 // 设置最大文件尺寸,这里是4MB 28 upload.setSizeMax(4*1024*1024); 29 // 开始读取上传信息 30 List fileItems = new ArrayList(); 31 try { 32 fileItems = upload.parseRequest(req); 33 } catch (FileUploadException e1) { 34 e1.printStackTrace(); 35 } 36 // 依次处理每个上传的文件 37 Iterator iter = fileItems.iterator(); 38 System.out.println("fileItems的大小是" + fileItems.size()); 39 // 正则匹配,过滤路径取文件名 40 String regExp = ".+\\\\(.+)$"; 41 Pattern p = Pattern.compile(regExp); 42 while (iter.hasNext()) { 43 FileItem item = (FileItem) iter.next(); 44 // 忽略其他不是文件域的所有表单信息 45 System.out.println("正在处理" + item.getFieldName()); 46 if (!item.isFormField()) { 47 String name = item.getName(); 48 long size = item.getSize(); 49 if ((name == null || name.equals("")) && size == 0) 50 continue; 51 Matcher m = p.matcher(name); 52 boolean result = m.find(); 53 if (result) { 54 boolean flag = false; 55 for (int temp = 0; temp < errorType.length; temp++) { 56 if(m.group(1).endsWith(errorType[temp])) { 57 flag = true; 58 } 59 } 60 if(!flag) { 61 System.out.println("上传了不支持的文件类型"); 62 throw new IOException(name + ": wrong type"); 63 } 64 try { 65 String fileName = uploadTo + format.format(new Date()) + m.group(1).substring(m.group(1).indexOf(".")); 66 item.write(new File(fileName)); 67 //调用ReadExcel类进行读出excel 68 ReadExcel.readExcel(fileName, resp.getWriter()); 69 System.out.println(name + "\t\t" + size); 70 } catch (Exception e) { 71 e.printStackTrace(); 72 } 73 } 74 } else { 75 // 这里添加对不是上传文件表单项的处理 76 System.out.println("这是一个表单项"); 77 } 78 } 79 80 } 81 82 @Override 83 protected void doPost(HttpServletRequest req, HttpServletResponse resp) 84 throws ServletException, IOException { 85 doGet(req, resp); 86 } 87 88 @Override 89 public void init() throws ServletException { 90 tempPathFile = new File("d:\\temp\\buffer\\"); 91 if (!tempPathFile.exists()) { 92 tempPathFile.mkdirs(); 93 } 94 } 95 } 96

三、读出excel文件内容的类

1 public class ReadExcel { 2 3 public static void readExcel(String pathname, PrintWriter out) { 4 try { 5 //打开文件 6 Workbook book = Workbook.getWorkbook(new File(pathname)) ; 7 //取得第一个sheet 8 Sheet sheet = book.getSheet(0); 9 //取得行数 10 int rows = sheet.getRows(); 11 for(int i = 0; i < rows; i++) { 12 Cell [] cell = sheet.getRow(i); 13 for(int j=0; j<cell.length; j++) { 14 //getCell(列,行) 15 out.print(sheet.getCell(j, i).getContents()); 16 out.print("&nbsp;"); 17 } 18 out.println("<br/>"); 19 } 20 //关闭文件 21 book.close(); 22 } catch (BiffException e) { 23 e.printStackTrace(); 24 } catch (IOException e) { 25 e.printStackTrace(); 26 } 27 } 28 29 } 30

总结:上面只是一个很简单的导入excel文件的例子,如果想做完善还得下更多的功夫。在做的过程中如果出现Workbook打不开,请更换jxl 版本,尽量用低版本,这样与jdk兼容会好点,我在做这个导入excel的时候,就遇到了版本兼容问题,处理了半天才发现问题所在。所以想做这个例子给大 家参考,以后不要犯和我同样的错误。O(∩_∩)O哈哈~

[转载]深入理解ASP.NET MVC(4)

mikel阅读(927)

[转载]深入理解ASP.NET MVC(4) – P_Chou Go deep and Keep learning – 博客园.

到目前为止Route对象只剩下DataTokens属性没有涉及,事实上这个Areas机制的核心。

DataTokens实际上也是一个RouteValueDictionary,在用MapRoute方法构造在Route构造的时候,可以传一个namespaces字符串数组,这个参数会构造成Route对象的DataTokens[“Namespaces”],它的值将被MVC框架优先用来在对应的名字空间中查找相应的Controller。 如果在指定的名字空间中能找到Controller,那么,就算在其他名字空间中有相同名字的Controller(大小写敏感)也没关系;如果在指定的 名字空间中没有找到Controller,那么将在所有引用的程序集中查找,此时如果出现重复名字的Controller,那么将出现多个匹配的错误。这 种行为是DefaultControllerFactory实现的,关于DefaultControllerFactory将在以后分析。

Areas机制是这样的一种机制:在不同的Area中可以有相同名字的Controller,也就是说Controller的名字可以重复了!这样 整个web应用程序可以按功能划分成几个模块,每个模块是一个Area,每个Area互相独立,可以独立地由某个开发人员随意定义URL或 Controller而不影响其他Area。

比如:有管理员和用户两个模块,也许需要如下的URL:

Admin/Home/Index

User/Home/Index

于是可以用VS集成的Area生成模板创建两个Area:Admin和User,分别地,由两个开发人员分别负责开发,他们都需要用 HomeController和Index方法,有了Areas机制,HomeController被分别放到两个不同的名字空间中,这就不会有冲突。

讲到这里你也许隐约明白DataTokens和Areas机制的某种关系了。在vs创建Areas的时候到底做了哪些事情呢?

一、首先在每个Area中Controller都将被放置到一个名字空间中,例如:MyAppName.Areas.Admin.Controllers;

二、为每个Area创建一个AreaRegistration的继承类,如果是Admin的Area将是AdminAreaRegistration。在这个类中重写AreaName属性和RegisterArea方法:

1 public override void RegisterArea(AreaRegistrationContext context)
2 {
3 context.MapRoute(
4 "Admin_default",
5 "Admin/{controller}/{action}/{id}",
6 new { action = "Index", id = UrlParameter.Optional }
7 );
8 }

可以看到在RegisterArea方法中也调用了MapRoute方法注册路由。需要注意的是这个的MapRoute虽然也是操作全局路由表,但是它的实现略有不同:

1.首先设置的URL Pattern是以Area名字开头的,这样做是必要的,毕竟这个URL Pattern最终是放在全局中的;

2.它会将DataTokens[“Namespaces”]设置成当前Area的名字空间,比如 MyAppName.Areas.Admin.*。结果是,当一个请求到来是,DefaultControllerFactory会优先到这个名字空间下 查找Controller。而且会增加一个DataTokens[“UseNamespaceFallback”],并设置为false,这样当且仅当显示设置的名字空间中有需要的Controller时,才能成功,其他名字空间的的同名Controller将无效;

3.最后,还会添加一个叫DataTokens[“area”]的键值,并设置为当前Area名字,这是为了在反向映射(outbounding)URL的时候使用。因此在MVC中”area”键是有特殊用途的,所以不能用于url pattern的参数。

在Areas机制中有一个冲突需要注意。由于路由表只有一张,如果当前的url映射到了”root area”(即在Global域),那么将从当前所有的名字空间中查找Controller,此时很可能找到多个匹配的。解决方案是,在 Globla.asax.cs中设置路由的时候,为DataTokens设置优先名字空间。

进一步扩展

当从深层次了解了路由工作机制后,就进行一些自定义了。

自定义RouteBase

有前面的分析,可以知道,在inbound时Route(继承自RouteBase)需要提供一个RouteData,因此RouteBase定义 了GetRouteData方法,这是我们可以自己实现的;同时,GetVirtualPath方法用于outbound。所以,只要实现了这两个方法就 可以完成一个RouteBase的实现。比如:当想要把一个老的网站改造成新的基于MVC架构的,又不想使原来的url失效,简单的处理方案可以像下面这 样:

01 public class LegacyUrlsRoute : RouteBase
02 {
03 // In practice, you might fetch these from a database
04 // and cache them in memory
05 private static string[] legacyUrls = new string[] {
06 "~/articles/may/zebra-danio-health-tips.html",
07 "~/articles/VelociraptorCalendar.pdf",
08 "~/guides/tim.smith/BuildYourOwnPC_final.asp"
09 };
10 public override RouteData GetRouteData(HttpContextBase httpContext)
11 {
12 string url = httpContext.Request.AppRelativeCurrentExecutionFilePath;
13 if(legacyUrls.Contains(url, StringComparer.OrdinalIgnoreCase)) {
14 RouteData rd = new RouteData(this, new MvcRouteHandler());
15 rd.Values.Add("controller", "LegacyContent");
16 rd.Values.Add("action", "HandleLegacyUrl");
17 rd.Values.Add("url", url);
18 return rd;
19 }
20 else
21 return null; // Not a legacy URL
22 }
23 public override VirtualPathData GetVirtualPath(RequestContext requestContext,
24 RouteValueDictionary values)
25 {
26 // This route entry never generates outbound URLs
27 return null;
28 }
29 }

自定义IRouteHandler

通常在MVC框架中IRouteHandler由MvcRouteHandler实现,这个MVC框架的入口。尽管如此,我们还是可以自己定义一个 IRouteHandler。当我们需要对某些请求做优化处理的时候可以考虑这样做。因为,自定义实现IRouteHandler意味着将忽略MVC框 架。比如下面这个实现:

public class HelloWorldHandler : IRouteHandler
{
public IHttpHandler GetHttpHandler(RequestContext requestContext)
{
return new HelloWorldHttpHandler();
}
private class HelloWorldHttpHandler : IHttpHandler
{
public bool IsReusable { get { return false; } }
public void ProcessRequest(HttpContext context)
{
context.Response.Write("Hello, world!");
}
}
}

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

mikel阅读(1119)

[转载]服务器端执行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阅读(1175)

[转载]通过源代码研究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)中定义的。