[转载]jQuery EasyUI 1.3.0 Demo合集、离线API、动态换肤

mikel阅读(1113)

[转载][原]jQuery EasyUI 1.3.0 Demo合集、离线API、动态换肤 – purediy – 博客园.

JQuery EasyUI 1.3 Demo合集、离线API、动态换肤。

JQuery EasyUI 1.2.6源码 Demo合集 离线API 的时候就发现作者要放出来1.3了,前段时间发现已经更新了,没来得及整理,现给出jQuery EasyUI 1.3 Demo合集、离线API,以及把easyui的各种皮肤整合在一起,包括官网上的扩张皮肤,可以动态换肤了。这次没有像1.2.6翻译的源码,暂时也没用到,使用的时候再说吧。

打包地址猛击下载,http://files.cnblogs.com/purediy/jquery-easyui-1.3.0.zip

Demo预览图片

demo

API预览图片

API

[转载]如何写出兼容大部分浏览器的CSS 代码

mikel阅读(1073)

转载如何写出兼容大部分浏览器的CSS 代码 – LoveJenny – 博客园.

前阵子一直在从事b/s 项目的开发,在css 方面有一些心得体会,特写来与大家分享,欢迎大家评论,不过请勿人身攻击啊,因为在前几年我也写过一篇文章:[原]兼容浏览器的布局CSS心得体会

楼下有很多人的评论:

image

image

image

就让我继续毁人吧。

 

1:使用Firefox 当主开发浏览器

为什么我推荐使用firefox 做为主开发浏览器,

首先目前市场上浏览器很多,ie6 + ,firefox ,opera,chrome,qq 浏览器,360浏览器,搜狗浏览器…

为什么我把qq 浏览器,360浏览器,搜狗浏览器 这些也算进去,是因为这些浏览器虽然使用ie的内核,但是在某些地方显示的效果不一样。

有很多人在开发的时候选择ie 做为浏览器,当然我认为如果使用ie 做为主开发浏览器的话,那应该是使用

ie8+ ,毕竟在ie8+ 的时候才出现了比较强大的工具,另外如果使用ie的话,那么你经常问自己的问题是“ie 下显示正常,为什么firefox 下显示不正常呢?”,这点后面会有解释。

 

回归正题:我推荐使用firefox 做为主开发浏览器的原因:

a:firefox 下面有开发神器firebug.

firebug 究竟有多厉害,相信使用过的人都知道的,另外firebug 还有yslow,等一系列测速插件,并且firebug启动速度也比较快。

b:firefox 有很多开发插件,虽然firebug已经很出色了,但是这些插件在某些地方还是很有用的。

c:firefox 是遵守css 标准的浏览器,众所周知ie 在这方面做的比较烂。

以前我曾经看过一篇文章,大致讲的是

“ie 下显示正常,为什么firefox 下显示不正常呢?”,

其实我感觉应该这样理解

“为什么firefox 下显示不正常,但是在ie下显示正常呢?”

因为当你这样问自己后,你自己就会去查找,究竟是哪些元素ie没有遵守css 标准呢。

d:firebug 虽然有控制台,但是有时候JavaScript 代码console.write(“something”);,并不会正常的显示到控制台上面去,

当你碰到console 失败的时候,请尝试下下面的步骤

1:启用firebug

image

2:禁用所有面板和清空激活列表。

image

3:重新启用firebug。

image

2:使用reset.css

使用reset.css 可以省掉很多事情,比如ul ,li 他们在很多浏览器下面的margin ,padding 都不一样,

通过使用reset.css 可以将大部分不一致的地方给屏蔽掉,没有理由不使用reset.css啊。

下面是我随便挑选的一个reset.css.

yui reset.css

3:学习一些基本的CSS 知识

如果想写好css 的话,那么一些基本的css知识是应该知道的,比如float,margin,padding,position..属性,还有盒子模型,文档流模型,在这里没什么技巧,我只能推荐一些书籍了。

精通CSS:高级Web标准解决方案(第2版)

一些基本的书籍随便看看,上面的这本书籍才是经典中的经典,个人认为学css不看这本书,看遍千本也枉然。

 

4:常见的bug

常见的一些bug你应该都知道的,事实上在我写css的这段时间,我发现大部分bug 都基本上是这些bug,大部分显示不正确的也是这些bug.

a:clearfix

在很多时候一刀切是非常管用的,尤其是对于那些非常喜欢用float的同学。

.clearfix:before, .clearfix:after { content:""; display:table; } .clearfix:after{ clear:both; overflow:hidden; } .clearfix{ *zoom:1; }

b:double margin (双倍边距)

有时候你会发现两个元素的margin 在不同的浏览器上显示不一样,如果你查看css 的时候发现你设置了float的话,那么很可能你碰到了double margin.  解决这个办法的原因很简单,比如设置margin 为0 (即不使用margin,我最早就是这种想法),

最根本的是设置float 元素的display = “inline”.

由于float 元素本来display 就是inline .所以每当你要使用float 的时候,请记住将display 设为inline.

 

c:关于width ,height,line-height,border

border 是一个很好用的东西,在很多时候,仅仅给元素加个border就解决实际问题了,因为加了border的元素在ie 下面就有了hasLayout 属性。

line-height 也是一个非常让人头痛的属性,有时候你会发现无论你怎么设置height,height 好像不起作用似的,height不起作用有可能是display :line ,也有可能float,同样也有可能是line-height。

有人说过只要是float 的对象就应该设置width,height,关于这点我认为在某些情况下设置width 是必要的,但是设置height 就不一定了。

d:bug 其实没有那么多

有很多些css 的同学,当碰到一两个比较棘手的问题的时候,就说这个是ie的bug,可是大部分情况下,是自己的写法没有符合标准,或者是没有正确的理解css,所以他 们经常做的就是,试试加个width,加个height。试试加个border,试试修改下margin,padding。 当然很多时候运气比较好,一下就加对了,但运气不好的时候就比较郁闷了。解决了bug 是运气,没解决bug就是ie的bug,可是事实上ie 的bug 大部分都曾经出现过,所以在对待bug 的时候,还是要心平气和的分析问题,解决问题。

 

5:应该记住的一些浏览器css写法

如果你使用firefox 做为主开发浏览器的话,我相信你的页面在opera,chrome 下应该也不会太乱,但是ie 我就不敢保证了,这个时候就应该调整ie 兼容性了。

有很多种方式,比如使用条件注释针对各个ie 浏览器引入不同的css。

<!--[if IE 5]>仅IE5.5可见<![endif]--> <!--[if gt IE 5.5]>仅IE 5.5以上可见<![endif]--> <!--[if lt IE 5.5]>仅IE 5.5以下可见<![endif]--> <!--[if gte IE 5.5]>IE 5.5及以上可见<![endif]--> <!--[if lte IE 5.5]>IE 5.5及以下可见<![endif]--> <!--[if !IE 5.5]>非IE 5.5的IE可见<![endif]>

 

我个人比较喜欢的一种方式还是使用css hacker.

我是从ie8,ie7,ie6,逐渐的调的,因为越到后面需要调的东西越多,而通过这种方式来调的话,我的css 代码改动是比较小的,即所谓的小步前进吧。

首先找出是哪个元素引起布局混乱,然后看看是不是属于哪些常见的bug,接着给这个元素设定一个

border.

通过慢慢的查找,相信除了ie6 ,布局应该都是ok的。但是ie6应该怎么做呢?

image

我比较喜欢的是 “_”

image

参考自:http://zhidao.baidu.com/question/329196001.html

6 :金子般的table

有很多人都不喜欢table,所以才有了div without table. 对于这些人来说table是不需要的,div 是神。

我以前写过一篇文章:你也可以的,Div Without Table

早这篇文章中,我使用div来布局,虽然题目叫做Div Without Table ,但是我并不是想表达Table 是没用的,Table 是应该被Without 的,我始终坚信一个原则“div 布局,table 展示数据”。

为什么table 是金子呢?

首先table 是一个很早就出现的元素,而且由于table 比较简单,所以各个浏览器对table 的渲染 大部分都是一致的,所以使用table你很容易实现一些功能。

a:居中显示

在有些时候居中显示会让你比较郁闷,你会感觉到table 的好用和舒服。

b:n行n列

对于几行几列的这种布局,不要怀疑,不要犹豫,使用table吧。

c:某些让你改bug 很郁闷的地方。

有时候你会发现你调来调去总是调不好,如果这个bug 引入table 的代价比较小的话,个人也推荐使用table。

 

传说div 比table 渲染的要快,因为table 必须得等到</table> ,table 中的元素才会被渲染出来,我没有测试过,不知道是不是这样。

 

7:使用一些CSS 框架

如果你的页面可以使用一些CSS框架的话,那就尝试使用一款CSS框架吧,960grid, blueprint 应该都还算可以。

[原创]ASP.NET MVC多域名多站点解析问题

mikel阅读(1011)

原有的一个ASP.NET MVC的项目只是一个域名建站点指向,现在由于项目大了,需要拆分成三个独立域名站点分别指向不同的首页,但是程序是一套程序,问题出来了,首页怎么指向的问题,于是看到Global.asax里面的routing规则,发现可以根据初始化访问的不同域名设置不同的默认首页,然后每个域名建立一个默认页面里面重定向到各自的首页,然后IIS中建立三个站点分别将默认页面设置为各自的重定向页面,不再只是用Default.aspx页面了,问题解决。

其实很多时候问题不一定要解决得那么华丽,实用即可!

Global.asax代码如下:

 String HostName = HttpContext.Current.Request.Url.Host.ToString().ToLower(); //获取URL主机地址
  if (HostName.IndexOf("mikel") >= 0)
  {
            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
            );
   }
  if (HostName.IndexOf("kiwing") >= 0)
  {
            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "User", action = "Index", id = "" }  // Parameter defaults
            );
   }

Default.aspx.cs页面代码:

 public void Page_Load(object sender, System.EventArgs e)
{
Response.Redirect("~/Home/Index/");
}

KiwingIndex.aspx.cs页面代码:

 public void Page_Load(object sender, System.EventArgs e)
{
Response.Redirect("~/User/Index/");
}

[转载]Android模拟器连接物理键盘

mikel阅读(901)

[转载]Android模拟器连接物理键盘 – 范长法@三月软件 – 博客园.

  在Android模拟器中测试的时候,经常需要向譬如EditText中输入数据,Android模拟器键盘输入方式有三种:

1. Android系统自带的虚拟键盘,点击文本框时出现:

2. Android虚拟机左边的可视虚拟键盘:

3.我们使用电脑的物理键盘。

 

刚开始测试时,每次都需要用鼠标在模拟器上的屏幕上像傻瓜一样乱点,用了一天中午受不了了,摸索了一番,终于发现原来Android设置虚拟机支持物理键盘的时候是需要在建模拟器时指定。

下面是在新建虚拟机或Edit虚拟机的界面:

 

添加上面两项:

keyboard lid support: 模拟器物理键盘,设为yes,表示模拟器物理键盘可用;

keyboard support : 电脑键盘,设为yes,电脑键盘可用。

 

这样就ok了,呵呵,用物理键盘真的很爽。以后搞开发还是需要先把编译器熟悉熟悉一下,这样可以提高效率。不要只是因为一时的懒不想去摸索。搞计算机的勤快还是必须的。

[转载]以防万一,把被删除的数据都备份起来

mikel阅读(940)

[转载]以防万一,把被删除的数据都备份起来,关键数据丢了哭都来不及了 – 通用信息化建设平台 – 博客园.

防止误删除有目前通用权限管理里有2个方法,1个是打删除标志,另外一个把数据备份到另外一个表里,这个看开发人员的选择,觉得哪个做法好,就可以用哪个方法,删除标志的方法虽然简单,但是过滤数据很麻烦,而且数据库量大后数据库的性能有明显的影响。

备份到另外一个表,虽然程序处理繁杂一些,但是数据库的性能映像很小,有对数据洁癖的人适合用这个方式。下面是例子程序,可以模仿通用权限管理把自己的业务系统都进行改造,思路思想也都可以借鉴一下。

总算有心情写个博客了,被删除的数据若有需要恢复过来,不是专业的数据库管理员,那是一个很难的事情,而且知道哪些数据被删除了,也不是那么 简单的事情,写个触发器是一个不错的处理方法,把当前要被删除的数据,都存放早相同结构的另外一个表里。 相对自增量而言GUID为主键的处理起来更简单了。

通用权限管理,吉日嘎拉

被删除的表,主键最好不是自增量,因为需要保留原始数据的主键,否则数据库很难恢复或者涉及到主细表的问题等等,所以被删除的表的主键建议不要用则增量。

通用权限管理,吉日嘎拉

我们在角色管理界面上选中2条数据库,在调试模式下,然后按删除,看程序的运行效果如何?

通用权限管理,吉日嘎拉

已经被放到被删除的表里来了,主键也没被篡改,非常不错。

通用权限管理,吉日嘎拉

这个是为了特意在测试调试模式下把被删除的数据转移到另外一个表的做法。

通用权限管理,吉日嘎拉

 

下面是没有采用数据库触发器的方式,对删除的表进行数据备份的方法。

1:这里设计到备份表的主键问题。

2:数据库事务问题,要么成功,要么失败,要么全成功。

3:不要数据库触发器,而是用程序去实现。

4:兼容多种数据库。

5:下出来的代码量非常小,不能有过于繁琐的代码,需要重复利用很多现有的类库。

6:由于系统同时支持多个子系统,意思是有可能是多个系统的不同的角色表,所以要支持多系统的权限管理说白了就是多表的操作,不只是固定的2个表。

/// <summary>
        /// 批量删除角色
        /// </summary>
        /// <param name="userInfo">用户</param>
        /// <param name="ids">主键数组</param>
        /// <returns>影响行数</returns>
        public int BatchDelete(BaseUserInfo userInfo, string[] ids)
        {
            // 写入调试信息
            #if (DEBUG)
                int milliStart = BaseBusinessLogic.StartDebug(userInfo, MethodBase.GetCurrentMethod());
            #endif

            // 加强安全验证防止未授权匿名调用
            #if (!DEBUG)
                LogOnService.UserIsLogOn(userInfo);
            #endif

            int returnValue = 0;
            using (IDbHelper dbHelper = DbHelperFactory.GetHelper(BaseSystemInfo.UserCenterDbType))
            {
                try
                {
                    dbHelper.Open(UserCenterDbConnection);
                    // 开始数据库事务
                    dbHelper.BeginTransaction();
                    string tableName = BaseRoleEntity.TableName;
                    if (!string.IsNullOrEmpty(BaseSystemInfo.SystemCode))
                    {
                        tableName = BaseSystemInfo.SystemCode + "Role";
                    }
                    BaseRoleManager roleManager = new BaseRoleManager(dbHelper, userInfo, tableName);
                    // 这里是直接删除功能的实现
                    // returnValue = roleManager.BatchDelete(ids);

                    BaseRoleEntity roleEntity = null;
                    // 把删除的记录放到被删除的表里(表名后面加了后缀Deleted,也可以放在另外一个数据库里也可以的)
                    BaseRoleManager roleDeletedManager = new BaseRoleManager(dbHelper, userInfo, tableName + "Deleted");
                    foreach (var id in ids)
                    {
                        // 逐个删除,逐个备份
                        roleEntity = roleManager.GetEntity(id);
                        // 先添加到被删除的表里,这时候原先数据的主键需要保留的,否则恢复数据时可能会乱套
                        roleDeletedManager.Add(roleEntity);
                        // 数据备份好后再进行删除处理
                        returnValue += roleManager.Delete(id);
                    }
                    // 提交数据库事务
                    BaseLogManager.Instance.Add(dbHelper, userInfo, serviceName, AppMessage.RoleService_BatchDelete, MethodBase.GetCurrentMethod());
                    dbHelper.CommitTransaction();
                }
                catch (Exception ex)
                {
                    // 撤销数据库事务
                    dbHelper.RollbackTransaction();
                    BaseExceptionManager.LogException(dbHelper, userInfo, ex);
                    throw ex;
                }
                finally
                {
                    dbHelper.Close();
                }
            }

            // 写入调试信息
            #if (DEBUG)
                BaseBusinessLogic.EndDebug(MethodBase.GetCurrentMethod(), milliStart);
            #endif

            return returnValue;
        }

通用权限管理系统的架构,几乎没修改几行代码,就可以实现用户的删除数据是备份数据的需求,虽然没数据库触发器处理那么简单,但是程序的灵活性很强,可阅读性、可调试性、多数据库的兼容性等等方面,还是有明显的优点的。

删除处理的方法还可以写在代码生成器里,这样所有的删除操作,都可以实现数据库的备份了,又简单又好用,也不用动脑子了,很省事。

将权限管理、工作流管理做到我能力的极致,一个人只能做好那么很少的几件事情。

[转载]多站点Session共享解决方案

mikel阅读(978)

[转载]多站点Session共享解决方案 – 杰之蓝 – 博客园.

对于大多数的网站来说,都会使用Session来维护用户在一次会话中操作;Session对于任何Web项目来说都是必不可少的(当然除去那 里网站里不包含任何用户操作的,^_^这个对于Web2.0时代的网站来说好像是不太可能的吧)。对于单独的站点来说,一个站点只用一个Session就 OK了,但对于同时多个站点来说,如何对多个站点时实现Session共享呢?

常见的作法有:

  • 使用Cookie方式实现多个站点间的共享(这种方式只限于几个站点都在同一域名的情况下);
  • 使用.net自动的状态服务(ASP.NET State Service);
  • 使用.net的Session数据库;
  • 使用MemcachedDB。

以上几种方案各有优点,至于每种方式的优点在那里,缺点在那里?一时半会真的很难说清,如果有一篇文章就能说清的,那都是扯蛋,这种东西没有在实际中使用,单单在表现上说这个方案好那个方案不好,都是扯蛋。

我们的项目现在采用的是第三种方案,使用Session数据库解决多站点的Session共享。有不同看法的朋友都可以聊一下,下面说一下我们的实现方式:

  1. 既然使用Session数据库了,当然必须要先对数据库进行创建,既然是使用微软的方案了,微软肯定也就为大家提供了相应的实现方式。通过命令行方式进入:
    1 C:\Windows\Microsoft.NET\Framework64\v4.0.30319

    在此目录下运行:

    1 aspnet_regsql.exe -sstype c -ssadd -d 你的数据库名 -U 用户名 -P 密码 -S 数据库服务地址

    注意:此处的参数是区分大小写的;执行完上述命令后,会进行Session数据库创建阶段,创建完成后可以打开数据库查看创建是否成功。

  2. Session数据库创建成功了并不代表就可以实现多项目的Session共享了,还需要对目前的Session数据库做一些小小的手脚,其实就是为了欺骗数据库说“哎,我就一个应用程序在运行。^_^”。下面来看看怎么做这点小手段:
ALTERPROCEDURE[dbo].[TempGetAppID]
@appName    tAppName,
@appIdint OUTPUT
AS
SET@appName=LOWER(@appName)
SET@appId=NULL

SELECTtop1@appId= AppId
FROM[SNSSessionDB].dbo.ASPStateTempApplications
--WHERE AppName = @appName

IF@appIdISNULLBEGIN
BEGINTRAN

SELECT@appId= AppId
FROM[SNSSessionDB].dbo.ASPStateTempApplications WITH (TABLOCKX)
WHERE AppName =@appName

IF@appIdISNULL
BEGIN
EXEC GetHashCode @appName, @appId OUTPUT

INSERT[SNSSessionDB].dbo.ASPStateTempApplications
VALUES
         (@appId, @appName)

IF@@ERROR=2627
BEGIN
DECLARE@dupApp tAppName

SELECT@dupApp=RTRIM(AppName)
FROM[SNSSessionDB].dbo.ASPStateTempApplications 
WHERE AppId =@appId

RAISERROR('SQL session state fatal error: hash-code collision between applications ''%s'' and ''%s''. Please rename the 1st application to resolve the problem.', 
, 1, @appName, @dupApp)
END
END

COMMIT
END

RETURN0

其实只是做了一点改动,就是加了一个top 1 ,我每次查的时候,只能第一次的AppID这样的话,就是说我多个项目只一个Session实例。

上面的各种手段都做了,其实目的只有一个就是在项目中使用这个Session数据库(发点牢骚:说微软坑爹吧,其实一点都不假,他啥都替你想到了,我们这只简单的会使用他就行了,这也是一代一代.NET开发人员的悲剧,每次微软发生新的技术改动,我们这些苦逼的开发人员就要去学习学习,然后微软突然那天说我不对这个技术再做升级了,我要放弃他了,好了你就看吧,大街上一个个苦着脸的,有一半都是搞开发的。)。怎么在项目中使用他呢?修改 web.config 在system.web 加入或修改以下项

1
2  

  4. OK,你大功告成了.记得重启一下IIS ,最好把自己本机上的cookie或垃圾项清除一下,这样效果更好。

  通过上面的一系列操作,终于OK了,在项目中使用的时候,就像我们平常一样赋值和调用就OK了。

  OK了,终于写完了。

[转载]ASP.NET MVC的View是如何呈现出来的[实例篇]

mikel阅读(980)

[转载]ASP.NET MVC的View是如何呈现出来的[实例篇] – Artech – 博客园.

在《[设计篇]》篇中我们通过对View引擎的总体介绍讲述了从ViewResult的创建到View呈现的原理,为了让读者对View引擎及其View呈现机制具有一个深刻的认识,我们自定义一个简单的用于呈现静态HTML的StaticFileViewEngine。 在一个通过Visual Studio的ASP.NET MVC项目模板创建的空Web应用中,我们定义了如下一个针对于静态HTML内容呈现的自定义StaticFileView。 StaticFileView实现了IView接口,在实现的Render方法中读取制定文件的内容写入作为参数的TextWriter。 [本文已经同步到《How ASP.NET MVC Works?》中]

1: public class StaticFileView:IView

2: {

3: public string FileName { get; private set; }

4: public StaticFileView(string fileName)

5: {

6: this.FileName = fileName;

7: }

8: public void Render(ViewContext viewContext, TextWriter writer)

9: {

10: byte[] buffer;

11: using (FileStream fs = new FileStream(this.FileName, FileMode.Open))

12: {

13: buffer = new byte[fs.Length];

14: fs.Read(buffer, 0, buffer.Length);

15: }

16: writer.Write(Encoding.UTF8.GetString(buffer));

17: }

18: }

由于StaticFileView中定义的内容完全是静态的,所以缓存显得很有必要。我们只需要基于Controller和View名称对View实施缓存,为此我们定义了如下一个作为Key的数据类型ViewEngineResultCacheKey。

1: internal class ViewEngineResultCacheKey

2: {

3: public string ControllerName { get; private set; }

4: public string ViewName { get; private set; }

5:

6: public ViewEngineResultCacheKey(string controllerName, string viewName)

7: {

8: this.ControllerName = controllerName ?? string.Empty;

9: this.ViewName = viewName ?? string.Empty;

10: }

11: public override int GetHashCode()

12: {

13: return this.ControllerName.ToLower().GetHashCode() ^ this.ViewName.ToLower().GetHashCode();

14: }

15:

16: public override bool Equals(object obj)

17: {

18: ViewEngineResultCacheKey key = obj as ViewEngineResultCacheKey;

19: if (null == key)

20: {

21: return false;

22: }

23: return key.GetHashCode() == this.GetHashCode();

24: }

25: }

具有如下定义的StaticFileViewEngine代表StaticFileView对应的ViewEngine。我们通过一个字典类型的字段viewEngineResults作为对ViewEngineResult的缓存,而View的获取操作最终实现在InternalFindView方法中。通过StaticFileView表示的View定义在一个以View名称作为文件名的文本文件中,该文件的扩展名为.shtml(Static HTML)。

1: public class StaticFileViewEngine : IViewEngine

2: {

3: private DictionaryviewEngineResults = new Dictionary();

4: private object syncHelper = new object();

5: public ViewEngineResult FindPartialView(ControllerContext controllerContext, string partialViewName, bool useCache)

6: {

7: return this.FindView(controllerContext, partialViewName, null, useCache);

8: }

9:

10: public ViewEngineResult FindView(ControllerContext controllerContext, string viewName, string masterName, bool useCache)

11: {

12: string controllerName = controllerContext.RouteData.GetRequiredString("controller");

13: ViewEngineResultCacheKey key = new ViewEngineResultCacheKey(controllerName, viewName);

14: ViewEngineResult result;

15: if (!useCache)

16: {

17: result = InternalFindView(controllerContext, viewName, controllerName);

18: viewEngineResults[key] = result;

19: return result;

20: }

21: if(viewEngineResults.TryGetValue(key, out result))

22: {

23: return result;

24: }

25: lock (syncHelper)

26: {

27: if (viewEngineResults.TryGetValue(key, out result))

28: {

29: return result;

30: }

31:

32: result = InternalFindView(controllerContext, viewName, controllerName);

33: viewEngineResults[key] = result;

34: return result;

35: }

36: }

37:

38: private ViewEngineResult InternalFindView(ControllerContext controllerContext, string viewName, string controllerName)

39: {

40: string[] searchLocations = new string[]

41: {

42: string.Format( "~/views/{0}/{1}.shtml", controllerName, viewName),

43: string.Format( "~/views/Shared/{0}.shtml", viewName)

44: };

45:

46: string fileName = controllerContext.HttpContext.Request.MapPath(searchLocations[0]);

47: if (File.Exists(fileName))

48: {

49: return new ViewEngineResult(new StaticFileView(fileName), this);

50: }

51: fileName = string.Format(@"\views\Shared\{0}.shtml", viewName);

52: if (File.Exists(fileName))

53: {

54: return new ViewEngineResult(new StaticFileView(fileName), this);

55: }

56: return new ViewEngineResult(searchLocations);

57: }

58:

59: public void ReleaseView(ControllerContext controllerContext, IView view)

60: { }

61: }

在InternalFindView中,我们先在“~/Views/{ControllerName}/”目录下寻找View文件,如果不存在则在“~/Views/Shared/”寻找。如果对应View文件被找到,则以此创建一个StaticFileView对象,并最终返回封装该View对象的ViewEngineResult。如果目标View文件找不到,则根据基于这两个目录的搜寻地址列表创建并返回对应的ViewEngineResult。 现在我们在Global.asax通过如下的代码对自定义的StaticFileViewEngine进行注册,我们将创建的StaticFileViewEngine作为第一个使用的ViewEngine。

1: public class MvcApplication : System.Web.HttpApplication

2: {

3: protected void Application_Start()

4: {

5: //其他操作

6: ViewEngines.Engines.Insert(0, new StaticFileViewEngine());

7: }

8: }

然后我们定义了如下一个简单的HomeController,Action方法ShowNonExistentView中通过调用View方法呈现一个不存在的View(NonExistentView),而ShowStaticFileView方法则将对应的StaticFileView呈现出来。

1: public class HomeController : Controller

2: {

3: public ActionResult ShowNonExistentView()

4: {

5: return View("NonExistentView");

6: }

7:

8: public ActionResult ShowStaticFileView()

9: {

10: return View();

11: }

12: }

我们为Action方法ShowStaticFileView创建一个StaticFileView类型的View文件ShowStaticFileView.shtml(该View文件保存在“~/Views/Home”目录下,扩展名不是.cshtml,而是shtml),其内容就是如下一段完整的HTML。

1:

2:

3:

4:Static File View

5:

6:

7: 这是一个自定义的StaticFileView!

8:

9:

现在运行我们的程序,在浏览器中输入相应的地址访问Action方法ShowNonExistentView,会得到如下图所示的输出结果。图中列 出的View搜寻位置列表中的前两项正是我们自定义的StaticFileViewEngine寻找对应.shtml文件的两个地址。

 

image

 

如果我们改变浏览器的地址来访问另一个Action方法ShowStaticFileView,会呈现出如下图所示的输出结果,不难看出呈现出来的正是定义在ShowStaticFileView.shtml中的HTML。

 

image

ASP.NET MVC的View是如何被呈现出来的?[设计篇]

[转载]easyui datagrid批量提交json数据到服务器

mikel阅读(1261)

[转载]easyui datagrid 批量 提交 json 数据到服务器 – 管宇 – 博客园.

1. 涉及到的技术。

ASP.NET mvc 3.0.

JQuery,  JQuery easyui datagrid.

JQuery-json – 把js对象,生成json格式的插件 (http://code.google.com/p/jquery-json/ )

 

2. 场景。

我们知道由datagrid编辑后生成的数据是js对象。 这样我们提交到服务器,是无法解析的(如果你有好的方式,请在下面评论,ths)。所以需要解析为json.

 

datagrid  生成json :

var table = $("#table_directory").datagrid("getRows");

var obj = "[";

for (var i = 0; i < table.length; i++) {

     $('#table_directory').datagrid("endEdit", i);

     obj += $.toJSON(table[i]);

     if (i != table.length - 1) {
        obj += ",";
     }
}

obj += "]";

服务器解析json:
JavaScriptSerializer JavaScriptSerializer = new JavaScriptSerializer();
List listSectionBase = javaScriptSerializer.Deserialize(jsonChapter, typeof(List)) as List;
SectionBase 这个类不用管。

是你自己的类即可,哪怕服务器端的属性和前端提交的不一致,也没有问题。 这样数据已经到了服务器,自己就可以继续进行服务器操作了。

[转载]通过一个模拟程序让你明白ASP.NET MVC是如何运行的

mikel阅读(825)

[转载]通过一个模拟程序让你明白ASP.NET MVC是如何运行的 – Artech – 博客园.

ASP.NET MVC的路由系统通过对HTTP请求的解析得到表示Controller、Action和其他相关的数据,并以此为依据激活Controller对象,调 用相应的Action方法,并将方法返回的ActionResult写入HTTP回复中。为了更好的演示其实现原理,我创建一个简单的ASP.NET Web应用来模拟ASP.NET MVC的路由机制。这个例子中的相关组件基本上就是根据ASP.NET MVC的同名组件设计的,只是我将它们进行了最大限度的简化,因为我们只需要用它来演示大致的实现原理而已。[源代码从这里下载]

目录:
一、一个通过查询字符串表示Controller和Action的“MVC”程序
二、通过Route解析HTTP请求获得路由信息
三、在Global.asax中注册Route
四、Route的执行
五、通过MvcHandler处理请求
六、将ActionResult写入Http回复
七、实例的配置和定义

一、一个通过查询字符串表示Controller和Action的“MVC”程序

image

如 右图所示,我们的Web应用非常简单。HomeController.cs为定义Controller类型的文件,而Index.html表示 HomeController中名称为Index的Action对应的View。我们按照ASP.NET MVC的原理,通过解析请求URL得到Controller和Action的名称。如果Controller为Home,则激活 HomeController,如果当前的Action为Index,则将Index.html这个静态文件的内容作为HTTP回复返回。

我不想定义复杂的解析Controller和Action的逻辑,再这里我直接通过请求URL相应的查询字符串controler和action表示Controller和Action的名称。也就是说如果通过浏览器访问地址http://localhost/mvcapp/?controller=Home&action=Index 可以访问到Index.html中的内容(注:我们并没有将Index.html作为站点的默认页面)。

image

接下来我简单的介绍一下是哪些组建促使这个简单的ASP.NET Web应用能够按照MVC的模式来执行。为了使你能够在真正的ASP.NET MVC找到匹配的组件,我们采用了相同的接口和类型名称。

二、通过Route解析HTTP请求获得路由信息

我 定义了如下一个RouteData类型表示解析HTTP请求得到的Controller和Action等信息。Assemblies和 Namespaces表示需要引入的命名空间和程序集,这是因为URL中只能解析出Controller的类型名称,需要相应的命名空间采用得到它的类型 全名。如果对应的程序集不曾加载,还需要加载相应的程序集。

   1: public class RouteData

   2: {

   3:     public string Controller { get; set; }

   4:     public string Action { get; set; }

   5:     public IList<string> Assemblies { get; private set; }

   6:     public IList<string> Namespaces { get; private set; }

   7:     public IRouteHandler RouteHandler { get; set; }

   8:  

   9:     public RouteData(string controller, string action, IRouteHandler routeHandler)

  10:     {

  11:         this.Controller = controller;

  12:         this.Action = action;

  13:         this.RouteHandler = routeHandler;

  14:         this.Namespaces = RouteTable.Namespaces;

  15:         this.Assemblies = RouteTable.Assemblies;

  16:     }

  17: }

真正实现对HTTP请求进行解析并得到RouteData的Route继承自基类RouteBase。我们还定义个了一个表示Route集合的RouteCollection类型,它的GetRouteData方法对集合的所有Route对象进行遍历,并调用其GetRouteData方法。如果得到的RouteData不为空,则返回之。

   1: public abstract class RouteBase

   2: {

   3:     public abstract RouteData GetRouteData(HttpContextBase httpContext);

   4: }

   5:  

   6: public class RouteCollection: Collection<RouteBase>

   7: {

   8:     public RouteData GetRouteData(HttpContextBase httpContext)

   9:     {

  10:         foreach (RouteBase route in this)

  11:         {

  12:             var routeData = route.GetRouteData(httpContext);

  13:             if (null != routeData)

  14:             {

  15:                 return routeData;

  16:             }

  17:         }

  18:         return null;

  19:     }

  20: }

和ASP.NET MVC一样,我们定义了如下一个RouteTable对象,其静态属性正是一个RouteCollection对象。两个静态属性Namespaces和Assemblies为命名空间和程序集名称的全局维护。

   1: public class RouteTable

   2: {

   3:     static RouteTable()

   4:     {

   5:         Routes = new RouteCollection();

   6:         Namespaces = new List<string>();

   7:         Assemblies = new List<string>();

   8:     }

   9:     public static RouteCollection Routes { get; private set; }

  10:     public static IList<string> Namespaces { get; private set; }

  11:     public static IList<string> Assemblies { get; private set; }

  12: }

而我们实例中完成基于查询字符串的Controller和Action解析的QueryStringRoute对应如下。在GetRouteData方法中,除了根据查询字符解析并初始化Controller和Action名称之外,还将RouteHandler指定为MvcRouteHandler。而MvcRouteHandler得GetHttpHandler方法直接返回的是根据RequestContext创建的MvcHandler对象。



   1: public class QueryStringRoute : RouteBase

   2: {

   3:     public override RouteData GetRouteData(HttpContextBase httpContext)

   4:     {

   5:         if (httpContext.Request.QueryString.AllKeys.Contains("controller") &&

   6:             httpContext.Request.QueryString.AllKeys.Contains("controller") )

   7:         {

   8:             string controller = httpContext.Request.QueryString["controller"];

   9:             string action = httpContext.Request.QueryString["action"];

  10:             IRouteHandler routeHandler = new MvcRouteHandler();

  11:             return new RouteData(controller, action, routeHandler);               

  12:         }

  13:         return null;

  14:     }

  15: }

  16:  

  17: public class MvcRouteHandler: IRouteHandler

  18: {

  19:     public IHttpHandler GetHttpHandler(RequestContext requestContext)

  20:     {

  21:         return new MvcHandler(requestContext);

  22:     }

  23: }

三、在Global.asax中注册Route

通过上面定义的RouteTable类型,我们在Global.asax中按照如下的方式在应用启动的时候QueryStringRoute对象添加到RouteTable的静态属性Routes表示的Route列表中。同时为需要的命名空间和程序集名称进行初始化,以辅助后续步骤中对Controller的创建。

   1: public class Global : System.Web.HttpApplication

   2: {

   3:     protected void Application_Start(object sender, EventArgs e)

   4:     {

   5:         RouteTable.Routes.Add(new QueryStringRoute());

   6:         RouteTable.Assemblies.Add("MvcApp, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null");

   7:         RouteTable.Namespaces.Add("Artech.MvcApp");

   8:     }

   9: }

四、Route的执行

通过RouteTable的Routes属性表示的Route列表对请求的解析和路由信息的获取是通过自定义的HttpModule来实现的,它的类型为UrlRoutingModule。如下面的代码片断所示,UrlRoutingModule注册了HttpApplication的PostResolveRequestCache事件,并在该事件触发的时候调用Route列表的GetRouteData方法,并根据得到RouteData创建RequestContext。最后通过RouteData的RouteHandler得到真正用于处理该请求的HttpHandler对象,并对其进行映射。这意味着后续将会采用这个映射的HttpHandler进行请求的处理。

   1: public class UrlRoutingModule: IHttpModule

   2: {

   3:     public void Dispose() { }

   4:     public void Init(HttpApplication context)

   5:     {

   6:         context.PostResolveRequestCache += (sender, args) =>

   7:             {

   8:                 HttpContextWrapper contextWrapper = new HttpContextWrapper(context.Context);

   9:                 HttpContextBase httpContext = (HttpContextBase)contextWrapper;

  10:                 RouteData routeData = RouteTable.Routes.GetRouteData(httpContext);

  11:                 if (null == routeData)

  12:                 {

  13:                     return;

  14:                 }

  15:                 RequestContext requestContext = new RequestContext { HttpContext = httpContext, RouteData = routeData };                    

  16:                 httpContext.RemapHandler(routeData.RouteHandler.GetHttpHandler(requestContext));

  17:             };

  18:     }

  19: }

五、通过MvcHandler处理请求

在UrlRoutingModule映射的实际上是具有如下定义的MvcHandler,它具有一个RequestContext属性通过构造函数进行初始化。在ASP.NET MVC中,真正的请求处理体现在根据路由信息创建Controller,并执行相应的Action方法。这两个步骤体现的ProcessRequest方法中。

   1: public class MvcHandler: IHttpHandler

   2: {

   3:     public RequestContext RequestContext{get; private set;}

   4:     public IControllerFactory ControllerFactory

   5:     {

   6:         get { return ControllerBuilder.Current.GetControllerFactory(); }

   7:     }

   8:     public MvcHandler(RequestContext requestContext)

   9:     {

  10:         this.RequestContext = requestContext;

  11:     }

  12:     public bool IsReusable

  13:     {

  14:         get { return false; }

  15:     }

  16:     public void ProcessRequest(HttpContext context)

  17:     {

  18:         RouteData routeData = this.RequestContext.RouteData;

  19:         var controller =  this.ControllerFactory.CreateController(this.RequestContext, routeData.Controller);

  20:         controller.Execute(this.RequestContext);

  21:     }

  22: }

Controller实现了具有如下定义的接口IController,所有Action方法都通过Execute方法执行,该方法的参数的表示当前请求上下文的RequestContext对象。IController通过相应的Controller工厂创建,下面的代码同时也定义了Controller工厂接口的定义。

   1: public interface IController

   2: {

   3:     void Execute(RequestContext requestContext);

   4: }

   5: public interface IControllerFactory

   6: {

   7:     IController CreateController(RequestContext requestContext, string controllerName);

   8: }

我们定义了如下一个简单名称为DefaultController,它的Execute方法定义很简单:通过包含在RequestContext的RouteData得到当前的Action,并将它作为方法名得到相应的MethodInfo对象,滨个通过反射调用它得到一个ActionResult对象,最后执行ActionResult的ExecuteResult方法。该方法的参数是基于RequestContext创建的另一个上下文ControllerContext。

   1: public class DefaultController : IController

   2: {

   3:     public void Execute(RequestContext requestContext)

   4:     {

   5:         string action = requestContext.RouteData.Action;

   6:         MethodInfo method = this.GetType().GetMethod(action);

   7:         ActionResult result = (ActionResult)method.Invoke(this, null);

   8:         ControllerContext controllerContext = new ControllerContext

   9:         {

  10:             RequestContext = requestContext

  11:         };

  12:         result.ExecuteResult(controllerContext);

  13:     }

  14: }

我们定义了具有如下定义的Controller工厂类DefaultControllerFactory。创建Controller的逻辑也不复杂:通过RouteData表示的Controller名称得到相应的Controller类型,通过反射创建Controller对象。由于RouteData中只包含Controller的名称,所以需要通过命名空间和程序集的辅助才能解析出真正的类型。

   1: class DefaultControllerFactory : IControllerFactory

   2: {

   3:     public IController CreateController(RequestContext requestContext, string controllerName)

   4:     {

   5:         RouteData routeData = requestContext.RouteData;

   6:         string controllerType = string.Format("{0}Controller", controllerName);

   7:         IController controller;

   8:         controller = this.CreateControler(controllerType);

   9:         if (null != controller)

  10:         {

  11:             return controller;

  12:         }

  13:         foreach (string assembly in routeData.Assemblies)

  14:         {

  15:             controller = this.CreateControler(controllerType, assembly);

  16:             if (null != controller)

  17:             {

  18:                 return controller;

  19:             }

  20:  

  21:             foreach (string ns in routeData.Namespaces)

  22:             {

  23:                 controllerType = string.Format("{0}.{1}Controller", ns, controllerName);

  24:                 controller = this.CreateControler(controllerType, assembly);

  25:                 if (null != controller)

  26:                 {

  27:                     return controller;

  28:                 }

  29:             }

  30:         }

  31:  

  32:         throw new InvalidOperationException("Cannot locate the controller");

  33:     }

  34:     private IController CreateControler(string controllerType, string assembly = null)

  35:     {

  36:         Type type = null;

  37:         if (null == assembly)

  38:         {

  39:             type = Type.GetType(controllerType);

  40:         }

  41:         else

  42:         {

  43:             type = Assembly.Load(assembly).GetType(controllerType);

  44:         }

  45:         if (null == type)

  46:         {

  47:             return null;

  48:         }

  49:         return Activator.CreateInstance(type) as IController;

  50:     }

  51: }

六、将ActionResult写入Http回复

Controller的Action方法的返回值为具有如下定义的ActionResult类型,通过ExecuteResult方法将相应的执行结果写入HTTP回复中。我定义了如下一个StaticViewResult,它根据RouteData中的Action信息找到匹配的.html静态文件,并将文件的内容写入HttpResponse。

   1: public abstract class ActionResult

   2: {

   3:     public abstract void ExecuteResult(ControllerContext context);

   4: }

   5:  

   6: public class StaticViewResult: ActionResult

   7: {

   8:     public override void ExecuteResult(ControllerContext context)

   9:     {

  10:         context.RequestContext.HttpContext.Response.WriteFile(context.RequestContext.RouteData.Action + ".html");

  11:     }

  12: }

七、实例的配置和定义

在我们的实例中定义的HomeController定义如下,在表示Action的Index方法中,直接返回一个StaticViewResult对象。

   1: public class HomeController : DefaultController

   2: {

   3:     public ActionResult Index()

   4:     {

   5:         return new StaticViewResult();

   6:     }

   7: }

然后在配置中进行了针对UrlRoutingModule的注册,仅此而已。

   1: <configuration>

   2:   <system.webServer>

   3:     <modules>

   4:       <add name="UrlRoutingModule" type="Artech.MvcRouting.UrlRoutingModule, Artech.MvcRouting"/>

   5:     </modules>

   6:   </system.webServer>

   7: </configuration>

[转载]揭秘 ClownFish 比手写代码还快的原因

mikel阅读(1006)

[转载]揭秘 ClownFish 比手写代码还快的原因 – Fish Li – 博客园.

在上篇博客中,我介绍了我的最新版本的通用数据访问层:ClownFish

在那篇博客的回复评论中,有些人感觉比较好奇:为什么ClownFish能比手工代码的执行速度更快?
不过,也有人不相信,那些人认为反射肯定的速度肯定比不上手写代码。
显然后者完全是在瞎猜,凭自己的感觉在猜。

今天的博客不打算再介绍ClownFish在功能上有什么优点,只是想回答上篇博客中那些感兴趣的人,
解答他们的疑惑:为什么ClownFish能比手工代码的执行速度更快。

我认为ClownFish拥有更快速度的主要有以下原因:
1. 运行时不使用反射。
2. 没有从名称到序号的查找过程。
3. 尽量使用专用版本的读取方法。
4. 为每个实体类型生成一个专用的加载器,减少各种查找开销。
除此之外,在高并发的支持上也做过一些优化。

运行时不使用反射

许多人都知道:反射性能较差。

有些人甚至以此为由不使用反射,还有些人在寻找优化反射性能的方法。 其实反射不管如何优化,都会比不使用反射的代码慢。 所以,ClownFish在运行时,根本就不使用反射。 看到这里,或许有些人又疑惑了:不使用反射如何处理各种类型的加载?

通常说来,数据访问层【应该】是可以加载未知类型的数据实体对象,不可能是专用的手工代码,因此,使用反射好像是必然的选择。 我想,几乎所有人在设计实体对象加载时,都会这样做。 ClownFish的前辈版本也是这样设计的。

我喜欢使用反射,因为它的功能实在是太强大了,我花了很多时间研究反射的优化方法。
当然了,我也试过一些较为成熟的方法:Emit,可惜最终的性能还是不够理想。

我想我应该解释一下Emit的性能不够理想的原因了,要不然,还会有人不相信。 虽然Emit是目前最为有效的解决反射性能的方法,但它【在使用的时候】有以下缺点:
1. 生成的委托类型都是object,需要拆箱与装箱。
2. 生成的委托比较零散,需要一个字典容器来保存,在使用时会有字典的查找开销。
3. 这种方法推荐是按需生成委托的方式,如果保存容器设计不当,在读写时会有大量的锁开销,影响并发。

既然反射方案有这么多的麻烦以及性能又不好,那么不使用它不就行了吗?
或许有些人认为:这不是又回到原点了嘛,数据访问层又不知道它要加载什么类型的实体对象!

其实,我们并没有回到原点,而是来到一个岔路口上,摆有面前有二条路:
1. 众人皆知的反射方案。
2. 动态代码生成并编译的方案。

微软在推出ASP.NET时,一直强调:ASP.NET的程序是经过编译的,因此比ASP要快。
在研究ASP.NET过程中,我正好从它那里发现一种可以在某些场景替代反射的方案: 那就是:动态代码生成并编译的方案。

我们可以回想一下:我们设计的ASPX页面中,那些字符标签,它们是如何运行的,它们并不是C#代码呢。 如果你还想不明拍白的话,可以这样去想:为什么我写了一个控制台程序,需要在编译后,才能运行(可执行文件), 而ASP.NET程序呢? 我们不需要编译啊,写完代码就可以运行!

在我以前的博客ASP.NET页面优化,性能提升8倍的方法中, 我分析过ASP.NET编译之后的页面代码是什么样的。 那篇博客的一些示例代码大致介绍了ASP.NET在编译页面时所做的处理过程,其实也就是:根据我们的代码生成了另一些可运行的代码。 ClownFish也借鉴了这种思想,会根据数据实体类型生成对应的【实体加载器】。 实体加载器是什么,我后面会介绍,这里要说的是:实体加载器中没有任何反射调用,它所包含了经过特别优化处理的代码, 那些代码比较复杂,但是在性能方面是却是最优的(我认为我已优化到极致了),它们虽然也算是通用代码,然而却比通常的手工版代码要快。

优化反射性能的终极方法

前面我提到了【动态代码生成并编译的方案】,我认为这种方案是目前为止最有效的优化反射性能的方法,尤其适合需要大最使用反射的场合中代替反射。

这个方法的实现方式是:在运行时生成可运行的代码,然后调用编译器编译,最后运行编译后的程序集。

由于最后运行的是编译后的程序集,因此,没有任何性能上的损失,而且,在如果动态生成的代码的执行性能比较优秀, 那么,完全就有可能比手工版本的代码还要高效。

这个方法需要二个阶段:
1. 代码生成:对于ClownFish来说,它会生成一份【类似于】手工版本的代码(代码优化方面的事情后面再说)。
2. 编译代码:调用C#编译器,在运行时编译前面生成的代码,得到一个程序集。

在上面的二个步骤中,第二步是比较简单的,因为 .net framework 对编译器也提供了封装类型, 为我们在运行时动态编译代码提供了方便的功能。

比如:ClownFish会在运行时根据实体类型动态生成一些C#代码,那么我们可以通过这下面的代码得到C#编译器的包装类型实例:

CodeDomProvider.CreateProvider("CSharp")

上面这行代码将根据【当前运行时 CLR版本】匹配的C#编译器提供程序。
如果要获取特定版本的C#编译器提供程序,可以使用下面的代码:

Dictionary<string, string> dict = new Dictionary<string,string>();
dict["CompilerVersion"] = "v3.5";
dict["WarnAsError"] = "false";

CSharpCodeProvider csProvider = new CSharpCodeProvider(dict);

在得到一个CodeDomProvider的实例后,只要调用CompileAssemblyFromSource方法就可以得到编译后的程序集,例如:

CompilerResults cr = csProvider.CompileAssemblyFromSource(cp, codes);

return cr.CompiledAssembly;

因此,剩下的任务是看你如何在运行时生成代码了,这是个细致的体力活,应该是没有太多的技术难度的。

由于ClownFish使用了这种动态生成技术,所以ClownFish的性能不比手工代码差。 然而,ClownFish还能在生成代码时做更多的优化,最终的性能可以超越手工代码。 下面来看一下ClownFish在代码优化方面的改进。

没有从名称到序号的查找过程

我想很多人应该知道DbDataReader提供了二种获取数据的索引器:

public abstract object this[int ordinal] { get; }

public abstract object this[string name] { get; }

这二个索引器的具体执行过程,我们可以查看SQLDataReader的实现:

public override object this[int i]
{
    get {
        return this.GetValue(i);
    }
}
public override object this[string name]
{
    get {
        return this.GetValue(this.GetOrdinal(name));
    }
}

从代码中我们可以明显地看出:其实都是对GetValue方法的调用,然而,后者会多一个GetOrdinal(name)调用, 它用于从列名到索引序号的转换,GetOrdinal又是如何实现的呢。在这里我不想贴出那一堆的代码,太长了。 所以,根据这个差别,我们可以知道调用后者也是较慢的,因为每个取值过程都有一次【从名称到序号的查找过程】。

虽然知道有这个差别,但是,我们在直接从DbDataReader中取值时,还只好选择字符串为参数的那个索引器, 因为我们不能假设数据库返回结果中数据列的顺序。 可以想像一下:如果我们要加载一个数据实体列表,它包含30条记录,每个数据实体又包含10个数据成员, 那么,这种查找过程将重复300次! 因此,它对性能的损耗是不可忽视的。

ClownFish的加载方式则不同,它一定会调用整数序号的那个索引器版本, 因此,完全没有这些性能开销。

我曾听过我身边的朋友谈过他们的想法:我也可以在手工代码中,加入一个Dictionary<string, int>对象来保存这种索引关系, 然后,在每次取值时,从Dictionary获取索引序号,然后再去调用DbDataReader的整数序号的那个索引器。

如果您也是这样想的,那么,我该打断一下您的思路了:从Dictionary查找难道不花时间吗?

所以,采用Dictionary保存名称到序号的思路肯定不是最优的。
ClownFish的这部分实现中,是完全没有这种查找开销的。

那么,如何才能做到完全没有查找开销呢?
我想很多人应该会关心这个问题。
其实答案并不复杂,首先,我们确实需要一张索引表,它保存了从名称到序号的映射关系。
如果您认为Dictionary是实现这种数据关系理想的方法,那么也可以。

下面我来告诉你此时的解决方法:
我们直接根据Dictionary做foreach迭代,每次是不是都可以同时拿到一对 name与index?
有了index,是不是可以直接调用GetValue,取得数据结果,
另外,由于已经得到了名称,是不是可以知道该写到实体对象的哪个数据成员上去?
所以,采用这种方法,完全可以避开【从名称到序号的查找过程】,将性能的影响减少到最低。

当然了,这只是一个思路,在实现时需要根据这个思路去设计代码生成器。

类似的,DataRow也支持这样二种索引器,优化的方法是一样的。

尽量使用专用版本的读取方法

DbDataReader,DataRow在读取数据时,不仅仅只提供索引器这一种方法,还提供了一些专用方法:

public abstract bool GetBoolean(int ordinal);
public abstract byte GetByte(int ordinal);
public abstract char GetChar(int ordinal);
public abstract string GetDataTypeName(int ordinal);
public abstract DateTime GetDateTime(int ordinal);
public abstract decimal GetDecimal(int ordinal);
public abstract float GetFloat(int ordinal);
public abstract Guid GetGuid(int ordinal);
public abstract short GetInt16(int ordinal);
public abstract int GetInt32(int ordinal);
public abstract long GetInt64(int ordinal);

由于这些方法都是针对特定类型做过优化,因此使用它们可以获得更好的性能,还能减少装箱折箱的次数。
平时我们确实也不曾调用它们,因为调用它们需要一个序号。
但是现在不一样了,在前面的小节中我已经解决了这个问题, 所以,ClownFish在生成代码时,会选择最合适的方法来调用。

那么,ClownFish又是如何该知道调用哪个方法呢?
答案是:根据实体类型的数据成员的类型来决定,ClownFish会通过反射方法来获取实体类型的定义, 自然可以知道它包含了多少数据成员以及它们的数据类型。

ClownFish的实体加载器

前面二个小节,我介绍了ClownFish在生成代码时会采用的一些优化措施, 那么,这些生成的代码又是如何组织的呢?

ClownFish会分别为每个实体类型生成一个实体加载器,内部称为ModelLoader,它包含这些方法:

public TModel GetItemFromReader(DbDataReader reader)
public List<TModel> GetListFromReader(DbDataReader reader, int capacity)
public TModel GetItemFromTable(DataTable table)
public List<TModel> GetListFromTable(DataTable table, int capacity)

public object GetValue(object obj, string name)
public void SetValue(object obj, string name, object value)

前4个是用于加载数据实体(或列表)的,后二个是用于访问实体数据成员的(避开反射调用)。

从这些加载实体的方法可以看出,最复杂情况:加载列表是在加载器内部完成的。 由于使用动态生成的代码,因此,中间代码没有object委托的那种装箱折箱对性能的损耗, 对性能的提升也是有帮助的。

ClownFish提倡使用参数化的SQL语句,调用多个参数的SQL语句或者存储过程时, 可以传递一个自定义类型的对象。在运行时,表现为需要读取这些参数对象中的数据成员。 为了避免使用反射,ClownFish直接生成了GetValue, SetValue方法,从而完全避开了反射调用。

例如,GetValue方法其实是一段很简单的代码,但它可以避开反射调用:

public static object GetValue(object obj, string name)
{
    Product item = (Product)obj;
    switch( name ) {
        case "ProductID":
            return item.ProductID;
        case "ProductName":
            return item.ProductName;

        default:
            throw new ArgumentOutOfRangeException("name", 
                    string.Format("Property or field {0} not found.", name));
    }
}

说明:这些代码是ClownFish在运行时生成的,并不需要我们去实现。

本文前面部分说了Emit也不是最优秀的方案,下面我来通过一个例子再次解释我的理由。 还是前面那个例子:“如果我们要加载一个数据实体列表,它包含30条记录,每个数据实体又包含10个数据成员。” 由于Emit比较复杂,所以现在能找到的包装方法通常只能实现针对一个属性(或字段)生成一个访问委托。 委托生成好了,我们保存在哪里? 要选择一个容器吧,我想几乎绝大多人会选择Dictionary<T, K>的实例中。 悲剧也就从这里开始了!

你可以计算一下,在将数据库的结果转成实体列表时,需要查找Dictionary多少次,是不是300次?
前面好不容易优化了一个300次查找,现在又要来一个300次查找!

ClownFish的实现中,根本没有这么多的查找过程,因为动态生成的代码可以直接调用, 且包含了循环部分的代码。因此,要比其它反射方案要高效很多。

对并发的优化

Emit与Dictionary的悲剧故事还未结束。

由于Dictionary不是线程安全的类型,所以读写都需要上锁。 然而很多人又非常懒,喜欢直接使用lock, 这样又会导致在高并发时性能低下的问题。 虽然可以采用ReaderWriterLock来 缓解 这个问题,但是我并没有选择它,因为它有点麻烦,而且读写之间仍然会有阻塞。

ClownFish的内部实现中,也需要缓存一些类型描述信息, 我选择了Hashtable。因为我看到MSDN中对Hashtable有这样一段描述:

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

根据这个特性,我可以直接读取Hashtable,仅当Hashtable不存在结果时,才去lock一下, 因此,真正出现锁的时候极少。 如果采用在初始化时编译实体类型,那么就完全没有锁的影响了。

另一方面,由于ClownFish会为每种实体类型生成一个加载器, 因此,也大大降低了对缓存容器的查找次数,将锁定的需求减少到最低水平。

小结:ClownFish对并发的优化主要采用了专用加载器与Hashtable的方法, 这二种方法的结合使用,可以最大限度减少锁的使用,提高并发吞吐量。

ClownFish的实体编译方法

为了能让ClownFish在运行时拥有最优秀的性能,ClownFish选择了动态生成代码并编译的方法, 而且,为了避免在生成代码、编译代码期间对调用线程的阻塞影响, ClownFish采用了后台线程的方式来处理,如果要加载的实体类型的加载器没有编译完成, 会按照老版本的方式直接采用反射的方式来执行,一旦加载器编译完成,则后续调用将会使用加载器。

所以,这里又涉及到加载器的生成及和编译时机问题。

在这个编译时机问题上,我更关注的是性能影响,所以设计了编译模式的概念来解决。
而且,为了保证ClownFish能满足各种使用场景,ClownFish提供了三种编译方法:

public static class BuildManager {
    // 自动编译模式,此模式会自动收集待编译的数据实体类型。 public static void StartAutoCompile(Func<bool> func)
    public static void StartAutoCompile(Func<bool> func, int timerPeriod)

    // 手工提交编译模式,此模式要求手工提交需要编译的数据实体类型。 public static void CompileModelTypesSync(Type[] types, bool throwOnFailure)
    public static void CompileModelTypesAsync(Type[] types)
}

【手工提交编译模式】适合在程序初始化时调用,它又分为【同步】提交和【异步】提交二种方式。
如果所有的数据实体类型有一定特征,那么可以考虑这种方式。调用方法:

Type[] models = BuildManager.FindModelTypesFromCurrentApplication(
                            t => t.FullName.StartsWith("Test.Models."));
BuildManager.CompileModelTypesSync(models, true);

或者调用异步版本:

Type[] models = BuildManager.FindModelTypesFromCurrentApplication(
                            t => t.FullName.StartsWith("Test.Models."));
BuildManager.CompileModelTypesAsync(models);

在示例代码中,由于所有的数据实体类型都定义在 Test.Models 命名空间中, 所以只需二行代码就可以为所有的实体类型生成加载器供运行时调用。

【自动编译模式】的启动也很简单:

// 启动自动编译数据实体加载器的工作模式。 // 编译的触发条件:请求实体加载器超过2000次,或者,等待编译的类型数量超过100次 BuildManager.StartAutoCompile(() => BuildManager.RequestCount > 2000 || BuildManager.WaitTypesCount > 100);

// 启动自动编译数据实体加载器的工作模式。每10秒【固定】启动一个编译过程。 // 注意:StartAutoCompile只能调用一次,第二次调用时,会引发异常。 //BuildManager.StartAutoCompile(() => true, 10000);

说明:
1. 手工提交编译的方法可以多次调用,每次调用都可能会生成一个程序集。
2. 手工提交编译模式与自动编译模式可以混用。
3. 自动编译模式的方法只能调用一次。

好了,还是继续讨论性能问题。
在程序初始化时,如果采用【手工提交编译模式】,显然在运行时会有最出色的性能。

如果您的项目中,数据实体类型定义比较混乱,那么可以选择【自动编译模式】。 当ClownFish发现某个数据实体类型的加载器没有编译生成时, 会记住这个类型,然后在某个时刻一起编译这些缺少加载器的实体类型。

注意,ClownFish并没有选择那种【按需生成】的方法, 我认为启动代码生成器与编译器是件昂贵的操作,会占用较多的工作线程时间, 尤其是需要频繁启动时,成本会比较高,所以,我选择了后台线程与批量处理的方式。

在【自动编译模式】下,ClownFish会被一个Timer触发,然后检查是否需要启动代码生成代码编译的任务, 如果确实有类型需要编译,才会真正启动任务。所以,这样的设计会减少动态生成的程序集数量, 也会减少启动编译器所带来的工作负担,毕竟一个项目中,实体类型的数量可能会很多,批量编译能节省很多资源。

 

好了,【揭秘 ClownFish 比手写代码还快的原因】的文字部分到此为止, 希望本文能给那些还在开发维护数据访问层的朋友一些参考, 也希望告诉那些正在研究改进反射性能的朋友:优化大量反射的终极方法应该是动态代码生成技术。