2008年3月24日星期一

深入理解 ASP.NET 动态控件 (Part 6 - 模板控件)

在之前的文章中,我极力推荐大家使用Repeater和MultiView这类TemplateControl,为什么呢?因为只有这样做,才算是符合MVP或MVC模式。(到底是MVP还是MVC,这视乎你选用什么呈现引擎了。)

虽然我们要动态创建控件,但实际上这部分控件仍然属于View的部分,我们应该尽量采用ASPX的声明性名义来描述这些控件,避免用C#代码来创建控件、设置属性并添加为子控件。就拿最简单的例子来说,创建一个LinkButton,通常我们都需要设置它的ID、Text、OnClick属性/事件,甚至还要设置OnCommand、CommandName、CommandArgument等属性/事件,那就是大概3到5个属性了,用ASPX来声明只需要1~2行代码,而用C#代码则需要写至少5行(把new和Add()也算上的话),由此可见在定义控件这类声明上ASPX比C#代码的可读性要高。

接下来,我们来研究一下TemplateControl是如何工作的,这自然要从如何编写一个TemplateControl讲起。

编写模板控件

在这里,我们假设要编写一个SimpleRepeater控件,自身不支持数据绑定,只有唯一一个名为ItemTemplate的模板,并且就按照Count属性指定的次数重复出现该模板。首先,让我们来定义这两个属性:
public ITemplate ItemTemplate { get; set; }
public int Count { get; set; }

因为ItemTemplate属性不是以键值对的形式在SimpleRepeater的声明中给出的,而是以内嵌一对标签的方式定义的,因此我们需要让解释器去把<ItemTemplate>...</ItemTemplate>中间的内容读取出来,并把解释结果作为ItemTemplate属性的值处理。这时候,我们就需要为SimpleRepeater类加上ParseChildrenAttribute,也就是这样子:
[ParseChildren(true)] public class SimpleRepeater {...}

最后,我们需要重载一下CreateChildControl()方法,把ItemTemplate的内容作为子控件添加到SimpleRepeater之内:
protected override void CreateChildControls()
{
if (ItemTemplate != null)
{
Controls.Clear();
for (int i = 0; i < Count; i++)
{
Control control = new Control();
ItemTemplate.InstantiateIn(control);
Controls.Add(control);
}
}
}

这段代码的用意应该是相当清晰的了,就是循环Count指定那么多次,每次循环创建一个空白的Control,用ItemTemplate.InstantiateIn()方法填充它,最后把它添加到SimpleRepeater.Controls里面,就那么简单。那么这个神秘的InstantiateIn()方法到底是干什么的呢?后面来解释。

编译模板控件

在之前的《深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)》里面,我详细地解释了ASP.NET 2.0的编译模型。在《深入理解 ASP.NET 动态控件 (Part 5 - 编译实验)》中,我们又做了一个动手实验,亲眼看到了ASPX和C#代码是如何编译到一起的。现在让我们来看看当碰上模板控件时,代码会被如何编译吧。我们把上面编写的SimpleRepeater注册到页面上,前缀为ctrl,并且编写如下一段代码:
<ctrl:SimpleRepeater ID="SimpleRepeater1" Count="10" runat="server">
<ItemTemplate>
<asp:Button ID="Button1" Text="Button" runat="server" />
</ItemTemplate>
</ctrl:SimpleRepeater>

然后还是用aspnet_compiler编译一下,并且用Reflector打开编译出来的dll看看。我们可以看到构造SimpleRepeater实例是通过这样一个语句完成的:
return new SimpleRepeater { ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control1)), Count = 10 };

这个语句其实就是一个普通的new语句,并且给ItemTemplate和Count两个属性赋值了,唯一值得关注的就是ItemTemplate的值。ItemTemplate的类型是ITemplate,因此任何实现了ITemplate接口的类都可以复制给它,然而我们却从来没指定过到底要赋值什么类型的实例给它,因为这已经由ASP.NET帮我们想好了,假若我们不指定的话,那就是CompiledTemplateBuilder类型。还是熟悉的Builder模式,它只需要已委托的形式接受一个实例化ITemplate的函数,然后就能返回实例化好的ITemplate控件子树。可能你会问,既然我已经有实例化ITemplate的函数,干什么要先传给你CompiledTemplateBuilder,让你来调用一下,再把实例化好的给我?我自己实例化不好吗?在此,ASP.NET引擎的做法只是为了保持Builder模式的一致性,处处用Builder模式来分离逻辑而已。

那么这个用于实例化ITemplate的函数从哪来呢?在解释器进入到<ItemTemplate>...</ItemTemplate>内部时,它会继续层层构建Builder模式,就如同在整个页面内执行的一样。因此,整个<ItemTemplate>...</ItemTemplate>会被解释器转化为一个函数,它也是通过层层调用内部函数完成自身的控件子树的构建,传递给BuilderTemplateBuilder构造函数的委托正是指向此函数。

因此,模板控件里面的内容将如同模板外的内容一样,被无缝地解释和构建到一起来。

INamingContainer

如果你查看SimpleRepeater输出的HTML代码,你会发现里面有10个<input id="Button1" name="Button1" type="button" value="Button" />。我们都知道,重复的id是不符合标准的,因此我们需要通过INamingContainer把这个问题解决掉。因为重复的控件是ITemplate,所以应该对它加上INamingContainer,然而它的实例编译时自动使用了CompiledTemplateBuilder,我们如何把INamingContainer加上去呢?我们就只能把INamingContainer加到它的父控件上面去。此时,我们需要一个实现了INamingContainer的简单控件:
public class SimpleRepeaterItem : System.Web.UI.Control, System.Web.UI.INamingContainer {}

然后我们把SimpleRepeater.CreateChildControls()方法的这个语句:Control control = new Control(),替换为:SimpleRepeaterItem control = new SimpleRepeaterItem()。这样,ITemplate的容器就变成了一个具有INamingContainer接口的控件,这时候各个Button的客户端id就会自动加上其容器的id作为前缀,因为容器的服务器端ID是自动变好的,所以必然是各不相同的,这样就解决了Button客户端id相同的问题。

通过这个例子,我们了解到了编写模板控件时必须为模板的容器加上INamingContainer,因为模板内的控件ID命名是可能重复的,加上INamingContainer就可以避免它们的客户端id重复。

小结

这次的文章解释了为什么我们应该尽量使用模板控件来实现动态控件,并且也说明了如何编写自己的模板控件,以及模板控件最终是被如何编译为Builder模式的代码的。

2008年3月22日星期六

每个 blog 都会有一堆 draft posts

lulu说她有个blog post选题的文件夹,我也有一堆blog post都draft存放于各处,可能是邮箱中,可能是Blogger的草稿中,也可能是Windows Live Writer等软件的草稿中。通常,一篇写得好的长文章都不是即兴而作的,而是用draft慢慢写出来的。有点点feel的时候,要逼着自己写成一篇完整的文章很难,反而当有几个draft都是同一个相近的题目时,可以合并在一起写成一篇好文章。

这估计是因为,如果只是有一点灵感,这是不全面的,很难自圆其说。当几个灵感合并在一起,就能看到概貌了,前因后果清晰了,正方反方都出场了,自然写出来的文章就会显得内容丰富一些。

2008年3月21日星期五

深入理解 ASP.NET 动态控件 (Part 5 - 编译实验)

这次的文章是一个小小的动手实验,你需要准备好Visual Studio 2005或者Visual Studio 2008,以及最新版本的Reflector。通过这次的实验,你将对ASPX与C#代码如何合并编译为一个dll代码有所理解。

在实验开始之前,首先来一个小问题:如果不允许你使用ASPX,要你完全使用C#代码写一个具备复杂控件树的页面你会怎么写?把声明控件的代码都放在Page_Load里面吗?或者有更好的代码编写方法?先想想这个问题,然后继续往下看。

实验的第一步,也就是在Visual Studio里面创建一个ASP.NET项目,并编写一个简单的ASPX页面。例如下面这个例子:(以下代码仅包括HtmlForm内的主体部分)
<asp:MultiView ID="MultiView1" runat="server">
  <asp:View ID="View1" runat="server">
    <div>Please choose either of the followings:</div>
    <asp:RadioButton ID="RadioButton1" runat="server" />
    <asp:RadioButton ID="RadioButton2" runat="server" />
  </asp:View>
  <asp:View ID="View2" runat="server">
    <div>Please choose any of the following:</div>
    <asp:CheckBox ID="CheckBox1" runat="server" />
    <asp:CheckBox ID="CheckBox2" runat="server" />
  </asp:View>
</asp:MultiView>

在这个例子中,我们构建了一个简单的控件树,同时又不至于过于复杂,确保了编译出来的代码相对简单一些。接下来我们就需要将它编译了,最简单的手动编译方法就是用ASP.NET 2.0自带的aspnet_compiler.exe,这个文件默认会在这个目录中:C:\Windows\Microsoft.NET\Framework\v2.0.50727。你可以使用aspnet_compiler -h来查看完整的帮助,例如编译一个IIS默认站点中的ASP.NET子站点可以使用这样的代码:
aspnet_compiler  -v / -p C:\inetpub\wwwroot\site C:\output\site

接下来,我们到输出目录的bin子目录里把dll抓到Reflector里面看看吧。你会看到这个dll里面有三个namespace,分别是-(在Reflector中代表没有namespace)、__ASP、ASP。假设你编译的站点有一个Default.aspx,那么在无namespace的类当中就会有一个_Default的类,对应的就是Default.aspx.cs编译出来的类。大家应该还记得《深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)》里面提到的,直接继承自Page的类是用后台代码编译出来的,_Default类就是这样一个具体例子了。我们打开_Default类来看看,就会发现MultiView1已经是其成员了,为什么呢?MultiView1仅仅在ASPX中声明,没有在C#中声明啊。回头看看《深入理解 ASP.NET 动态控件 (Part 2 - 编译过程)》就能解释了这种现象——Default.aspx.cs是标记为partial的,而在你手动编辑的文件中,这是唯一一个partial,另外一个partial由编译器根据Default.aspx自动生成,编译器解释完Default.aspx后在自动生成的partial中定义了MultiView1,因此两个partial合并编译后,_Default类自然就有了MultiView1这个成员了。

接下来,我们再看看ASP这个namespace下的default_aspx类,这个是ASPX文件继承自上述_Default类后编译的结果,它完整表述了ASPX文件中整个控件树的逻辑,而不仅仅是一个包含一堆成员控件定义的Page派生类。这个类的执行入口是FrameworkInitialize()方法,它通过调用__BuildControlTree()方法来构建控件树。在这个方法里面,你可以看到<!DOCTYPE ...>这样的字符串是被解释为LiteralControl的,LiteralControl在Render()时就会把这段文本原样输出。同时你还可以看到,它调用了另外两个方法,分别用来构建HtmlHead和HtmlForm,这两个方法通过类似的形式继续调用其他方法来构建更深层次的控件。

通过阅读default_aspx类的代码,你已经能够理解ASPX的控件树是如何转化为C#代码的了——采用的正是Builder模式。了解到这一点,这次动手试验的目的也就达到了。如果你看到文章开头的那个问题时,你已经想到了使用Builder模式,那么此时也就验证了你的想法是完全正确的。

下一次的文章将是与TemplateControl相关的,我们将继续动手做一些小实验,敬请期待。欢迎订阅我的blog:

2008年3月14日星期五

在校学生找实习、找工作、了解企业情况等等等等

因为时不时就有低年级的同学跑来问我这类问题,所以我干脆写篇文章好了。

信息获取

最先想到的,也是最重要的,是你想干什么,而不是你父母想你去干什么,或者哪个赚钱之类的。在计算机系里面,你总能碰到一些对这个行业没什么感觉的人,他们会说当年填报志愿的时候根本没什么喜欢不喜欢可言,于是在父母驱使下或者金钱诱惑下就报了计算机系。显然你不想犯这类错误,因此第一步是弄清楚你想要什么,或者说,有什么是你可以选择不要的。世界上没有十全十美的事情,所以一切都是权衡取舍(trade off),这个思想贯穿着计算机体系设计的方方面面,缺乏这种思想肯定会导致你在这个行业里发展受到限制,所以首先你要想好的就是你将来的职业发展取什么而舍什么。

在你先清晰了解自己之后,才有如何了解企业实际情况的问题。如果说第一个问题严重依赖于你个人悟性的话,那么第二个问题就依赖于你的人脉了,至少是你的外向程度和活跃程度。

一个企业,其自身对外的公关肯定只会说好话,就算没有任何夸张成分,也不会让你看到这个企业内部任何的不足。因此,如果你需要知道一些细节,看看这企业是否如你想象中那么好,你就必须认识在这家企业里面的员工,通过他们好好了解一下。如果你是一个在学校里已经很活跃的学生,整天在BBS上板聊,或者参加各种学生社团的活动,那么你肯定有机会认识到不少比你高一两个年级的同学,看看他们里面有没有一些正好在你想去的企业工作的,联系他们了解一下企业的情况,例如工资和各种福利怎么算啊,工作与学习的氛围如何啊,以及他们是否也认为你适合这个企业。

如果有机会多问几个人的话,最好都问问,并且以人生经历和价值取向与你相近的人的说法为主要参考。这是因为,在同一家企业里面,不同的员工看到它不同的方面,并且都带有自己的主观想法,因此得出的评价可能截然不同。这就好像假如你问我北京是否好住,我肯定说不好住,因为我看重的是商业环境,我觉得北京的服务业经常让我失望,因此就这样说。但肯定也有人很喜欢住北京的,他看重其他一些因素,并且觉得我看重的那些因素可以忽略不计。如果你确实很想去一家企业,就应该多找几个员工了解情况,特别是头脑清晰能看清楚自己所在企业优劣势的人。如果你碰到一个已经被洗脑的员工,那么问什么也没用,反正他就只能帮你洗脑……

联系途径

根据往年的经验,现在正是2009年毕业的学生找实习的时候,甚至有2010年毕业的学生也提前开始找实习了。积极的人肯定早已开始四处打听消息,包括各大公司招聘什么实习职位,往年难度如何,转正比例多少,等等。在这里,我就要“曝光”我身边的一些朋友了,大家不要怪我“出卖”朋友了哦,因为谁都知道成功把有才华的人推荐给自己公司的意义所在。

在这里,我主要说的是BGM(Baidu、Google、Microsoft的意思,不是background music),如果你对其他IT企业有兴趣的话,也可以找我帮忙,只要我认识该企业的人,我会充当一下“路由”的角色为你“尽力服务”的。

Baidu

如果分别把BGM比作人的话,Baidu是一个典型的中国年轻人,一点都不张扬。就如同在中国随手抓一个ASP.NET MVP问他,“你是不是很熟悉页面生命周期”,他可能回答道,“懂一点吧,有什么问题你先说说看。”虽然从资源(resources)的级别来说,Baidu是很难和Google、Microsoft去比的,但是开放程度(openness)还是是相当高的,你可以做自己喜欢做的项目,可以获取到项目所需的资源。

想去Baidu的可以找我,或者布丁,先了解一下。当然,如果你来找我的话,最好你也能说服我为什么值得推荐你。我主要做Web前端开发的,而布丁则是做后端开发的,所以如果你想申请这两个方面的职位,可以直接找我们了解Baidu现在所采用的技术或者流程。如果是其他职位,也可以帮你联系其他同事问问。完整的实习职位列表,请看这里

Google

Google是一个典型的美国年轻人,你不问他也会很主动地把他的优势展示给你看。在我看来,Google最大的诱惑不在于公司内的“饮食娱乐”项目(虽然这些也很吸引),而是作为一般的中国员工也可以随便跑到美国总部去,顺道参加美国举行的一些会议或者讲座。看看Junyu同学就跑到Austin去开SXSW了。

如果想去Google的话,去找Junyu问问吧。别告诉我找不到他的联系方式,我通常只链接别人的blog,因为该URL就是个人的标识(即使抛开OpenID不谈),拿着这个URL你手上就已经掌握了搜索他联系方式的一切资料。Junyu喜欢把人拐卖进Google里,想了解招聘职位什么的,找他就好了。

Microsoft

Microsoft是个稳重的美国中年男人,Google所晒的东西,Microsoft会暗自想着“我当年又不是没晒过,现在我已经不屑于晒了”。很成熟的流程,健全的管理体制,都很好地说明了这一点。

貌似我没有很熟的朋友在Microsoft,不算太熟那些又不敢晒出来,所以想找人推荐的话还是联系我,然后我再帮你联系吧。想了解的话,可以找Jeffrey或者Dflying这两位老员工加现任MVP,他们现在不再是员工了,可能观点会更加客观一些。

其他事情

据我所知,很多人也是准备找实习了,才想到要写简历的。这是一项需要创意的工作,在此我提供我的简历Junyu的简历给大家作为参考。虽然我们的网站下方都写着Creative Commons License,不过不建议你直接使用别人设计好的模板。特别是,假如你想申请Web Developer/Designer类别的职位的话,你可不能够错过这个机会好好展示你的设计风格和编码艺术。

最后,祝大家在08年里都能找到自己喜欢的工作学习环境,好好享受生活。

2008年3月8日星期六

使用 .NET 实现 Ajax 长连接 (Part 2 - Mutex Wait & Signal)

在上一次的文章中,我们说到了如何设计一个ASP.NET Web Service来处理长连接请求。很多人对此就提出了问题,如何hold住请求让它30秒不断开了?这其实很简单,只需要Sleep()一下就可以了:

Thread.Sleep(30 * 1000);

然而问题是,我们不是要等30秒然后看看是否有事件需要返回,而是在这30秒内随时有事件随时返回。因此,我们需要一套机制来在等待的过程中检查是否有事件发生了。

Monitor模型

在.NET里面,大家最熟悉的线程同步模型应该就是Monitor模型了。没听说过?就是C#的那个lock关键字,实际上它编译出来就是一对Monitor.Enter()和Monitor.Exit()。

通过lock命令,我们可以针对一个对象创建一个临界区,代码执行到临界区入口时必须获取到该对象的锁才能执行下去,并且在临界区的出口释放该锁。然而这种模型不太适用于解决我们的问题,因为我们需要等待一个事件,如果使用lock来等待的话,那就是说要先在Web Service外部把对象锁上,然后等事件触发了就解锁,这时候Web Service才顺利进入临界区域。

事实上,要进行这类型的阻塞,还有一个更好的选择,那就是Mutex。

Mutex模型

Mutex,也就是mutual exclusive的缩写,“互斥”的意思。Mutex是如何运作的?这有点像是银行的排队叫号系统,所有等待服务的人都坐在大厅里等候(wait)被叫,当一个服务窗口空闲时它就会发出一个信号(signal)来通知下一位等候服务的人。总之,所有执行wait指令的线程都在等候,而每一个signal能够让一个线程结束等候继续执行。

在.NET里面,wait和signal这两个操作分别对应Mutex.WaitOne()和Mutex.ReleaseMutex()这两个方法。我们可以让Web Service的线程使用Mutex.WaitOne()进入等候状态,而在事件发生时使用Mutex.ReleaseMutex()来通知Web Service线程。因为必须在Mutex.ReleaseMutex()发生后Mutex.WaitOne()才可能继续执行下去,因此能够执行下去就证明必然有事件发生了并且调用了Mutex.ReleaseMutext(),这时候就可以放心地去读取事件消息了。

简单示例

在选定使用Mutex模型后,我们来编写一个简单的示例。首先,我们要在WebService派生类内定义一个Mutex,还有一个代表消息的字符串。

Mutex mutex = new Mutex();
string message;

然后,我们定义两个WebMethod。为了把问题简单化,我们选用上一篇文章中开头所说的两个函数签名,也就说只能在一个Web Service内自己发自己收,没有发送目标的概念,也没有超时的概念,还没有可靠性设计。同时,我们将Message类型替换为普通字符串,以便于我们测试。

我们先编写发送消息的函数:

public void Send(string message) {
  this.message = message;
  this.mutex.ReleaseMutex();
}

在这个发送函数里,首先我们把消息放进了类内全局的变量中,然后让全局的Mutex类释放一个signal。这时候,如果有线程在等待,它可以马上执行下去。如果此时没有线程在等待,那么下一个wait的线程执行到该阻塞的地方就能够不受阻塞继续执行下去。

现在我们来编写接收消息的函数:

public string Wait() {
  this.mutex.WaitOne();
  return this.message;
}

接收函数一开始就进入wait状态。在得到signal后,需要做的事情就是把全局的消息返回给客户端。

亲身体验

最后,我们可以通过ASP.NET Web Service本身支持的Web测试界面来测试一下我们的代码。我们开两个浏览器窗口,一个进入Send()调用,一个进入Wait()调用。然后我们按照如下方法来测试:

  1. 首先执行Send("Hello"),然后执行Wait()。这时候你可以马上看到"Hello"。
  2. 首先执行Wait(),让它等待返回,这时候执行Send("Hello")。随后你可以看到Wait()那段返回"Hello"了。
  3. 按如下顺序执行:Send("Hello");Wait();Send("World");Wait();
  4. 按如下顺序执行:Send("Hello");Send("World");Wait();Wait();
  5. 按如下顺序执行:Wait();Wait();Send("Hello");Send("World");
  6. 按如下顺序执行:Wait();Send("Hello");Wait();Send("World");

你会发现这样一些奇怪的结果:第3个测试返回的是"World"和"World"。第5个测试先返回"Hello"的并不一定是先执行的那个Wait()线程。后者在某些情况下不是什么问题,特别是长连接中一般之后一个Wait()线程在等待中,所以我们可以不管。而前者,则是因为没有消息队列所造成的,我们只有长度为1的消息窗口,所以只能缓存最后一个消息。这个问题我们将在下一篇文章中解决。

小结

在本文中,我们看到了不同的线程同步模型的差异。Monitor模型的lock本质上是一个Semaphore,也就是一个不能连续signal的Mutex,一个signal发出去后必须被一个wait接收了才能进行下一次的signal。同时,Semaphore也限制了signal和wait必须在同一个线程内成对执行,而Mutex则没有此限制。虽然.NET是针对Monitor模型优化的,但在我们的需求当中,只能通过Mutex模型来解决。

接着,我们便写了一个小小的消协发送与接收函数,实现了我们想要的阻塞式Web Service。同时我们也看到了没有消息队列造成的问题,因此确定接下来我们要做一个消息队列。如果你想知道消息队列如何编写,欢迎订阅我的blog:

2008年3月4日星期二

拿到了美国签证

准备了一大堆的材料,包括在校证明和百度的工资证明,结果签证官只看了我的邀请函,问了几个问题就搞定了,真爽快。他问的问题包括,我现在在什么学校(企业),去美国干什么,MVP是什么,大概就这样了,超级简单,还弄得我之前紧张了那么久。

最有趣的地方是,原来驻广州的是美国总领馆,而驻北京北京的只是大使馆,移民业务都在广州处理。整个广州大使馆都是说广东话的,除非看到你的签证签发地是非广东话地区,那就会说普通话或者英语。在移民大厅内,新移民在广东话的带读下进行宣誓,非常好玩。