﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>博客园-Allen Lee's Magic</title><link>http://www.cnblogs.com/allenlooplee/</link><description>这里没有答案，顶多给你几个值得一试的猜想。</description><language>zh-cn</language><lastBuildDate>Thu, 28 Aug 2008 11:26:17 GMT</lastBuildDate><pubDate>Thu, 28 Aug 2008 11:26:17 GMT</pubDate><ttl>60</ttl><item><title>从C# 3.0到F#</title><link>http://www.cnblogs.com/allenlooplee/archive/2008/07/25/1251631.html</link><dc:creator>Allen Lee</dc:creator><author>Allen Lee</author><pubDate>Fri, 25 Jul 2008 11:18:00 GMT</pubDate><guid>http://www.cnblogs.com/allenlooplee/archive/2008/07/25/1251631.html</guid><wfw:comment>http://www.cnblogs.com/allenlooplee/comments/1251631.html</wfw:comment><comments>http://www.cnblogs.com/allenlooplee/archive/2008/07/25/1251631.html#Feedback</comments><slash:comments>100</slash:comments><wfw:commentRss>http://www.cnblogs.com/allenlooplee/comments/commentRss/1251631.html</wfw:commentRss><trackback:ping>http://www.cnblogs.com/allenlooplee/services/trackbacks/1251631.html</trackback:ping><description><![CDATA[<FONT face="Arial, Helvetica, sans-serif">
<P style="TEXT-ALIGN: center"><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>从C# 3.0到F# </STRONG></SPAN></P>
<P>&nbsp;</P>
<P style="TEXT-ALIGN: right"><SPAN style="COLOR: #0070c0"><STRONG><EM>Written by Allen Lee </EM></STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>缘起 </STRONG></SPAN></P>
<P>当你看到这篇文章的标题时，你有什么感觉？是不是很想脱口而出："到底搞什么飞机啊，我C#还没来得及用好，现在又搞个F#，还让不让人活啊？"《程序员修炼之道》曾经建议我们"learn at least one new language every year"，但<A href="http://duartes.org/gustavo/blog/">Gustavo Duarte</A>却对这种建议提出质疑，并宣称"learning new programming languages is often a waste of time for professional programmers"。面对这种争论，你可能会显示出某种理性：除非我有需要（学习新的语言），否则我认为够用就可以了。那么，你什么时候会有需要？回想一下你的项目经历，是否发现，有权提出这种需要的往往不是你，而是你的项目，只要项目有需要，即使是老掉牙的语言你也得学。根据马斯洛的需要层次理论，如果你的项目已经让你忙得一塌糊涂了，那么你根本不会有闲情和兴致学习新的语言，而在现今这个讲求快速见效的社会里，或许只有专门研究语言的人才支付得起学习新的语言的代价了，但我并非专门研究语言的人，至少现在不是，那么，我为何要学习新的语言呢？ </P>
<P>我曾经在杰拉尔德·温伯格（Gerald M. Weinberg）的《咨询的奥秘——成功提出和获得建议的指南》里读到一个有趣的"锤子法则"（The Law of The Hammer）： </P>
<P style="MARGIN-LEFT: 36pt"><STRONG>在圣诞节收到锤子做礼物的孩子会发现每样东西都需要敲打。 </STRONG></P>
<P>读完上面这句话之后，你的脑子里想着什么？或许你已经猜到我想说什么了，工具在为使用者带来便利的同时也会约束使用者解决问题的思路和方法，编程语言直接体现了对问题的抽象和表达，不同范式的编程语言则协助程序员从不同的角度把握问题，这也正是我学习新的语言的主要原因。要有持久的学习行为，学习动机应该是指向内部的，其道理和<A href="http://www.psychologies.com.cn/read/cataloginfo.jsp?booktype_id=1&amp;book_id=2&amp;catalog_id=28">《七个心理寓言》</A>的"动机的寓言：孩子在为谁而玩"所说的是一样的。那么，我又为何选择F#呢？其实，这完全是因为C# 3.0，我们知道C# 3.0向函数式编程借鉴了不少，所以在学习C# 3.0的时候，我突然萌生了想了解函数式编程语言的念头，后来，在一次偶遇中，我邂逅了F#。在学习F#的过程中，我发现许多C# 3.0的新功能的"影子"（有人说C# 3.0的新功能是从F#那里借鉴过来的，是真的吗？），于是萌生了写下这篇文章的念头。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>如何创建类型？ </STRONG></SPAN></P>
<P>人们在接触新事物时通常不会抛开现有的积累，换句话说，你的知识和经验会影响你如何接受新事物。如果你是一个有使用面向对象编程语言经验的程序员，那么你第一个想问的问题很可能就是："我如何创建类型？" </P>
<P>F#支持一种叫做Record Type的类型，它和C#里使用自动属性定义的类有点像： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F1.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 1 </STRONG></SPAN></P>
<P>而Book的实例化也是非常直观的： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F2.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 2 </STRONG></SPAN></P>
<P>F#会根据给出的属性名字以及值的类型推断出你要实例化的类型是Book。读到这里，你可能会问："如果有两个不同的类型定义了相同的属性呢？"虽然出现这种情况的概率不大，但若真的让你碰上了，你可以使用显式语法来实例化它： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F3.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 3 </STRONG></SPAN></P>
<P>面向对象编程的一个特征是封装，狭义的封装是指封装对象的内部状态（广义的封装则是指封装系统的变化因素），而对象的内部状态在对象的生命周期里发生改变是很常见的，但当我们试图在F# Interactive（类似于Python的交互式控制台）里修改Price属性时却报告错误（"&lt;-"用于赋值，相当于C#的"="）： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F4.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 1 </STRONG></SPAN></P>
<P>为什么会这样呢？原来，在F#里，对象默认是不可变的（immutable），就像.NET的字符串那样。修改Price属性可以看作创建一个新对象，把原对象的Title、Authors和Tags属性的值复制到新对象对应的属性，并为新对象的Price属性设置新的值： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F5.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 4 </STRONG></SPAN></P>
<P>读到这里，你可能在想：虽然现在内存很便宜了，但也不至于要用这种方法来耗啊？在面向对象编程里，拥有和维护可变的内部状态是对象的一个很重要的特征，正因为这样我们得以完成许多复杂的操作，但也正是可变的内部状态提高了并发操作的复杂程度和处理代价。泛泛而谈可变对象和不可变对象孰优孰劣是没有意义的，对于一个给定的系统，一些对象适合设计成可变的，另一些则应该考虑设计成不可变的，从而使两者达到一定的平衡。 </P>
<P>在C#里，对象默认是可变的，但你可以通过readonly关键字使某个（些）数据"固定"下来；F#刚好相反，对象默认是不可变的，但你可以通过mutable关键字使某个（些）数据"活动"起来。如果我把Book重新定义为： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F6.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 5 </STRONG></SPAN></P>
<P>那么修改Price属性就不会报错了。如果你决定使对象可变，那么你就应该做好并发处理的工作。什么？你的程序是单机单核单线程的？那你可以掷硬币决定对象是可变的还是不可变的。 </P>
<P>除了Record Type，F#还支持Discriminated Union、Tuple和Constructed Class Type，有兴趣的话不妨到<A href="http://research.microsoft.com/fsharp/fsharp.aspx">F# Home</A>看看。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>如何初始化对象？ </STRONG></SPAN></P>
<P>C# 3.0引入了对象初始化器和集合初始化器，F#也提供了类似的功能。举个例子，假设我想初始化System.Windows.Forms.ListViewItem，并且设置它的Text、Selected和ToolTipText属性，我可以这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F7.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 6 </STRONG></SPAN></P>
<P>F#把这个功能叫做初始属性设置（initial property settings）或者可选属性设置（optional property settings）。上面代码等效于： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F8.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 7 </STRONG></SPAN></P>
<P>从这里可以看出，使用这个功能的前提条件是要初始化的属性必须具有set访问器。读到这里，你可能会问："如果我要调用的构造函数是有参数的呢？"那也没问题，举个简单的例子，假设你要调用接受一个字符串作为参数的那个构造函数，你可以这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F9.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 8 </STRONG></SPAN></P>
<P>或者这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F10.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 9 </STRONG></SPAN></P>
<P>第一种方法就是简单地把你要初始化的属性追加到构造函数的参数后面；而第二种方法则使用了F#的命名参数（Named Argument）功能。命名参数可以放在任何位置，例如Selected和ToolTipText之间，但匿名参数就必须按顺序放在要初始化的属性前面。读到这里，你可能会问："如果我要调用的构造函数的参数和我要初始化的属性重名了呢？"你在考验F#的忍耐力吗（笑）？当然，这种情况是有可能出现，首先，F# 不允许同一个名字出现两次，不管它是构造函数的参数的名字还是属性的名字，所以你不可能鱼与熊掌兼得（即构造函数的参数和属性同时初始化）；其次，一旦出现这种情况了，F#会优先考虑构造函数的参数。 </P>
<P>我们探讨了如何初始化一个对象，那么初始化一组对象又是怎样的呢？在F#里，说到集合类型就不得不提Microsoft.FSharp.Collections.List&lt;'a&gt;了（"'a"是F#的类型参数表示法）。假设我要实例化一组Book对象（Book的定义参见代码1），并把它们储存在List&lt;'a&gt;里，我可以这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F11.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 10 </STRONG></SPAN></P>
<P>列表里的每个元素通过";"分割。F#能够结合元素的类型推断出列表的完整类型，在这里是List&lt;Book&gt;（也可以表示为Book list）。F#的List&lt;'a&gt;通常只在F#里使用，如果要访问.NET的类库或者和其他语言交互，那么你通常会考虑使用数组（F#的List&lt;'a&gt;和数组的语法非常接近，能看出其中的区别吗？）： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F12.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 11 </STRONG></SPAN></P>
<P>而对于整数列表，F#还支持区间表达式，你可以指定起始值和终止值： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F13.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 2 </STRONG></SPAN></P>
<P>甚至指定递增值（步长）： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F14.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 3 </STRONG></SPAN></P>
<P>如果你有兴趣进一步了解F#的List&lt;'a&gt;，可以阅读<A href="http://diditwith.net/default.aspx">Dustin Campbell</A>的<A href="http://diditwith.net/2008/03/03/WhyILoveFListsTheBasics.aspx">《Why I Love F#: Lists - The Basics》</A>和<A href="http://blogs.msdn.com/chrsmith/default.aspx">Chris Smith</A>的<A href="http://blogs.msdn.com/chrsmith/archive/2008/07/10/mastering-f-lists.aspx">《Mastering F# Lists》</A>。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>如何外包逻辑？ </STRONG></SPAN></P>
<P>有一次，我和两个朋友到东方既白吃饭，选餐的时候，其中一个考虑了很久，终于发话了："椰香咖喱牛肉饭可不可以不要椰香？"服务员看着我的朋友，非常不好意思地说："这是不可以的。"看到服务员的表情，我猜她应该是苦于不知如何向一个12岁的小朋友解释"烧饭的工作遵循了一套标准化的流程，这个流程是不能随意更改的"。此时，我的朋友大概在想：同样的钱，不能加东西可以理解，为什么连减东西也不可以呢？虽然他最后还是选了椰香咖喱牛肉饭，但我猜他心里肯定觉得东方既白做得太呆板了。试想一下，如果你打算使用我提供的Sort方法排序books2数组（参见代码11），却发现这个方法只接受一个数组作为参数，你肯定会问："我如何告诉这个方法我要根据价格进行排序？"接着，我告诉你："不好意思，这是不可以的，这个方法会自行选择合适的排序依据。"此时，你会有什么感觉？ </P>
<P>无可否认，我们已经进入了一个个性化的时代，用户不再像从前那样满足于你所提供的普遍适用的标准化软件，他们希望你的软件是可配置的，必要时还能够扩展，也就是可以满足他们的个性化需求。然而，把逻辑外包出去并不只是为了满足用户的个性化需求，为什么这样说？试想一下，你可不可以写出这样一个Sort方法，每次调用时都能"猜中"用户的排序依据？很明显，当我把books2数组传给Sort方法时，如果我不说，它不可能知道我想按书名排序还是按价格排序，是升序还是降序。换句话说，把逻辑外包出去其实就是把这种不稳定的因素封装起来，再转嫁给用户，然后美其名曰"用户参与"，当然，由于用户认为你不是把麻烦抛给他，而是为他带来灵活性，于是造就了"双赢"。 </P>
<P>考察System.Array.Sort方法的众多重载版本，不难发现.NET外包逻辑的两种主要方式是：委托和接口。在F#里，我们可以通过Lambda表达式向接受委托作为参数的重载版本注入逻辑（<A href="http://research.microsoft.com/fsharp/manual/FSharp.Core/Microsoft.FSharp.Core.Operators.html">compare</A>函数是F#提供的通用比较函数）： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F15.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 12 </STRONG></SPAN></P>
<P>当然，使用命名函数注入逻辑也是可以的： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F16.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 13 </STRONG></SPAN></P>
<P>代码13除了向我们示范如何在F#里定义函数，还向我们展示了一个有趣的东西，留意comparePrice函数的定义，我并没有为x和y这两个参数指定类型，但F#却从函数体以及上下文推断出它们的类型是Book！ </P>
<P>另外，F#并没有刻意区分命名函数和Lambda表达式，代码13的comparePrice函数也可以这样定义： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F17.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 14 </STRONG></SPAN></P>
<P>代码13和代码14定义的两个comparePrice函数是等效的，使用上也没有区别，从代码14可以看出，在F#里，函数其实就是值，而我们在代码12里使用的Lambda表达式只不过是代码14定义的comparePrice函数的函数体。 </P>
<P>Lambda表达式使你能够以一种紧凑的方式注入逻辑，但如果别人外包逻辑的方式是接口而不是委托呢？这个时候就轮到<A href="http://research.microsoft.com/fsharp/manual/spec2.aspx">对象表达式（Object Expression）</A>出场了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F18.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 15 </STRONG></SPAN></P>
<P>在这里，我通过"_"告诉F#我希望它帮我推断IComparer&lt;'a&gt;的类型变量，而F#也不负所托，成功推断出它的类型是Book。F#的对象表达式也算是一种匿名类型，但它和C# 3.0的匿名类型是不同的。在F#的对象表达式里，你可以实现接口的成员或者重写基类的成员，但不能添加任何新的成员；而C# 3.0的匿名类型则只允许属性的存在。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>如何扩展类型？ </STRONG></SPAN></P>
<P>假设我要把一组Book对象添加到System.Windows.Forms.ListView上，我应该怎样？对于习惯运用命令式编程方式思考问题的人，他可能会首先想到创建一个ToListViewItem函数： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F19.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 16 </STRONG></SPAN></P>
<P>然后"foreach"那组Book对象，对每个Book对象应用ToListViewItem函数，并把函数返回的结果添加到ListView。由于ToListViewItem函数是一个和Book对象相关的操作，你也可能会考虑把它纳入Book类型的定义，使它变成Book类型的实例成员函数： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F20.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 17 </STRONG></SPAN></P>
<P>需要说明的是，ToListViewItem成员函数前面的"b"指代当前的对象实例，相当于C#的"this"关键字和VB.NET的"Me"关键字，但因为F#没有强制使用特定的关键字，所以你可以根据具体的需要选择合适的符号，当然，你也可以通过制定命名规范强制使用特定的符号。现在，你可以"foreach"那组Book对象，对每个Book对象调用它的ToListViewItem成员函数，并把函数返回的结果添加到ListView。这个时候，可能会有人提出质疑："Book类型本来是一个中立的数据载体，现在你在它的成员函数里使用了ListViewItem类型，无疑强化了Book类型和Windows Forms框架之间的关系，有损它本身的纯粹性。"面对这样的质疑，你有什么想法？ </P>
<P>事实上，我们可能希望保留Book类型和ToListViewItem函数之间的"所属"关系，但这种关系会一直处于封印状态，直到我们通过某种仪式将它解封才可以使用。这个时候，我们可以考虑通过F# 的<A href="http://research.microsoft.com/fsharp/manual/spec2.aspx">类型扩展（Type Extension）</A>把ToListViewItem成员函数分离到一个单独的模块里（假设Book类型位于Lib命名空间里）： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F21.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 18 </STRONG></SPAN></P>
<P>这样，如果我们没有引用Ext模块，Book类型和ToListViewItem函数之间的关系就会继续保持封印状态： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F22.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 4 </STRONG></SPAN></P>
<P>而当我们在代码里引用Ext模块时，他们之间的关系就会解封： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F23.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 5 </STRONG></SPAN></P>
<P>有没有觉得F#的类型扩展和.NET Framework 3.5的扩展方法很像？ 事实上，F#的类型扩展有两种形态：当类型扩展的代码和相关的类型处在相同的命名空间里时，它等效于分部类，F#把这种扩展称为固有扩展（Intrinsic Extension）；而当两者处于不同的命名空间里时，它相当于扩展方法，F#把这种扩展称为可选扩展（Optional Extension）。但由于类型扩展和扩展方法在IL层面的实现方式是不同的，于是F#的类型扩展无法被C# 3.0/VB.NET 9.0识别，而C# 3.0/VB.NET 9.0的扩展方法也无法被F#识别。你可能会感到很奇怪：既然.NET 3.5已经提供了现成的实现方案，为什么F#还要另外弄一个出来呢？其实，现在的F#（1.9.4）是基于.NET Framework 2.0而不是3.5的，所以同时存在两种不同的实现方案并非有意的，不过<A href="http://blogs.msdn.com/dsyme/">Don Syme</A>说将来F#会改用.NET Framework 3.5的实现方案，即代码18定义的类型扩展也能被C# 3.0/VB.NET 9.0识别。 </P>
<P>那么，我现在是否可以在F#里定义能被C# 3.0/VB.NET 9.0识别的扩展方法？当然可以，虽然F#现在无法识别扩展方法，但这并不妨碍你在F#里定义这种方法： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F24.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 19 </STRONG></SPAN></P>
<P>在F#里定义扩展方法的语法和在VB.NET 9.0里的语法相似，都是显式使用ExtensionAttribute的。接着，把代码编译成DLL，在C# 3.0或者VB.NET 9.0项目里引用一下就可以使用了，编译的时候记得在F#的项目属性里设置.NET Framework 3.5的相关DLL的引用。 </P>
<P>噢，说着说着，差点忘记原本的目的了，我们做了这么多，最终还是难逃"foreach"那组Book对象，对每个对象应用变换操作，并把变换结果添加到ListView，那么，F#有没有提供更简单的方法可以用来完成这项任务呢？其实，F#更倾向于通过函数的组合来完成相关的工作： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F25.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 20 </STRONG></SPAN></P>
<P>上面这行代码向我们揭示了数据的流动：books2是数据源，它的数据流经Array.map函数和ToListViewItem函数组合而成的变换函数，然后流向listView.Items.AddRange方法。我们也可以这样理解这行代码：用ToListViewItem函数对books2数组的每个元素做变换操作，然后把结果传给listView.Items.AddRange方法。从这行代码可以看出，运用函数式编程方式来思考这个问题，我们最初定义的ToListViewItem函数就已经足够了。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>如何查询数据？ </STRONG></SPAN></P>
<P>C# 3.0最引人注目的地方莫过于使用LINQ查询数据了，前面提到，目前的F#是基于.NET Framework 2.0的，那么它又如何查询数据呢？我们知道，IEnumerable&lt;'a&gt;是LINQ的核心接口，也是执行任何查询操作的必要条件。在F#里，你可以使用seq&lt;'a&gt;或者IEnumerable&lt;'a&gt;，seq&lt;'a&gt;是F#为IEnumerable&lt;'a&gt;提供的类型缩写（Type Abbreviations），相当于C++的typedef，而适用于seq&lt;'a&gt;的函数则位于Microsoft.FSharp.Collections.Seq模块里，例如Seq.filter函数、Seq.map函数、Seq.orderBy函数等。 </P>
<P>假设我现在想用F#对代码10的books做一个查询，找出Tags属性里包含"F#"字眼的书，然后根据Price属性进行排序，那么我该如何做呢？ </P>
<P>要判断某本书是否符合我的条件，可以把它的Tags属性按";"符号分割成一个字符串数组，然后判断这个数组里面是否包含"F#"字眼。要把一个字符串按指定的符号分割成一个字符串数组，可以使用System.String.Split实例方法，但要判断一个集合是否包含指定的元素，除了通过Lambda表达式，似乎没有更加直接的方法，所以我仿照System.Linq.Enumerable.Contains方法定义了一个contains函数： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F26.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 21 </STRONG></SPAN></P>
<P>contains函数使用了Seq.exists函数，并且通过Lambda表达式告知判断条件为是否相等。你可能会感到很奇怪：为什么没有给contains函数的参数指定类型却可以通过编译？如果这个代码是合法的，那么value和source参数的类型是什么？当F#的编译器看到这段代码时，它发现value和source参数将会用作Seq.exists函数的参数，于是便试图从Seq.exists函数的签名推断value和source参数的类型，由于Seq.exists函数是一个泛型函数，而我们又没有在定义contains函数时给出进一步的约束，于是便断定contains函数也是一个泛型函数，并自动为value和source参数添加类型参数， F#把这个过程叫做自动泛型化（automatic generalization）。另外，你也可能感到奇怪：contains函数的参数顺序和Enumerable.Contains方法恰好相反，为什么呢？我们知道， Enumerable.Contains方法把表示集合的参数放在第一个位置是为了满足扩展方法的要求；而我在这里把表示集合的参数放在contains函数的参数列表的最后一个位置则是为了满足"|&gt;"运算符的要求。我们在代码20已经见识过"|&gt;"运算符了，它的作用是可以把目标函数的最后一个参数提到运算符的前面，换句话说，如果我要判断coll集合里是否包含elem元素，contains函数就可以这样用：coll |&gt; contains elem。如果你有兴趣进一步了解"|&gt;"运算符，可以阅读<A href="http://lorgonblog.spaces.live.com/">Brian</A>的<A href="http://lorgonblog.spaces.live.com/Blog/cns!701679AD17B6D310!165.entry">《Pipelining in F#》</A>。 </P>
<P>有了上面的准备，我们就可以开始查询数据了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F27.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 22 </STRONG></SPAN></P>
<P>Seq.filter函数、Seq.map函数和Seq.orderBy函数分别相当于Enumerable.Where方法、Enumerable.Select方法和Enumerable.OrderBy方法。如果我们把Seq.filter函数、Seq.map函数和Seq.orderBy函数分别定义为where函数、select函数和orderby函数： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F28.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 23 </STRONG></SPAN></P>
<P>那么代码22就可以写成这样了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F29.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 24 </STRONG></SPAN></P>
<P>我们来看看对应的C# 3.0代码： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F30.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 25 </STRONG></SPAN></P>
<P>是否感到有点喜出望外？什么？感到很失望？因为我写了一个多余的select？囧。。。 </P>
<P>读到这里，你可能会问："如果我只想获取Title和Price属性呢？"这个时候就轮到F#的Tuple类型出场了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F31.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 26 </STRONG></SPAN></P>
<P>读到这里，你可能会问："如果我想从q里提取所有的书名，生成一个新的集合呢？"你可以通过select函数做到，但现在我想换个玩法，试一下F#的序列表达式（Sequence Expression）： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F32.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 27 </STRONG></SPAN></P>
<P>我们知道，q里的每个元素都是包含两个数据的Tuple类型，也就是说，每个元素都能匹配"(a, b)"模式，其中，书名会匹配到a，而价格则匹配到b，但由于我只关心书名，于是在b的位置上使用"_"，表明我不关心b位置的数据。同样地，如果我想从q里提出所有的价格，然后对它们求和，我可以这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F33.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 28 </STRONG></SPAN></P>
<P>如果你读过我的<A href="/allenlooplee/archive/2008/06/01/1211520.html">《我眼中的C# 3.0》</A>，你应该会记得C# 3.0还提供了字典初始化语法，那么，F#是否也支持类似的语法？很遗憾，不支持，至少目前还没看到这种语法，然而，要在F#里把List&lt;'a&gt;变成Map&lt;'key, 'a&gt;却是非常容易的： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/072508_1117_C30F34.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 29 </STRONG></SPAN></P>
<P>上面这句话可以这样理解：把books按照"fun b -&gt; (b.Title, b)"方法进行映射，然后把所得结果用作Map.of_list函数的输入。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>业余研究语言的人 </STRONG></SPAN></P>
<P>我们经常使用编程语言，但我们是否曾经停下来想一下：编程语言究竟是什么？或许对你来说，编程语言只不过是用来编写程序的一种工具，所以根本用不着耗费心思去想这个问题，但对我来说，它并非只是一种工具。我有一个好朋友很喜欢研究股票市场，她说通过股票市场可以看到人性的种种；而我则喜欢研究编程语言，因为通过编程语言可以了解设计者和使用者的思维方式，就像其他研究语言的人通过自然语言可以了解使用者的种群文化一样。 </P>
<P>当一种新的编程语言出现并且得到很多受众的支持时，可能就会有人出来说某种旧的编程语言要被取代，甚至宣称这种旧的编程语言的消亡，与此同时，也会有人站出来，对新的编程语言的必要性提出种种质疑……。没有人希望自己选择的编程语言失去活力，当你选择一种编程语言时，其实就接受这种编程语言背后的世界观，所以你会誓死捍卫你的选择，无怪乎每次编程语言之间的优劣争论都可以上升到宗教信仰的程度。面对这类争论，我觉得optionsScalper的态度比较实际（<A href="http://cs.hubfs.net/forums/permalink/326/331/ShowThread.aspx">原贴</A>）： </P>
<P style="MARGIN-LEFT: 36pt"><STRONG>I tend not to think of "vs." when doing this type of work. F#, C#, C++ and others are nothing more than tools. Used well, each is capable of yielding great results. </STRONG></P>
<P>今天，我因为C# 3.0踏进了F#的大门；明天，我会因为C# 4.0踏进谁的大门呢？无论答案是什么，可能都是很久以后的事了……</P></FONT><img src ="http://www.cnblogs.com/allenlooplee/aggbug/1251631.html?type=1" width = "1" height = "1" /><br><br><a href="http://news.cnblogs.com/n/41940/" target="_blank">[新闻]惠普139亿美元完成对EDS的收购</a>]]></description></item><item><title>我眼中的C# 3.0</title><link>http://www.cnblogs.com/allenlooplee/archive/2008/06/01/1211520.html</link><dc:creator>Allen Lee</dc:creator><author>Allen Lee</author><pubDate>Sun, 01 Jun 2008 02:49:00 GMT</pubDate><guid>http://www.cnblogs.com/allenlooplee/archive/2008/06/01/1211520.html</guid><wfw:comment>http://www.cnblogs.com/allenlooplee/comments/1211520.html</wfw:comment><comments>http://www.cnblogs.com/allenlooplee/archive/2008/06/01/1211520.html#Feedback</comments><slash:comments>101</slash:comments><wfw:commentRss>http://www.cnblogs.com/allenlooplee/comments/commentRss/1211520.html</wfw:commentRss><trackback:ping>http://www.cnblogs.com/allenlooplee/services/trackbacks/1211520.html</trackback:ping><description><![CDATA[<FONT face="Arial, Helvetica, sans-serif">
<P style="TEXT-ALIGN: center"><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>我眼中的C# 3.0 </STRONG></SPAN></P>
<P>&nbsp;</P>
<P style="TEXT-ALIGN: right"><SPAN style="COLOR: #0070c0"><STRONG><EM>Written by Allen Lee </EM></STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>缘起 </STRONG></SPAN></P>
<P>每次有新技术发布时，我们总能感受到两种截然不同的情绪：一种是恐惧和抵抗，伴随着这种情绪的还有诸如"C# 2.0用的挺好的，为什么要在C# 3.0搞到那么复杂？"或者"我还在使用C# 1.0呢？"等言辞；另一种则是兴奋和拥抱，伴随着这种情绪的还有诸如"原来这个问题在C# 3.0里可以这么简单！"等言辞。 </P>
<P>最近我在公司内部做一个LINQ的系列讲座，在我为其中C# 3.0新特性这一讲准备演示文稿时，突然萌生了写下这篇文章的念头。语言的特性乃至其本身并没有对错之分，是否接受在很大程度上是一个感性问题，即你是否喜欢这样的做事方式，我并没有打算说服任何人接受C# 3.0和LINQ，写这篇文章也只是想和大家分享一下我自己的感受。 </P>
<P>有一次我观看一个关于Expression Blend的培训视频，里面说了一句让我印象非常深刻的话： </P>
<P style="MARGIN-LEFT: 36pt"><STRONG>I know how it works because I know why it works. </STRONG></P>
<P>细细品味这句话，你会感受到它所要传达的信息：理解为何需要这个功能可以帮助你更好地理解如何使用这个功能，而这也正是我要在这篇文章里采用的表达方式。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>你是如何创建属性的？ </STRONG></SPAN></P>
<P>如果你长期使用C#，相信你不会对属性这个东西感到陌生。一般地，属性是对私有字段的一个简单包装，就像这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C301.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 1 </STRONG></SPAN></P>
<P>使用属性而不是直接公开私有字段的一个好处就是在属性的获取访问器或设置访问器里加入额外的逻辑并不会为客户端代码带来麻烦，例如你想在设置标题的时候做一些额外的检查。但如果你只是简单地包装一下，像上面的代码那样，就会发现你其实多写了不少可以省略的代码。既然Title属性和m_Title私有字段对应，获取访问器就肯定是返回m_Title的值，而设置访问器也肯定是把值设到m_Title。再者，如果你只通过Title属性来访问这个数据，那么m_Title私有字段就会变得无足轻重，这样的话，为什么不交给编译器代劳呢？这个时候，C# 3.0的自动属性就可以派上用场了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C302.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 2 </STRONG></SPAN></P>
<P>编译器会为你创建一个私有字段，并让获取访问器和设置访问器指向这个私有字段。当然，如果有需要，例如要在获取访问器或设置访问器里加入额外的逻辑时，你随时可以对获取访问器和设置访问器进行展开。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>你是如何初始化对象的？ </STRONG></SPAN></P>
<P>现在，假设我们有这样一个类： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C303.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 3 </STRONG></SPAN></P>
<P>你会怎样初始化它？一种做法是用Book的默认构造函数创建对象实例，然后分别为每个属性赋值： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C304.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 4 </STRONG></SPAN></P>
<P>另一种做法是使用C# 3.0对象初始化器： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C305.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 5 </STRONG></SPAN></P>
<P>乍看一下，C# 3.0的做法似乎没有让人感到任何优越感，现在，请你仔细观察一下，这两份代码分别包含多少个";"？代码4有5个";"，意味着它用了5个语句进行初始化；而代码5只有1个";"，意味着它只用了1个语句进行初始化。从词法的角度来看，如果此刻我只能接受一个表达式，那么代码4的做法就帮不上忙了。一个变通的方法是为Book类提供带参的构造函数，但这种方法也有弊端，用户可能只想在初始化时为部分属性提供数据，而我们又无法确切预知用户会提供哪些属性的组合，于是，我们可能要为用户提供足够多的构造函数重载，嗯，有点无聊，也有点多余。另一个变通的方法是提供接受最多参数的构造函数，如果用户为某个参数传递null，那么就忽略与之对应的属性，这个方法比较接近代码5的做法，不同的是，如果你的属性很多，而用户关心的只是很少一部分，就可能不得不输入很多null了。 </P>
<P>现在，假设你要实例化一组Book对象，并把它们储存在一个集合里，你会怎么做？下面是通常的做法： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C306.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 6 </STRONG></SPAN></P>
<P>如果结合使用C# 3.0的对象初始化器和集合初始化器，你就可以把代码简化为： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C307.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 7 </STRONG></SPAN></P>
<P>集合里的每个元素通过","分割，结合对象初始化器使用，整个集合的结构显得比较明晰。字典的初始化也可以同样简单： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C308.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 8 </STRONG></SPAN></P>
<P>说到这里，我相信你也能感觉到，C#似乎正在表达式化，以前需要很多条语句才能做到的事情，现在却可以用单个表达式描述出来，而这种理念也渗透在整个C# 3.0的氛围里。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>你是如何把运算逻辑外包出去的？ </STRONG></SPAN></P>
<P>假设我现在得到了一组Book的实例对象，你要对它们进行排序，那么你如何告诉它你要按价格来排序呢？ </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C309.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 9 </STRONG></SPAN></P>
<P>在C# 1.0里，我们需要特意为它提供一个独立的方法： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3010.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 10 </STRONG></SPAN></P>
<P>然后向Sort()方法传入所需委托的实例： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3011.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 11 </STRONG></SPAN></P>
<P>这在C# 2.0里可以进一步简化为： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3012.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 12 </STRONG></SPAN></P>
<P>如果使用C# 2.0的匿名方法，我们可以省去很多不必要的代码： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3013.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 13 </STRONG></SPAN></P>
<P>此外，使用匿名方法，Sort()方法和你希望它用来比较两个Book实例对象的逻辑可以放在同一个地方；而使用独立的命名方法，包含这个逻辑的方法可能会由于整理代码而被挪到别的地方。这样，当你看到代码12时，为了了解它内部的实现，就不得不花一些精力去寻找Compare()方法了。当然，你可以争辩说，我们可以制定一个编码规范，使得Compare()方法必须紧贴在Sort()方法的下方。是的，你可以，但如果这个逻辑并不需要重用，那么使用匿名方法还是具有明显的优势的。如果这个逻辑需要重用，那么匿名方法就无能为力了。 </P>
<P>现在，让我们来考察一下代码13，有没有发现匿名方法的表达方式还不够简练？我们知道，books集合里面只有Book的实例对象，所以Sort()方法传给我们两个参数的类型必定是Book，而Sort()方法期待的结果正是x.Price.CompareTo(y.Price)这个表达式的运算结果，至于delegate和return这样的字眼可以说在这里完全是多余的，那么为什么我们不直接这样表达呢： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3014.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 14 </STRONG></SPAN></P>
<P>这就是C# 3.0引入的Lambda表达式语法。我见过一些人，他们通常强调尽可能简单，但若事情突然变得比他们预期的还要简单很多，他们就开始感到不适，甚至拒绝接受这种简单，其实即使事物的发展方向和你的前进方向相一致，但如果发展速度大大超越了你，仍然有可能引发你内心对失控的恐惧。我希望Lambda表达式语法不会让你感到太大的不适，当然我更希望你会喜欢上它。 </P>
<P>Lambda表达式的理解其实可以很简单，就是"=&gt;"左边的参数参与右边的表达式运算，而运算结果将会返回，这有点像化合反应，即两种或两种以上的物质（左边的参数）生成一种新物质（右边的表达式的运算结果），不同的是，Lambda可以不接收任何参数，也可以不返回任何结果。 </P>
<P>"=&gt;"右边除了可以放表达式之外，还可以放语句，像这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3015.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 15 </STRONG></SPAN></P>
<P>我们把它称为Lambda语句（Lambda Statement），或许你已经发现，它和匿名方法相比只是不需要写delegate关键字和参数类型。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>你是如何为对象扩展与之相关的功能的？ </STRONG></SPAN></P>
<P>我一直在想，为什么String类没有提供一个Reverse()方法，把字符串翻转呢？我猜可能是因为这种操作没有什么现实意思，除非你要做一个文字游戏。实现Reverse()方法并不难，下面是其中一种做法： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3016.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 16 </STRONG></SPAN></P>
<P>使用方法也非常简单： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3017.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 17 </STRONG></SPAN></P>
<P>你甚至可以把Reverse()方法放到某个静态类里，例如Utils，这样，代码17就可以变成： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3018.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 18 </STRONG></SPAN></P>
<P>在C# 3.0之前，你最多只能走到这里，而到了C# 3.0，你还可以使用扩展方法对它做进一步调整，使代码18变成： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3019.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 19 </STRONG></SPAN></P>
<P>怎么样，看上去就像Reverse()方法是属于String的，而你所需要做的仅仅是在Reverse()方法的target参数前面加上"this"关键字： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3020.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 20 </STRONG></SPAN></P>
<P>我们知道，计算机的底层世界并不知道什么是面向对象，而我们在对象里定义的实例方法都包含一个隐藏参数，这个参数就是指向当前对象实例的指针，C# 3.0的扩展方法在形式上模仿了这种做法，但由于扩展方法本质上并不属于与之相关的类，所以你无法在扩展方法里访问类内部的私有成员。 </P>
<P>就上面的讨论来说，你可能认为，和代码18相比，代码19并没有太大的优势，那么为什么需要扩展方法呢？假设我们手头上有一堆书，我想找到最便宜的LINQ的书，使用标准查询运算符的话可以这样写： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3021.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 21 </STRONG></SPAN></P>
<P>我们知道，Where()、OrderBy()和First()等都是扩展方法，如果C# 3.0不支持扩展方法，那么代码21就不得不写成这样了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3022.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 22 </STRONG></SPAN></P>
<P>代码21的可读性明显比代码22的高，也显得更自然，而此时我们只是使用了3个标准查询运算符，你可以想象一下，在没有扩展方法的支持下要表达更复杂的查询会是怎样一番情景？ </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>你是如何表达你想要的东西的？ </STRONG></SPAN></P>
<P>现在，假设我想找到最便宜的LINQ的书，使用C# 2.0的语法，我可能需要这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3023.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 23 </STRONG></SPAN></P>
<P>虽然我已经使用了Array.IndexOf()方法、List&lt;T&gt;.Sort()方法和匿名函数来简化代码，但仍然无法掩盖一个事实，那就是我在讲述如何获取我想要的东西，而这也正是命令式编程（Imperative Programming）的核心思想。 </P>
<P>如果使用C# 3.0的语法，情况将会大不一样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3024.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 24 </STRONG></SPAN></P>
<P>在这里，你表达了你想要的东西，而不是获取这些东西的具体步骤，这是声明式编程（Declarative Programming）的核心思想，这样做的好处是明显的，你的需求可以被重新解析并执行，必要时还可以对底层的实现进行优化，但由于你并不关心和牵扯到具体的实现上，所以那些优化并不会导致你修改代码。 </P>
<P>命令式编程就像过程管理，你深入执行的细节，继而对整个过程的执行实施控制；而声明式编程则像目标管理（MBO），你制定目标，并把任务分配下去执行。代码23给人的感觉就是整个执行过程都非常的清楚，你可以对任何一个步骤进行修改或者调优；而代码24给人的感觉就是你除了说出你想要什么，你什么也不能做，这对于那些过程管理拥戴者来说可能是不可接受的，他们感到对事物失去了控制，无法建立安全感，因而产生了焦虑。曾经有人向我抱怨：如果你使用了LINQ，你就只能迫使自己相信它的实现是很好的。想想看，如果你的公司把饭堂业务承包给一个餐饮公司，你的公司可以插手别人如何招聘厨师、如何采购食物、如何烧菜烧饭吗？选择LINQ意味着你愿意把执行细节交给别人去处理，从而脱离这些细节，如果你根本无法放下对这些细节的控制，那么LINQ可能并不适合你。 </P>
<P>很难说这两种编程方式孰优孰劣，因为在某些场合下，善于过程管理的管理者确实更能让事态朝正确的方向发展；而在另一些场合下，目标管理为实现者提供足够的自由度，更能激励他们积极地进行思考。管理界对于过程管理和目标管理孰优孰劣之争论似乎从来没有停过，更何况编程界对于命令式编程和声明式编程孰优孰劣之争论，我个人倒是更倾向于把这看成是找出更适合你自己的风格，而不是盲目听信别人的说法。语言到底是发挥积极作用还是消极作用在很大程度上是取决于使用者的，我们应该使用语言有利的一面来协助我们的工作，而不是使用其有害的一面来伤害自己和别人。 </P>
<P>回到代码24，它把满足条件的书的所有信息都返回给我，如果我只需要书名和作者名字呢？我们知道，在面向对象的世界里，信息储存在对象里，于是我们不得不走到一个尴尬的境地，那就是我们要为此创建一个临时类： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3025.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 25 </STRONG></SPAN></P>
<P>噩梦正式开始了，如果我需要书名和价格呢？如果我需要书名、作者和价格呢？……（读者可以自行补全这个列表）这个时候就轮到C# 3.0的匿名类型和隐式类型化变量出场了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/060108_0249_C3026.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 26 </STRONG></SPAN></P>
<P>因为匿名类型是由编译器自动生成的，而在你写代码的时候它还没有名字，所以你无法用这个类型来声明这个变量，此时"var"关键字就派上用场了。这个是"var"关键字的最初目的，但得益于类型推断系统，我们还可以使用"var"关键字声明任何本地变量，只要我们在声明的同时给予它初始化，否则编译器无法进行推断。曾经有人问我：如果我想返回代码26里的wanted7怎么办？我们知道，方法的返回值需要明确给出类型，而在我们写下代码26时，编译器还没有给查询表达式里的匿名类型取名。如果你真的要把它返回，你只能把方法的返回值类型定为IEnumerable&lt;object&gt;，因为我们只能确定匿名类型是object的后代，但这样一来，客户端代码的日子就不太好过了，因为除了通过反射来访问你的对象，它别无他选。如果你真的要把它返回，那就意味着你和客户端代码有共享这个对象的需求，此时恰当的做法应该是使用命名类型。另外，代码26里构建匿名类型时的"book.Title"是"Title = book.Title"的简写，当你省略"Title ="时，编译器会假定你希望匿名类型的这个属性的名字和Book.Title的一样。 </P>
<P>匿名类型还有一个有趣的地方，它曾经是可变的（mutable），后来却变成不可变的（immutable），<A href="http://blogs.msdn.com/sreekarc/default.aspx">Sree</A>在<A href="http://blogs.msdn.com/sreekarc/archive/2007/04/03/immutable-the-new-anonymous-type.aspx">《Immutable is, the new Anonymous Type》</A>一文中给出了这个转变的解释。我们知道，在面向对象的世界里，对象封装并维护自身的状态，我们通过调用对象的方法所产生的副作用来影响对象的状态，而不可变则是函数式编程（Functional Programming）的核心特征，或许你已经感受到了，C# 3.0引入了大量函数式编程的东西，而函数式编程语言似乎也要风生水起，这究竟意味着什么呢？ </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0"><STRONG>前路在何方？ </STRONG></SPAN></P>
<P>无论你是否承认，C# 3.0在表达上比它之前的版本要来的简单，但要获得这种简单，你必须先用很多东西武装自己的脑袋，这使我想起曾经在一本书里看到的一句话： </P>
<P style="MARGIN-LEFT: 36pt"><SPAN style="FONT-FAMILY: 宋体"><STRONG>简单是由复杂来支撑的。 </STRONG></SPAN></P>
<P>不同语言之间的相互渗透已经不再是什么新奇之事了，引入其它语言的功能有时候甚至可以看作是在战略上入侵对手的市场，这在某种程度上有点像金融业的混业经营。下一个版本的C#将会是怎样的呢？或许这个问题令你兴奋不已，你甚至希望现在就让C# Team看看你的创造力；或许这个问题令你痛心不已，你害怕自己无法适应下一波的变革，因为变革可能导致动荡，动荡可能带来失控，失控可能引发焦虑。不管怎样，该来的是无法回避的，或许现在先让我们看看Matthew Podwysocki的<A href="http://weblogs.asp.net/podwysocki/archive/2008/05/24/what-is-the-future-of-c-anyways.aspx">《What Is the Future of C# Anyways?》</A>是否有一些启示…… </P>
<P>&nbsp;</P>
<P align=right>附：如果你有兴趣看看我的演示文稿，可以点击<A href="/Files/allenlooplee/CSharp3.zip">这里</A>下载。</P></FONT><img src ="http://www.cnblogs.com/allenlooplee/aggbug/1211520.html?type=1" width = "1" height = "1" /><br><br><a href="http://news.cnblogs.com/n/41939/" target="_blank">[新闻]搜狗五笔输入法发布</a>]]></description></item><item><title>再获 Microsoft MVP</title><link>http://www.cnblogs.com/allenlooplee/archive/2008/04/03/1136057.html</link><dc:creator>Allen Lee</dc:creator><author>Allen Lee</author><pubDate>Thu, 03 Apr 2008 05:29:00 GMT</pubDate><guid>http://www.cnblogs.com/allenlooplee/archive/2008/04/03/1136057.html</guid><wfw:comment>http://www.cnblogs.com/allenlooplee/comments/1136057.html</wfw:comment><comments>http://www.cnblogs.com/allenlooplee/archive/2008/04/03/1136057.html#Feedback</comments><slash:comments>64</slash:comments><wfw:commentRss>http://www.cnblogs.com/allenlooplee/comments/commentRss/1136057.html</wfw:commentRss><trackback:ping>http://www.cnblogs.com/allenlooplee/services/trackbacks/1136057.html</trackback:ping><description><![CDATA[今天突然发现园子里多了几篇MVP获奖感言的文章，所以也来凑个热闹。连续三次获得MVP称号是个什么样的感觉呢？很难描述，里面夹杂了激动和兴奋，又带上几分高处不胜寒，除了真心地说声谢谢之外，也不知道说什么好了，还是多些点文章比较实际，嘿嘿~<BR><BR>很多人有这么一种感觉：我的文章发布的很慢。其实，对于我来说，一篇发得出手的文章，从构思到最终发布，包含写Demo、打草稿、排版和审校等等，平均需要1~2周时间。我不知道用这么长的时间写一篇文章对大家来说意味着什么，只是如果东西还不能让我自己感到满意的话，我就不愿意拿出来。而在写文章的时候，我也希望尽可能写得详细一点，希望文章不仅仅停留在看完就算的程度，希望为大家打开一个思考的通道，希望把研究技术时活跃而发散的思维过程呈现给大家，希望把技术留给我的那份激动传递给大家……<BR><BR>加入博客园已经有三个多年头了，一路来也目睹博客园不断成长和壮大，而这里面<a href="http://dudu.cnblogs.com/">dudu</a>的坚持是功不可没的，也是这份坚持让博客园熬过一个又一个的难关。现在博客园正向团队经营模式发展，我也衷心祝愿博客园越来越好。<BR><BR>p.s. 最近感冒流行，我已中招了，大家要小心身体啊~<img src ="http://www.cnblogs.com/allenlooplee/aggbug/1136057.html?type=1" width = "1" height = "1" /><br><br><a href="http://news.cnblogs.com/n/41938/" target="_blank">[新闻]新学年开始 大学生适用的十大互联网应用</a>]]></description></item><item><title>烧水器事件簿 II</title><link>http://www.cnblogs.com/allenlooplee/archive/2008/01/06/1027571.html</link><dc:creator>Allen Lee</dc:creator><author>Allen Lee</author><pubDate>Sun, 06 Jan 2008 00:53:00 GMT</pubDate><guid>http://www.cnblogs.com/allenlooplee/archive/2008/01/06/1027571.html</guid><wfw:comment>http://www.cnblogs.com/allenlooplee/comments/1027571.html</wfw:comment><comments>http://www.cnblogs.com/allenlooplee/archive/2008/01/06/1027571.html#Feedback</comments><slash:comments>13</slash:comments><wfw:commentRss>http://www.cnblogs.com/allenlooplee/comments/commentRss/1027571.html</wfw:commentRss><trackback:ping>http://www.cnblogs.com/allenlooplee/services/trackbacks/1027571.html</trackback:ping><description><![CDATA[<FONT face="Arial, Helvetica, sans-serif">
<P style="TEXT-ALIGN: center"><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>烧水器事件簿 II </STRONG></SPAN></P>
<P>&nbsp;</P>
<P style="TEXT-ALIGN: right"><SPAN style="COLOR: #0070c0"><STRONG><EM>Written by Allen Lee </EM></STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>缘起 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>两</STRONG></SPAN>年前研究.NET事件机制时我写了一篇<A href="/allenlooplee/archive/2005/09/12/234780.html">《烧水器事件簿》</A>，然而，那篇文章存在一个很大的问题，就是Proton的逻辑并不直观。后来，Microsoft发布了Windows Workflow Foundation，我就在想，如果用Windows Workflow Foundation重写Proton，情况会否有所改善？如果有，能有多大的改善？在重写的过程中，原有的代码中哪些可以重用？哪些需要做出修改？两年后的今天，我终于有机会提笔探个究竟了。 </P>
<P>在这篇文章里，我将会探讨下列几个问题： </P>
<OL>
<LI>状态机工作流的开发； 
<LI>工作流实例和外界的通信； 
<LI>工作流实例的承载（hosting）； 
<LI>工作流实例的持久化与状态管理。 </LI></OL>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>烧水器状态机工作流 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>毫</STRONG></SPAN>无疑问，无论是烧水器事件，抑或是应运而生的Proton，都是以烧水器为中心的，于是，烧水器状态便顺理成章地成为Proton的重中之重了。 </P>
<P>烧水器的状态不外乎就两个：空闲状态和工作状态。刚买回来的时候，它的状态是空闲的，自第一个用户开始，它就不断地在两个状态之间切换：工作状态、空闲状态、工作状态、空闲状态……直到它因为老化或者损坏而不能工作为止。 </P>
<P>下面我们来看看如何用Visual Studio 2008为烧水器创建状态机工作流描绘这个逻辑。首先，创建一个State Machine Workflow Library项目： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II1.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 1 - 创建烧水器工作流库 </STRONG></SPAN></P>
<P>接着，在工作流设计器上添加烧水器的两个状态：BoilerIdleState和BoilerWorkingState，为这两个状态各添加一个EventDrivenActivity，并分别命名为StartBoiling和CompleteBoiling，然后设置状态的转换： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II2.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 2 - 设计烧水器工作流 </STRONG></SPAN></P>
<P>其中，BoilerIdleState被设为初始状态（Initial State）。或许你已经发现了，这个状态机没有完成状态（Completed State），正如烧水器自买回来的那一刻起就不会有所谓的"用完"，一般所说的"用完"只是"空闲"的一个别称，只要烧水器还能使用，我们就会一直用下去，同样的道理，烧水器状态机模拟了这个过程，这就是为什么它没有完成状态。 </P>
<P>当烧水器处于空闲状态时，它实际上是在等别人来用，用行话说，就是监听外部的请求事件，于是，我们需要在StartBoiling里添加一个HandleExternalEventActivity，并命名为HandleBoilerRequest： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II3.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 3 - 设计BoilerIdleState里的工作流 </STRONG></SPAN></P>
<P>而当烧水器接到某个用户的请求时，它就会开始工作，根据<A href="/allenlooplee/archive/2005/09/12/234780.html">《烧水器事件簿》</A>里Paul他们的实验结果，烧水器工作的时间最多为30分钟，30分钟之后它将会通告工作完成，并进入空闲状态。要模拟烧水器的30分钟工作过程，DelayActivity是最合适不过了，而30分钟后的工作完成的通告就非CallExternalMetodActivity莫属了。在BoilerWorkingState里添加DelayActivity和CallExternalMethodActivity，并分别命名为Boiling和NotifyBoilerIdle。 </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II4.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 4 - 设计BoilerWorkingState里的工作流 </STRONG></SPAN></P>
<P>至此，我们已经勾画出Proton的核心逻辑了，然而，或者你已经注意到了，HandleBoilerRequest和NotifyBoilerIdle的右上角都有一个红色的叹号，它的出现意味着这些活动缺少一些必要的设置，而这正是下一节要处理的问题。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>烧水器服务 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>回</STRONG></SPAN>顾图2，我们在烧水器工作流里定义了两个状态：空闲状态和工作状态。当烧水器接到用户的使用请求时，它会从空闲状态转到工作状态；而当烧水器从工作状态转到空闲状态时，它会向用户发出通知。在这两个状态转换的过程里，烧水器工作流和用户发生了交互，那么，这些交互是如何做到的呢？ </P>
<P>答案是Windows Workflow Foundation提供的本地通讯服务（Local Communication Services），它使得工作流实例和宿主应用程序之间的交互成为可能。之前我们分别在StartBoiling和CompleteBoiling里放置的HandleBoilerRequest和NotifyBoilerIdle就是为此而做的准备。然而，要使这两个世界连接起来，我们还需要定义一个充当"通讯标准"的接口，这个接口必须打上ExternalDataExchangeAttribute，里面定义的方法可以关联到CallExternalMetodActivity，而事件则可以关联到HandleExternalEventActivity，值得提醒的是，充当事件数据的类必须继承自ExternalDataEventArgs类，并打上SerializableAttribute。下面是IBoilerService接口的定义： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II5.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 1 - 烧水器服务接口 </STRONG></SPAN></P>
<P>不难看出，NotifyBoilerIdle方法将会关联到NotifyBoilerIdle；而BoilerRequest事件则会关联到HandleBoilerRequest。选中StartBoiling里的HandleBoilerRequest，并在属性窗口里设置InterfaceType和EventName这两个属性： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II6.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 5 - 设置HandleBoilerRequest </STRONG></SPAN></P>
<P>接着选中CompleteBoiling里的NotifyBoilerIdle，并在属性窗口里设置InterfaceType和MethodName这两个属性： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II7.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 6 - 设置NotifyBoilerIdle </STRONG></SPAN></P>
<P>还记得HandleBoilerRequest和NotifyBoilerIdle的右上角都有一个红色的叹号吗？当你做好关联后，这个叹号就会消失了。 </P>
<P>假如我们要在多处使用HandleBoilerRequest和NotifyBoilerIdle，但有不希望每次都重复乏味的手动关联，那么可以考虑使用wca.exe生成Communication Activity。wca.exe会自动搜寻指定的程序集里打上ExternalDataExchangeAttribute的接口，它会为接口里的每个事件创建一个HandleExternalEventActivity的派生类，为接口里的每个方法创建一个CallExternalMetodActivity的派生类。 </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II8.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 7 - 使用wca.exe生成Communication Activity </STRONG></SPAN></P>
<P>接着，我们来实现烧水器服务： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II9.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 2 - 烧水器服务实现 </STRONG></SPAN></P>
<P>需要说明的是，BoilerService必须打上SerializableAttribute，里面定义的NotifyBoilerIdle方法和BoilerRequest事件是给烧水器工作流使用的，而BoilerIdle事件和NotifyBoilerRequest方法则是给宿主应用程序使用的。 </P>
<P>至此，我们已经为烧水器工作流，以及它和宿主应用程序之间的交互提供了基础条件，是时候让它们动起来了。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>搭建程序主体 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>首</STRONG></SPAN>先，创建一个Windows 应用程序： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II10.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 8 - 创建宿主应用程序 </STRONG></SPAN></P>
<P>并添加Windows Workflow Foundation和BoilerWorkflowLibrary的引用： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II11.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 9 - 添加引用 </STRONG></SPAN></P>
<P>接着，布置一下Proton的主界面： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II12.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 10 - 简陋的用户界面 </STRONG></SPAN></P>
<P>Proton的用户界面简陋非常，皆因本文的侧重点并非丰富的用户交互，故一切从简，以免喧宾夺主。 </P>
<P>Proton启动的时候需要初始化用户队列、烧水器工作流和用户界面上面的一些元素。我打算重用<A href="/allenlooplee/archive/2005/09/12/234780.html">《烧水器事件簿》</A>里的Enrollee类、EnrolleeQueue类和EnrolleePriority枚举，其中Enrollee类需要稍稍修改，去掉不再需要的代码： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II13.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 3 - Enrollee类 </STRONG></SPAN></P>
<P>下面来看看初始化烧水器工作流的代码： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II14.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 4 - 初始化烧水器工作流 </STRONG></SPAN></P>
<P>宿主应用程序和工作流实例之间的通讯是通过本地通讯服务来实现的，在这里就是我们之前创建的烧水器服务，但我们不能直接把它关联到工作流运行时，而是通过ExternalDataExchangeService这个中介来关联。换句话说，我们把ExternalDataExchangeService关联到工作流引擎，然后把BoilerService关联到ExternalDataExchangeService。一切就绪后就启动工作流运行时，然后创建并启动烧水器工作流。 </P>
<P>当用户点击Boil按钮时，Proton将会禁用这个按钮，防止重复点击多次，从用户队列里抽取第一个用户，设置用户界面上的相关元素，然后就调用烧水器服务发送使用请求： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II15.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 5 - 开始烧水 </STRONG></SPAN></P>
<P>当烧水器使用完毕后（水开了），Proton将会通过消息框发出通知，并更新用户界面上的相关元素： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II16.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 6 - 订阅烧水器空闲通知 </STRONG></SPAN></P>
<P>最后，你可以抓住窗口关闭的机会释放工作流运行时所占用的资源： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II17.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 7 - 资源清理 </STRONG></SPAN></P>
<P>至此，Proton已经可以运行起来了，但每当烧水器进入工作状态时，由于DelayActivity的缘故，烧水器工作流的实例就要盘踞于内存等待30分钟之久，这对于本地Windows应用程序似乎没什么大不了，但如果我想用 .NET Remoting/WCF或者ASP.NET把它开发成网络上的应用并部署在服务器上，内存的浪费就会变得不容忽视了，而工作流持久化服务正是为了解决这类问题而来的。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>配置工作流持久化服务 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>W</STRONG></SPAN>indows Workflow Foundation自带了一个工作流持久化服务的实现，它可以把工作流实例持久化到SQL Server里。在开始之前，请检查你的装备是否齐全： </P>
<UL>
<LI>SQL Server 2005 Express Edition 
<LI>SQL Server Management Studio Express 
<LI>SqlPersistenceService_Logic.sql 
<LI>SqlPersistenceService_Schema.sql </LI></UL>
<P>打开SQL Server Management Studio Express并连接数据库引擎： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II18.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 11 - 连接数据库引擎 </STRONG></SPAN></P>
<P>在SQL Server Management Studio Express里创建一个数据库，并命名为SqlPersistenceStore： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II19.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 12 - 创建新的数据库 </STRONG></SPAN></P>
<P>依次执行装备列表里的SqlPersistenceService_Logic.sql和SqlPersistenceService_Schema.sql，它们位于Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN文件夹里，执行的时候请确认选中SqlPersistenceStore数据库： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II20.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 13 - 确认目标数据库是SqlPersistenceStore </STRONG></SPAN></P>
<P>这两个文件分别在SqlPersistenceStore数据库里创建工作流持久化服务所需的表和存储过程： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II21.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>图 14 - 工作流持久化服务所需的表和存储过程 </STRONG></SPAN></P>
<P>配置好数据库后就可以向工作流运行时添加工作流持久化服务了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II22.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 8 - 向工作流运行时添加工作流持久化服务 </STRONG></SPAN></P>
<P>它应该在工作流运行时的创建和启动之间执行： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/010608_0053_II23.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>代码 9 - AttachPersistenceService的位置 </STRONG></SPAN></P>
<P>这样，当烧水器工作流没有展开实质性的工作时，工作流持久化服务就会让它在数据库里休息一会。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>衍生问题 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>从</STRONG></SPAN>长期看，任何一个烧水器都难逃"退休"的命运，我们没有理由要求所买的烧水器可以代代相传，因此，当烧水器"退休"时，工作流持久化服务在数据库里储存的数据也应该删除。工作流持久化服务会在两种情况下删除这些数据，第一种情况是工作流顺利完成所有工作，但我们之前定义的烧水器工作流是一个没有完成状态的状态机，只要宿主没有退出，或者使用了工作流持久化服务，它就有可能永远运行下去，那么我们是否要为此添加一个BoilerRetiredState，并把它设为烧水器工作流的完成状态呢？如果是的话，我们是否要在用户界面上添加一个Retire按钮来触发向这个状态的转换呢？另一种情况是工作流遇到未处理异常不得不中止，或者用户在工作流运行的中途发出中止的命令，烧水器工作流在BoilerWorkingState里不可能遇到异常，由于DelayActivity的缘故，它正在数据库里面休息，然而，现实的烧水器却有可能在工作中突然报废，此时用户就应该通知Proton中止烧水器工作流，并舍弃储存在数据库里的状态信息，这次，Terminate按钮可能更易接受。 </P>
<P>在正式的系统里，用户可能不会满足于知道队列里有多少个用户在等候，他们可能希望知道排队的人是谁、自己排在哪里、前面有什么人等等，而管理员则希望拥有把队列里的某个（些）人踢出去的权力、在工作流还在运行的时候应用新出台的管理规则等等，而这一切将有可能会导致一个复杂度远大于现在使用的EnrolleeQueue类的队列管理系统出现。 </P>
<P>作为资源管理系统，Proton的责任就是管理烧水器的使用，然而，所有问题最终都会回归到人的问题，有时这些系统能够提高协作的效率，有时则会令事情变得更加复杂，究竟是哪种情况，最终还是取决于使用系统的人。 </P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>参考书目 </STRONG></SPAN></P>
<OL>
<LI><A href="http://www.amazon.com/gp/redirect.html%3FASIN=1904811213%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/1904811213%253FSubscriptionId=1SB7TNCP0504T6TCP5R2">Programming Windows Workflow Foundation: Practical WF Techniques and Examples using XAML and C#</A> by K, Scott Allen </LI>
<LI><A href="http://www.amazon.com/gp/redirect.html%3FASIN=073562335X%26tag=ws%26lcode=xm2%26cID=2025%26ccmID=165953%26location=/o/ASIN/073562335X%253FSubscriptionId=1SB7TNCP0504T6TCP5R2">Microsoft Windows Workflow Foundation Step by Step (Pro Step By Step Developer)</A> by Kenn Scribner</LI></OL></FONT><img src ="http://www.cnblogs.com/allenlooplee/aggbug/1027571.html?type=1" width = "1" height = "1" /><br><br><a href="http://news.cnblogs.com/n/41937/" target="_blank">[新闻]Google拟在9月发布自有浏览器 或采用火狐内核</a>]]></description></item><item><title>探索 Word 2007 开发 II（二）：引用 Amazon 图书信息</title><link>http://www.cnblogs.com/allenlooplee/archive/2007/11/01/945081.html</link><dc:creator>Allen Lee</dc:creator><author>Allen Lee</author><pubDate>Wed, 31 Oct 2007 23:31:00 GMT</pubDate><guid>http://www.cnblogs.com/allenlooplee/archive/2007/11/01/945081.html</guid><wfw:comment>http://www.cnblogs.com/allenlooplee/comments/945081.html</wfw:comment><comments>http://www.cnblogs.com/allenlooplee/archive/2007/11/01/945081.html#Feedback</comments><slash:comments>6</slash:comments><wfw:commentRss>http://www.cnblogs.com/allenlooplee/comments/commentRss/945081.html</wfw:commentRss><trackback:ping>http://www.cnblogs.com/allenlooplee/services/trackbacks/945081.html</trackback:ping><description><![CDATA[<FONT face="Arial, Helvetica, sans-serif">
<P style="TEXT-ALIGN: center"><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>探索 Word 2007 开发 II（二）：引用 Amazon 图书信息 </STRONG></SPAN></P>
<P>&nbsp;</P>
<P style="TEXT-ALIGN: right"><SPAN style="COLOR: #0070c0"><STRONG><EM>Written by Allen Lee </EM></STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>引经据典 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>写</STRONG></SPAN>文章的时候，我喜欢引经据典，只要有需要，我就会引用曾经看过的图书。然而，和上一回提到的问题类似，每当我要引用一本图书时，我得首先打开浏览器上网搜索一下这本书，接着把这本书的一些基本信息，例如书名和作者，复制到文章里，然后插入这本书的超链接。有没有办法让这个过程变得更加简单呢？如果我没记错的话，Amazon.com提供了搜索图书服务，那么为什么不考虑把它集成进来呢？ </P>
<P>引入一个解决方案的同时会把与之相随的问题也引入来，要成功把Amazon.com的搜索图书服务集成到Word 2007里，我们就得回答下面这些问题： </P>
<OL>
<LI>访问Amazon.com的搜索图书服务有哪些条件/要求？ 
<LI>如何向这个服务发送请求？又如何解析服务返回的数据？哪些数据能用到这里来？ 
<LI>有用的数据如何组织和显示？ </LI></OL>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>寻找经典 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>A</STRONG></SPAN>mazon Web Service提供了两种接口，一种是基于SOAP的，另一种是基于REST的，我将选用后者来完成本回的插件。本回面临的第一个难点就是构建满足需求的REST请求，要做到这点，就得先清楚需求是什么： </P>
<OL>
<LI>我希望按书名的部分或者全部进行搜索； 
<LI>我希望搜索的结果里面包括图书的书名、图书的作者、图书的封面和图书的超链接。 </LI></OL>
<DIV>
<TABLE style="BORDER-COLLAPSE: collapse" border=0>
<COLGROUP>
<COL style="WIDTH: 590px"></COLGROUP>
<TBODY vAlign=top>
<TR style="BACKGROUND: #9bbb59">
<TD style="BORDER-RIGHT: #9bbb59 1pt solid; PADDING-RIGHT: 7px; BORDER-TOP: #9bbb59 1pt solid; PADDING-LEFT: 7px; BORDER-LEFT: #9bbb59 1pt solid; BORDER-BOTTOM: #9bbb59 1pt solid"><SPAN style="COLOR: white"><STRONG>第二课堂</STRONG></SPAN></TD></TR>
<TR>
<TD style="BORDER-RIGHT: #9bbb59 1pt solid; PADDING-RIGHT: 7px; BORDER-TOP: medium none; PADDING-LEFT: 7px; BORDER-LEFT: #9bbb59 1pt solid; BORDER-BOTTOM: #9bbb59 1pt solid"><A href="http://developer.amazonwebservices.com/connect/entry.jspa?externalID=849&amp;categoryID=12"><STRONG>Introduction to AWS for C# Developers</STRONG></A><STRONG> by Mark Blomsma</STRONG></TD></TR></TBODY></TABLE></DIV>
<P>Amazon.com提供了<A href="http://developer.amazonwebservices.com/connect/entry.jspa?externalID=886&amp;categoryID=19">Amazon E-Commerce Service Developer Guide</A>，根据这份文档，我构建了如下所示的REST请求： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20071.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 1 - REST Request </STRONG></SPAN></P>
<P>它告诉Amazon Web Service： </P>
<OL>
<LI>我要请求的服务是AWSECommerceService，通过它我可以访问Amazon的产品数据库； 
<LI>我的访问密钥是什么，它可以通过在Amazon.com上注册帐号获得； 
<LI>我要请求的操作是ItemSearch，通过它我可以搜索Amazon的产品数据库； 
<LI>我要搜索书名包含"VSTO"字眼的图书。 </LI></OL>
<P>在浏览器里执行这个请求将的到如下所示的搜索结果： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20072.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 2 - REST Response </STRONG></SPAN></P>
<P>搜索结果所包含的信息量可以通过ResponseGroup参数来控制，ItemSearch操作使用Small作为该参数的默认值。通过把该参数的值该为Medium可以使返回的搜索结果包含图书的封面： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20073.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 3 - REST Response with Image URLs </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>有</STRONG></SPAN>了上面这些准备知识，我就可以着手实现搜索图书这部分功能了。首先，构建REST请求，并通过XElement.Load方法执行之： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20074.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 1 - Execute REST Request </STRONG></SPAN></P>
<P>需要注意的是，由于REST请求是一个URL，而书名可以包含URL不允许的字符，于是在把书名加到REST请求之前要先对其进行编码。 </P>
<P>接着，用LINQ to XML对搜索结果进行处理： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20075.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 2 - Process REST Response </STRONG></SPAN></P>
<P>其中用到的辅助处理方法有： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20076.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 3 - REST Response Process Helpers </STRONG></SPAN></P>
<P>在命令行程序里使用BookSearch.Search方法搜索书名包含"VSTO"字眼的图书： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20077.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 4 - Test BookSearch.Search Method </STRONG></SPAN></P>
<P>却抛出了NullReferenceException： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20078.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 4 - Oops! NullReferenceException is Raised </STRONG></SPAN></P>
<P>原来，搜索结果里面有一本书没有图片。由于并非每本书都有图片的，所以图片数据的辅助处理方法应先检查对应的XML元素是否存在，存在则进一步解析并返回图片的地址，否则返回null： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word20079.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 5 - New Version of GetImageUrl Method </STRONG></SPAN></P>
<P>再次运行Code 4，可以看到正确的结果了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200710.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 5 - Process Results </STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>列举经典 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>不</STRONG></SPAN>知不觉又到设计UI的时候了，由于UI的设计与用户的操作息息相关，于是我们得先看看用户希望怎么操作这个插件： </P>
<OL>
<LI>点击Ribbon上的Amazon Book按钮打开搜索图书的窗口； 
<LI>输入搜索关键字，并点击搜索； 
<LI>在搜索结果中选择想要引用的图书； 
<LI>插入选中图书的超链接。 </LI></OL>
<P>此外，我们还需要进一步细化搜索结果的那些数据将显示在搜索图书窗口里： </P>
<OL>
<LI>图书的封面； 
<LI>图书的完整标题； 
<LI>图书的作者列表。 </LI></OL>
<P>有了上面这些准备，我们就可以设计搜索图书窗口了。首先，在项目里添加如下所示的窗口： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200711.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 6 - Search Book Window </STRONG></SPAN></P>
<P>这个窗口是使用<A href="http://www.componentfactory.com/">Component Factory</A>的Krypton Toolkit控件包开发的，里面包含： </P>
<OL>
<LI>给用户输入书名的编辑框； 
<LI>执行搜索的按钮（Go按钮）； 
<LI>显示搜索结果的ListView控件； 
<LI>显示选定图书的书名的Label控件； 
<LI>显示选定图书的作者的Label控件； 
<LI>插入选定图书的超链接的按钮（Link按钮）； 
<LI>关闭窗口的按钮（Close按钮）。 </LI></OL>
<P>一开始，用户没有在编辑框输入任何东西，而ListView控件也没有显示任何结果，Go按钮和Link按钮应该是屏蔽状态（disable）的。当用户在编辑框了输入了东西，Go按钮会被激活： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200712.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 6 - Enable/Disable Go Button </STRONG></SPAN></P>
<P>同理，Link按钮也会在用户选定某本图书后激活： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200713.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 7 - Enable/Disable Link Button </STRONG></SPAN></P>
<P>当用户点击Go按钮时，将执行下列操作： </P>
<OL>
<LI>通过BookSearch.Search方法获取搜索结果； 
<LI>构建用于ListView显示图书封面的ImageList对象； 
<LI>向ListView填充搜索结果。 </LI></OL>
<P>对于第二步，我们需要ImageManager的帮忙： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200714.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 8 - ImageManager </STRONG></SPAN></P>
<P>有了它，Go按钮就可以完成它的任务了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200715.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 9 - EventHandler for Click Event of Go Button </STRONG></SPAN></P>
<P>ListView是一个不错的东西，但它不能单独为每个项指定图标，这是我最讨厌的。另外，你也可以对图片进行本地缓存，并让ImageManager在获取图片的时候先查看本地缓存，没有的话再去Amazon.com那里要。 </P>
<P>当用户选定某本图书时，我们需要把它的书名和作者显示在ListView下面的两个Label上，于是我把ListView的SelectedIndexChanged事件委托修改了一下： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200716.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 10 - EventHandler for SelectedIndexChanged Event of ListView </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>回</STRONG></SPAN>到Ribbon的设计上，我们需要一个按钮来打开搜索图书窗口，为此，我在Ribbon上添加了一个SplitButton，之所以选择它而不是普通的Button，乃因我脑海里一个突如其来的想法，稍后将会为你剖析这个想法的来龙去脉。添加了SplitButton后的Ribbon如下图所示： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200717.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 7 - Amazon Book SplitButton on Ribbon </STRONG></SPAN></P>
<P>然后实现SplitButton的Click事件委托： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200718.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 11 - EventHandler for Click Event of Amazon Book SplitButton </STRONG></SPAN></P>
<P>好了，运行插件看看效果： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200719.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 8 - Oops! The Value of ImageUrl is null </STRONG></SPAN></P>
<P>噢，抛了一个ArgumentNullException异常！还记得我是怎么处理ImageUrl吗？在Code 5里，当Amazon Web Service返回的搜索结果里面某本图书没有图片时，BookSearch.GetImageUrl方法返回null，类似地，ImageManager.GetImage方法也应该在遇到这种情况时返回一个"占位图片"： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200720.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 12 - New Version of GetImage Method </STRONG></SPAN></P>
<P>再次运行插件，可以看到预期的效果了： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200721.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 9 - Search Results </STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>连接经典 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>终</STRONG></SPAN>于轮到本回的主角出场了，当用户点击搜索图书窗口的Link按钮时将在当前文档的光标处插入选定图书的超链接： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200722.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 13 - EventHandler for Click Event of Link Button </STRONG></SPAN></P>
<P>虽然我现在可以通过搜索图书窗口插入图书的超链接，然而在大多数时候我更愿意遵循以下步骤插入图书的超链接： </P>
<OL>
<LI>在文档的某个位置输入要插入超链接的图书的书名； 
<LI>选中这个书名，使之处于高亮（highlight）状态； 
<LI>点击Ribbon上的Amazon Book按钮打开搜索图书的窗口，此时，这个书名显示在编辑框里，而搜索结果则显示在ListView里，并且第一本书处于选中状态； 
<LI>如果选定的图书不是我要找的，则在搜索结果里重新选择； 
<LI>点击Link按钮插入选定的图书的超链接。 </LI></OL>
<P>为此，我需要为搜索窗口添加Load事件委托： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200723.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 14 - EventHandler for Load Event of BookSearchView </STRONG></SPAN></P>
<P>由于搜索窗口的Load事件委托和Go按钮的Click事件委托都需要执行搜索操作和向ListView填充搜索结果，所以我把这部分代码提取出来以便重用，这样Go按钮的Click事件委托就可以简化为： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200724.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 15 - New Version of Click EventHandler for Go Button </STRONG></SPAN></P>
<P>此时，当我点击Link按钮，原先选中的那个书名将被替换成从Amazon.com获取的完成的书名以及作者的名字，但我希望在原先选中的那个书名的基础上插入超链接并追加作者的名字，于是，我把Link按钮的Click事件委托里为textToDisplay变量赋值的代码改成下面这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200725.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 16 - Assignment to textToDisplay Variable </STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="COLOR: #0070c0; TEXT-DECORATION: underline"><STRONG>幸运经典 </STRONG></SPAN></P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>还</STRONG></SPAN>记得在设计Ribbon的时候把Amazon Book按钮设为SplitButton而非普通的Button吗？之所以这样做，是因为我希望为这个插件添加一个比较特别的功能，类似于Google的"手气不错"（I'm Feeling Lucky）。我为Amazon Book按钮的下拉菜单添加了两个（普通）按钮，一个是Amazon Book按钮，另一个是Lucky Book按钮，运行起来像这样： </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200726.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Figure 10 - Lucky Book </STRONG></SPAN></P>
<P>其中，下拉菜单里的Amazon Book按钮的效果和Amazon Book主按钮一样，而Lucky Book按钮的效果则和Google的"手气不错"相似。当我在文档的某个位置输入要插入超链接的图书的书名，选中这个书名并点击Lucky Book按钮时，它将会执行基于这个书名的搜索，并插入搜索结果中第一本书的超链接。如果用户在没有选中任何文字的情况下点击Lucky Book按钮或者搜不到指定的图书，则告知用户相关信息。 </P>
<P><IMG alt="" src="http://www.cnblogs.com/images/cnblogs_com/allenlooplee/103107_2330_Word200727.png"> </P>
<P><SPAN style="FONT-SIZE: 9pt; COLOR: #4f81bd"><STRONG>Code 17 - Insert Lucky Book </STRONG></SPAN></P>
<P>&nbsp;</P>
<P><SPAN style="FONT-SIZE: 20pt; COLOR: #0070c0"><STRONG>至</STRONG></SPAN>此，Amazon Book插件的开发要告一段落了，虽然我还想实现更加完善的图片管理功能，虽然我还想提供更加丰富的搜索方式，虽然我还想通过配置让插件变得更加灵活，虽然……</P></FONT><img src ="http://www.cnblogs.com/allenlooplee/aggbug/945081.html?type=1" width = "1" height = "1" /><br><br><a href="http://news.cnblogs.com/n/41936/" target="_blank">[新闻]微软正在构思将Wi-Fi带入汽车</a>]]></description></item></channel></rss>