SPO Azure开发之-Storage

说起Azure的storage, 还得从最早那几个项目开始,原来有个academy的迁移和online直播的东西,大概有64个TB的视频数据需要挪到云端,原来都是丢在local的集群上的,于是得写个工具把这个东西丢过去。然后发现是个网络带宽和磁盘读写的体力活。于是一阵捣腾,把Azure storage里面能用的feature都给用了一遍。
Azure的存储提供三类结构:
Table: 支持异构化的数据表操作,主要用来存储表类型数据,个人觉得最大的优势还是在于支持异构数据项目,但是在习惯redis之类的key/value数据库以后,你会发现其优势也不是十分明显,当然,azure的table是以集群的方式出现的,可靠性可以管到99.98%,三个九,还是比较牛X的。
Queue:这一块也是比较成熟的分布式队列系统了,常用的队列操作,以及队列内key时间的控制都已经比较成熟,在大量并发获取key时,我们得保证在同一时间只有一个worker能拿到当前的key,并且如果这个item被搞定,那就需要清掉这条数据,如果没有,那么这个key需要自动在一段时间以后再出来,让worker继续来进行操作。
Blob:这是微软自己的一套分布式的存储结构,后台的代码没有看到过,但是大家用Azure VM存储,以及各种镜像备份都依赖于他,同样,在media Service当中,也是借助于blob的存储来完成媒体编码的转化,这里需要关注的是,blob->container>source。在第三个层级,是没有目录的概念的,这里可以通过资源的path来模拟层级结构,但是在内部实际上是没有文件夹之类的概念的。当然,对于blob里面的每一个container,你都可以把他看作是一个文件夹。
storage-concepts
来个全家福。
下面借助于本身这个用来迁移的工具,来说明Azure里面storage的一些简单应用场景。
1. 由于TB级别数据量对单条网络带宽的占用较大,实际上网络IO已经成为瓶颈,于是得分多路来用。
2. 对于转码的要求,在media Service当中有最大job的限制,这里就需要引入queue的帮助
3. 整体是分布式架构,程序得丢到多个机器上面一起跑。
大致思路是这样的,每个程序从自己的那份table当中,获取需要迁移到blob里面的内容列表,进行校验,列表此时就可以借助Azure的table来完成,当然,部分日志记录也是利用Azure的table来完成的。然后集中丢入计划压入到queue中,然后用work机器来完成上传,其实我们这块最早的源列表是在SharePoint上面的,轻松用CSOM搞定。每个work机器开三个线程,来负责数据传输的工作,其实由于网络带宽的缘故,本来想多开几个线程的,但是于效率无意。这里其实第一步到blob以后,还需要监控其调用media service转码的过程。于是超时,重试,以及分布式队列的策略需要自己用code进行控制了。

SPO Azure开发之-Auth

在最早使用SharePoint07的时候,认证还大多基于membership以及windows.而且那时候的SharePoint自带sso模块,也只是模拟用单一用户登录而已。在进入了SharePoint2010以后,由于identity foundation的逐渐成熟,claims based authentication的基于声明的认证方式,也进入到SP的标准模块当中。所以说这个东西并不是啥子新的东西,很早就有了。对于现在O365来说,一个tenant里面的基本上所有的认证都是通过这种方式来实现的。
话说这种方式为啥好呢,其实要从诸多的异构系统说起,如果有过做多系统单点登录的经验,你就不难了解这一套认证的设计思路了。最早我们做单点登录,无非以下几种方式。
1。模拟登陆
2。统一到一套认证中
3。对码跳转
由于在不同的系统当中,都有其本系统独立的人员权限和组。一般单点很难把所有不同的系统统一起来,如果采用唯一账号模拟登陆,那跳转后的系统权限也只能是这一个账号的所有拥有的内容。当然你也可以改造各个系统,使得他们的认证都从用同一套用户和认证,这样不可避免的要对各种系统进行改造了。如果系统各式各样,改造起来估计能头大致死。如果想改造得少一点,其间还得做不同用户在不同系统当中的映射关联,比如王小五在A系统里叫wangxiaowu,但是他在B系统里面就叫wanxiaowu了。如果你说他们没区别,ok. 你眼睛有点花了。
所以很多同志们就拿出了一种折中的解决办法,以一套相对统一的标准来解决这个问题。办法如下图:
2376.image3_5F00_thumb_5F00_56056FFD
其原理就是轻度改造现有系统,让用户都到STS当中去进行验证,而STS相当于认证中心,对所有挂接在其上的系统都做WS-Trust. 而对于各个用户信息,我们用claim来声明其信息,协议一般用SAML. 用户流图如下:
Untitled picture
下面可能主要通过解析SharePoint Online的认证过程,来解析这整个过程。
1。请求SharePoint online站点,站点检查用户浏览器是否已经登录,一般是看cookies里面是否有从sts拿到的token.当然,这个token是有过期时间的
2。之后跳转到统一认证中心的认证页面里面去,让用户进行登录,用户登录完完毕以后,会返回给用户一个security token.
3。之后用户浏览器拿着这个token跑到spo的页面里面去,然后获取到对应的cookies,里面包含两个,一个是fedauth一个trfa.
image_thumb
至于代码我这里就不重复黏贴了,有兴趣的可以在这里下载下来看。

SPO Azure开发之-APP

年初听到互联星空终于在天朝取建立了azure的数据中心,其实内部测试他们好久之前就开始了。由于工作的关系,两年前就开始使用没有release的SPO和Azure开始做一些项目了(当然是非天朝版本)。总体上看微软的Azure平台还是相当牛X的,至少目前基于微软系的内容,没有比这个更牛的了,从某些网站的测试数据来看,微软的azure速度也能进入前三甲,(至少比国内那些渣渣云平台好多了,话说今天还收到国内某渣渣平台的收费短信,话说我都删除了instance了,你们是从哪里计费的。)
来个微软的全球节点图:

由于一直是在做SharePoint相关的内容,从03到现在的13,可以说SP平台走过了十多个年头,最新的版本中SharePoint提供两个,一个是on-prime版本,一个是online版本。
On-prime版本我们内部用了N个release,发现有点渣渣,限制和online几乎一样,想不出有什么公司会继续去升级这个版本,唯一的优势在于升级了很多新东西,依然能够hack自己的asp.net程序进去使用。
Online版本要舒服得多,不过是放在了o365 online 的家族中,和lync exchange office的online tenant共享license.
他的限制在于我们不能再任意的使用原来基于farm和server side object model的解决方案,只能用CSOM对site collection和ca来进行操作。内部程序版本为15.0.xxxx。这意味着,在online的app当中,只有几种方式可以host你的app。
其实最早的时候还有self-host以及auto host.今天看微软最新更新的sdk来看,这两个坑坑已经被取代了。哈哈,其实他们自己也发现,那么分不太合理。(我会说我们有些开发都分不清么)。
这里抽了几张图,应该能看出个大概。
SharePoint-hosted apps

Provider-hosted apps

Mixture-hosted apps
以下是我依据的一些个人经验,从开发的角度来区分现在的几种app的模式。这不能不从sp的发展历程开始说起
要知道在原有的版本当中,特别是2010和2007这两个SharePoint版本当中,我们的重量级的解决方案都是以wsp的包的形式,部署在farm当中的,然后再apply到对应的某一个我想使用的site collection当中.我的包当中其实本体就是一个solution。而这个solution可以包含feature,而feature可以包含我的content type,module等等。这个结构是SharePoint当中管理组件和内容的基本结构。2013同样也使用的是这样一套结构。我们开发的内容都按照这套结构来进行构建,部署和使用。这样我们才能利用SP的很多优势内容如List,library,bdc等等。
但是我们在做大型的项目时候发现,我们原来使用的这些内容,都需要部署在场的级别,而且我们的内容都是从server side object model去进行操作的, 这样当一个场,支持几十个solution,里面有几百个feature的时候,就变成了噩梦,因为所有的东西都是在服务器上跑的,什么你都能操作,什么你都能控制,对于开发性来说本来是好事,但是内容多了以后,你就会发现有很多问题,因为并不是所有的solution你都清楚,并且都很ok. 而且由于是分布式部署,我们在每次安装wsp的时候,会重启整个目标site collection前端iis. 要知道,如果我有20台前端,那会是什么一种情况。话说原来在某大型央企的一个项目,每次上包都是胆战心惊的凌晨左右时间,人工值守。如果是运维的人员,你们懂的。
所以对于场级别的解决方案以及server side object model来说,尽量使能没有就没有,于是现在的这个版本就出现了,并且都是以app的方式呈现。当然,其本质还是一个solution.
SharePoint-host: 这里就是把你的app部署到一个app的专用服务器上面,在2013版本当中有一个专门的app中心,可以来承载你的应用。
Provider-host: 这里就是把你的app部署到一个其他你想用的服务器上面,一般我们都丢到Azure里面做成一个web Role, 这个东西我会在后面的文章给大家介绍。
混合模式这里我就没有用过,所以不做评价,大家自行看微软的SDK.
说到这里大家可能会比较好奇,我随意把我的app丢到一个服务器上面,那么他们是怎么和我的farm来进行验证和交互的呢,那么我会在后面给介绍,SPO的认证模块。
由于现有的SPO以及onprime在标准的app当中无法使用SSOM来与SPO交互,所以我们这里开发app当中能走的路就只有以下几条了:
1.SharePoint Rest API
SharePoint REST service architecture

这个其实原来2010版本就有,不过现在提供的接口更多了一些,具体的一般都放在/_api里面,大家可以从sdk里面去具体学习。http://msdn.microsoft.com/en-us/library/office/fp142380(v=office.15).aspx
这里需要注意的是,对于restful的每一次请求,需要在http头当中加入 token.
HttpWebRequest endpointRequest =
(HttpWebRequest)HttpWebRequest.Create(
“http:///_api/web/lists”);
endpointRequest.Method = “GET”;
endpointRequest.Accept = “application/json;odata=verbose”;
endpointRequest.Headers.Add(“Authorization”,
“Bearer ” + accessToken);
HttpWebResponse endpointResponse =
(HttpWebResponse)endpointRequest.GetResponse();
2.SharePoint Web Service
这个在2010里面也有,也有所丰富。
3.SharePoint client side Object Model
同样2010也有,也丰富不少。
这三个方式都是和语言无关的,你可以用JS,也可以用c#来写。都ok.

SAP BOE x3.1和他的小老婆水晶易表(Xcelsius)的故事

最近的时间,基本上大部分都花在了和BOE打交道上了。话说有一日发现Xcelsius的美观和实用,准备开始用水晶易表做一些专题业务的应用,后来发现水晶易表在Server端没有像大老婆crystalReportView这种专用控件设备,虽然可以将小老婆娶回家,和大老婆放一个院里(就是说吧xcelsius嵌入到水晶报表当中)。但是速度十分缓慢,操控性不是很好,而且看起来比较丑,丧失了小老婆本有的眉毛多姿,风情万种。于是只能自己动脑筋开发了(我现在知道为啥都喜欢在外面买小别墅给二奶了,偷腥的感觉,独立的感觉还真是不错。要想皇帝那么多老婆,都一个院的,美女最后都被整成白痴和残废了,就剩下一些心狠手辣的老女人了,皇帝每天就听老女人撒娇,多么悲剧啊,所以皇帝都到民间到处播种)。言归正传,话说BOE服务器上的东西,就是将一大堆的数据,文件,连接封装到了一个InfoObject里面,不仅仅是报表,连里面的用户,用户组,以及结构,都是InfoObject,所以,只要能从InfoObject里面,找出对应的文件格式和文件流,然后解析出来就可以了。当然,如果数据是及时查询的,涉及到回调,那就要解决权限的问题。这内部就相对比较复杂了,不过万变不离其宗–数据结构+算法=程序。
   对于水晶易表和后台数据的封装,有以下几种方式,当然,版本是在x3.1中
  1, xcelsius->webi->BOE
  2, xcelsius->webi->live office->BOE
  3, xcelsius->webService->BOE
   其中的变化就可以让大家自己去进行自由组合,可以衍生出更多的方法。下面开始介入正题,在Google搜索了一圈以后,发现从BOE上取SWF的文件,没有啥子价值的资料,让后就开始自己开动脑筋来解析了,因为发布到BOE服务器上面的swf文件,是能直接在BOE服务器上面查看的,而且在BOE的服务器上,也发现了解析swf的dll的存在,于是就依据连接,找到了iis当中,解析易表的文件夹,进去一看,靠,就三个文件GetSwf View FlashControl,打开一看,还都只有几行内容,都是用来查看flash文件的,巨简单,而且View里面就加了一个控件,控件的就是FlashControl.ascx, 不过看到了一些绑定的内容。
  在FlashControl.ascx中,

  //object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"
   codebase="http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0"
   width="100%" height="100%" id="myMovieName" VIEWASTEXT>
   param name="movie" value="<%= MovieURL %>"/>
   param name="quality" value="high"/>
   param name="bgcolor" value="#FFFFFF"/>
   param name="FlashVars" value="<%= FlashVars%>"/>
   embed src="<%= MovieURL %>" flashvars="<%= FlashVars%>" quality="high" bgcolor="#FFFFFF" width="100%" height="100%"
   name="myMovieName" align="" type="application/x-shockwave-flash" pluginspace="http://www.macromedia.com/go/getflashplayer"/>
  ///object>

   由此可以看出,里面肯定有后台代码来获取其中的数据,于是开动发编译大法,从后台dll中找寻价值代码。经过一番摸索,就在一个不经意间找到了他们的实现类。
  于是乎,从BOE请求的过程就跃然纸上了。其实和我们普通请求报表的过程没有什么区别,无非就是创建session,创建service,query对应的InfoObject,让后调用了一个特殊的在InfoObject下面的类,StreamContent这个类来获得取过来的文件流,让后打到输出流当中,就会将文件返回给客户端了。整个过程不超过一百行代码,当然,大部分方法都经过了封装。下面将关键代码粘贴如下,不知道如果粘贴整个类会不会遭到审查,所以就贴点关键代码吧。

  private void LoadData()
   {
   string s = base.Request[ActionHelper.OBJID_PARAM];
   if (s != null)
   {
   EnterpriseSession session = (EnterpriseSession) this.Session["boeSession"];
   InfoStore service = (InfoStore) session.GetService("InfoStore");
   int id = int.Parse(s);
   InfoObject obj2 = TraversalLogic.GetInstance().GetInfoObject(service, id, "SI_PROGID, SI_FILES");
   string progID = obj2.ProgID;
   string str3 = ".swf";
   string str4 = "view";
   string str5 = "application/x-shockwave-flash";
   base.Response.Clear();
   string str6 = "inline";
   base.Response.AppendHeader("content-disposition", str6 + "; filename="" + str4 + str3 + """);
   base.Response.ContentType = str5;
   InfoObject.StreamContent.WriteContent(obj2, base.Response.OutputStream);
   base.Response.Flush();
   base.Response.End();
   }
   }

  
   经过上述一个简单的过程,我们的swf流就可以得到了,但是这里出现了一个问题,试想,你的文件都是发布到BOE服务器上的,而你数据连接,都是封装到水晶易表里面的,如果没有经过验证的用户,单独的打开swf文件,肯定是无法取到数据的,而在BOE服务器上面查看,BOE就能够不用输入任何东西,就能够查看数据,所以我们还得模拟的写一个页面,来仿照BOE查看数据的内容,其实思路也非常清晰,当我们从BOE上面请求到InfoObject时,里面的内容就包含了你的验证的信息,我们只需要通过他的属性,就可以把这些信息拼接出来,让后再构造模拟串,就可以完成。
  核心代码如下:

   EnterpriseSession enterpriseSession = ActionHelper.GetEnterpriseSession(base.Request);
   this.Session["boeSession"] = enterpriseSession;
   string str = base.Request[ActionHelper.OBJID_PARAM];
   if (str == null)
   {
   str = base.Request["iDocID"];
   }
   string str2 = "GetSwf.aspx?" + ActionHelper.OBJID_PARAM.ToString() + "=" + Encoder.Encode(str);
   string s = enterpriseSession.LogonTokenMgr.CreateWCATokenEx("", 0x3e8, -1);
   string serializedSession = enterpriseSession.SerializedSession;
   string str5 = "";
   int id = int.Parse(str);
   InfoObject obj2 = TraversalLogic.GetInstance().GetInfoObject(base.GetInfoStore(base.Request, base.Response), id, "SI_FLASHVARS_STRING");
   if ((obj2 != null) && (obj2.Properties.Count > 0))
   {
   str5 = obj2.Properties["SI_FLASHVARS_STRING"].Value.ToString() + "&CELogonToken=" + Encoder.EncodeURL(s);
   }
   else
   {
   str5 = "CELogonToken=" + Encoder.EncodeURL(s);
   }
   str5 = str5 + "&CESerializedSession=" + Encoder.EncodeURL(serializedSession);
   this.FlashControl1.MovieURL = str2;
   this.FlashControl1.FlashVars = str5;
   }

   这样,就可以从BOE服务器上取回我们的swf文件了,但是发现我在回传的时候,会提示跨域的错误,因为swf里面封装的数据连接,在你的应用程序域当中执行的时候,会被boe服务器认为是不可信的,没关系,加个可信的跨域文件,就可以轻松搞定具体的可以搜索到这个内容的描述。
  Cross-domain Policy File Specification
  至此,和BOE的小老婆的故事就结束了,其实小老婆姿色的确不错,动态实时,效果酷炫,就是速度比较慢。比大老婆好多了(所以大款都喜欢去包那么一两个二奶)。

SharePoint2010SDK研究笔记

2010已然上市有一段时间了,但是本着微软产品的测试小白鼠精神,在去年就装了个RTM版本的,后来又开始陆续更新,自己起了个开发环境,写了两个demo,然后开始回头来,继续看SDK,因为所有有价值的资源,都基本来自SDK,原有组里的一个人就被拉去做SDK了,感觉2010的server08 sql08的绑定,直接制约了其本身的推广和使用,至少在用windows体系的产品,大多已然颇有微词,各种补丁,系统的稳定性,在客户没有成熟案例的时候,果然是不敢采用的。而且原有解决方案的升级,也是个比较严重的问题,曾经自己尝试过将自己07工程转化为10的SharePoint工程,各种失败和错误,而我自己用Stsdev的方案程序集却没有任何问题,所以过渡这个过程,必然是痛苦的,当然微软的哥们也在不断的完善和弥补。
   2010所带来的一些新的特性,的确是简化了开发,把开发变得更加傻瓜化,但是傻瓜化的过程当中,必然就有更多的制约,所以在扩展性和易用性之间要找到一个比较好的平衡点,还真是一个比较麻烦的过程。
   发了那么多的牢骚,在历经多次重做虚拟机以后,终于把sharepoint2010以及一堆乱七八糟的补丁给搞上了,下面开始从SDK的介绍顺序,来介绍自己所看到的一些新的特性。
  1,增强了的移动模块功能:
  手机和移动设备逐渐占据web这个不争的事实,已经然广大的厂商开放更多的兼容软件到移动争霸的版图内,其实在07中,对移动设备就有一定的支持,至不过功能十分有限,而且扩展十分麻烦,但是现在10增加的mobile特性,增色不少。
  (1),移动短信
  这个功能咋一看就是发个短信和通知,因为原有的ocs产品和outlook产品,已然能很好的支持消息和邮件的提醒了,这次专门提供了一个重写的SPAlert类,和一堆接口来兼容不同协议的sms和为手机发送短信的功能。当然,微软已然不忘深度集成了自己Outlook Message Service (OMS) protocol。其实原有的通知完全是在事件和代码里面来触发自己的短信发送的内容,此举虽然比较麻烦,但是定制性比较好,对接口依赖性都比较小,也能比较好的适应用户的需求,加了这个接口,除了代码好看一些以外,个人感觉没有什么实质性的用处。因为并发和队列的问题,瓶颈仍然在自己的短信接口处,而不在sharepoint.
  (2),mobile适配webpart
  这是一个非常牛逼的功能,遥想原来我们要做一个移动的页面,得自己收工修改系统默认的移动页面,而且还得为这个页面开发专门的webpart,相当于重写了自己的webpart,核心功能没有多少改变,还是在界面上折腾,用于适配不同的手机浏览器。当然,如果自己本身的程序对html4.0的支持不好的话,注定会非常的悲剧。而mobile的webpart适配特性,比较好的解决了这个问题,我们至于要在原有的webpart当中,增加对手机的重载就可以了,这在很大程序上解决了原来重复写两个webpart的问题。不过现在移动web在企业内的应用才开始逐步兴起,主流的门户,社交,b2b,的移动门户也都开始逐渐成熟。要大规模应用还需时日,当然,企业内部人手一个ipad+iphone,那么我们的应用都要做兼容,做第一批吃螃蟹人的还是比较爽的(虽然国内现在大多企业还没有脱离无纸办公,就让他们走入移动办公的时代,他们估计会崩溃的,其实还是体制,体制,还是体制问题)。
  (3)mobile文档查看器
  Word,pdf,xps,excel在网页上面直接的浏览,在web2.0后期就基本已经实现了,但是在手机上,大家要想直接打开word和pdf之类的文档,肯定还要借助于三方的工具,台式机采用的方式是装一个插件,转由客户端来处理已经下载的内容,但是移动设备的性能有限,不能一个浏览器占用很多的资源,就要求转化在服务器进行处理,所有的东西以html方式推送以获得广泛的适用性和兼容性(做移动浏览器可以不兼容某些控件,但是不可能不兼容html)。实际上也就是在服务端,把各种格式的文件转化推送为html的,office系列的文件格式目前依然不是问题,关键在于其他的格式,大家常见的百度文库,新浪的文库的做法,十分值得借鉴。这里moss就开放了一个注册,大家可以将自己的对某种类型的文件查看,跳转到自己的针对这种文件的查看的页面当中去,做法十分类似于httpHandler的截获处理的方式。不过这里更加简单,就是跳转到页面,而不是注册你的处理的dll,也让大家有更多的灵活性。
  下面这个例子就是注册xps的查看器
   FeatureId=”XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX” />
  这里是在注册对windows mobile系统的手机请求处理
  这里是在注册对docx文件的处理。
  2,重写了的BDC,现在叫BCS
  原来的BDC虽然有很好的想法,但是实际上使用起来,对BI的支持是十分有限的,10版本重写这部分的内容,希望能解决中看不中用的局面,原来的bdc是只能读不能回写,现在变了,只要外部数据可以写,那么你就可以借助于bcs回写(终于可以写了,原来就一个IdataReader,现在估计加了IdataWriter,不过权限的控制不知怎么做了,估计会比较麻烦了),原来用过BDC这块的内容,感觉用来演示和简单应用还行,但是到生产里面的话,实现需求的代价太大了,还不如自己写。对外部数据来源也扩展了好几种,但是用过后才知道,而且提供直接大lob二进制流,这个对性能的提升不是一点半点了。
  
  Business Data Connectivity (BDC) service
  BDC Connectors and the pluggable Connector Framework
  External lists
  External data columns
  
  (1)扩展的外部列表
  利用BCS的基础,原有的list得到了极大的加强,或许大家对5000以上大容量的列表的性能衰减,已然是非常无语,同样,在做企业的整合方案的时候,大家也都会在list库和数据库表之间进行权衡,更有甚者,我们还做过直接从content数据库当中读取列表库的内容,到自己的数据库表中,利用表中的itemid来关联大数据,从而借助于List来完成自己所需要的流程的挂载和数据的审批。所以借助于BCS的扩展,列表得到了扩展。
  
  BDC目前支持四种外部数据源扩展的方式,
  一种是对数据库,
  一种是对webservice
  一种是.net的model,
  一种是自定义(鬼才知道自定义的是什么)
  实际上BDC封装的就是我们在整个Mvc框架里面的所说的model,只不过这里他提供了简单的方式来连接外部的数据,不同的数据源他提供了不同的connect来对接数据,从而把各种数据都统一为了BDC实体。
  扩展的列表库就借助于BCS的这个特性,默认的系统当中有一个内容类型,而这个内容类型是专门用来承载已经定义好的数据实体的,那么我们就可以通过这个内容类型,扩展我们的数据到列表库当中了。同样借助于这种方式,扩展的外部数据列,也就不是问题了。
  3,全客户端对象
  原来我们开发的时候发现,我们所开发出来的内容,不管是啥子的,基本上都需要moss的环境来支持才可以运行,当大家在sharepoint的臃肿开发机上开发的时候,不得不上一个开发环境,我们所用.net的托管代码,都是在服务器上执行的,当然,还有一块是ECMAScript的脚本,这些服务端脚本都是在浏览器里面执行的,这个大家应该用得不多。
  说到这里,不得不想起在SHAREPOINT中对于大规模的数据和内容,而且跨越众多的分公司和机构的分布式问题。原来我也设计过超大规模的分省的方案,大家会发现,很多时候各省的接入方式不一样,而且vpn后的速度也不是十分的明显,最后折中的办法的按照省市在场中加多台的服务器组,各省的服务器分置在各省的机房,和总部通过vpn联系,并受控与主核心场,各省的application 和db都归属在各省的服务器上。目前这是我能想到的借助于SharePoint比较好的分布方式了,虽然性能有所提高,但是由于网络的限制,有些地区的vpn线路只有2mb,集成部署方案和更新的时候还是很慢,而且各省的爬网索引晚上的timerJob也总有挂掉的情况,于是不得不借助于自己所建立的中间层,来控制各个节点的内容。曾经也考虑过集中部署的方式,但是发现,以大数据量为主的交互内容,在地方网络资源有限的情况下,还是让信息的交互变得痛苦不堪。虽然优化了web part的代码,但是性能还是没有得到根本的解决。如果以Sharepoint的分布式解决方案来说,可以独立成文了,这里就不再继续偏题了。
  新的客户端对象模型,比原有moss提供web service的功能更加的强大,并且比ECMAScript的使用会更加的简单,大家可以利用客户端的对象模型,在远程的计算机上和服务端进行交互。托管的客户端代码都继承于ClientObject这个基类,命名空间Microsoft.SharePoint.Client.光说不练,还是来段代码吧。
  

  using System;
  using Microsoft.SharePoint.Client;
  
  namespace Microsoft.SDK.SharePointServices.Samples
  {
   class UsingClientContext
   {
   static void Main()
   {
   ClientContext clientContext = new ClientContext("http://MyServer/sites/MySiteCollection");
  
   Web oWebsite = clientContext.Web;
  
   clientContext.Load(oWebsite);
  
   clientContext.ExecuteQuery();
  
   Console.WriteLine(oWebsite.Title);
   }
   }
  }

  大家可以看到,通过context的交互,所有的操作都是在一次执行之后请求到客户端,然后在客户端执行,然后执行完更改以后再进行提交,这里肯定也有提供异步方式的执行方法ExecuteQueryAsync,要不然老傻傻的等在哪里,客户肯定是受不了的。不过目前所覆盖的对象还比较有限,但是基础的一些功能都能完成,具体的大家可以到SDK里面进行查询
  Server .NET Managed and Silverlight ECMAScript
  Microsoft.SharePoint.SPContext Microsoft.SharePoint.Client.ClientContext SP.ClientContext
  Microsoft.SharePoint.SPSite Microsoft.SharePoint.Client.Site SP.Site
  Microsoft.SharePoint.SPWeb Microsoft.SharePoint.Client.Web SP.Web
  Microsoft.SharePoint.SPList Microsoft.SharePoint.Client.List SP.List
  Microsoft.SharePoint.SPListItem Microsoft.SharePoint.Client.ListItem SP.ListItem
  Microsoft.SharePoint.SPField (including major derived classes) Microsoft.SharePoint.Client.Field SP.Field
  Microsoft.SharePoint.WebPartPages.SPLimitedWebPartManager Microsoft.SharePoint.Client.WebParts.LimitedWebPartManager SP.WebParts.LimitedWebPartManager
  4.强化的xslt客户端展现定制
  其实借助于xslt完全可以把xml数据展现成为你想要的任何html样式,关键就在于你的xslt怎么来转化,怎么来定义.其实微软内部有一种解决静态网站的方案就是用纯xslt+xml来构造页面,只不过所有的xml数据和xslt都得自己来设计和编写,大家都知道infopath的内容都是由xml构成的,所以借助于infoPath来定义xml数据是一种十分方便的形式。整体的解决方案就变成了infoPath+xslt+Form Service+SharePoint。对于静态来说,还是十分好用的,而且xslt引擎对于静态的处理,效率是很高的。
  5.同步框架引擎For SharePoint
  这块的内容还是在于同步不同设备和平台上面的数据,但是由于支持的问题,目前这个同步框架版本为4.0CTP,在SharePoint里面体现为,在GetListItemChangesWithKnowledge的时候内部封装用到了这个同步库。
  未完待续,,,

Sharepoint里面的自定义类型字段

你用百度或者google搜索一下,肯定显示出来的东西一大堆,然后你就会发现,其中有90%是雷同或者是完全照抄,然后你再看中文版本的东西,然后用英文搜索之,最后发现中文里面大多有价值的东西都是英文直接照抄翻译过来的,比较有水平的技术人员一般都写上了原英文名的出处和作者,而大多只是翻译过来,稍加修改,然后就打上了自己的名字,最后你根据文章做起来,错误百出,而且大多数解决办法还得靠英文原文或者下面的问题回答来解决,这是最早在技术论坛里面的看blog的感觉。当然,目前也有这种感觉,所以有些牛人就说,你要看学技术,别学着忽悠翻译和转载,理解才是王道。
谨记教诲,东西只有自己消化了的,才是自己的。
说起内容类型,一般只要是SharePoint的初级介绍文章,都会有所涉及,大意是这样:SharePoint用来扩展现有的字段类型不足,为开发人员预留的扩展接口。要自己做一个自定义类型字段,其实不难,用VS里面的SharePoint模板就可以迅速建立一个起来。当然,详细的分来,自定义类型字段,也就是CustomerField里面大致有这么几个组成部分。
1,域的值:我们知道,每一个字段里面都存储着一堆的值,而这些值是什么样子的?就是用这个类来进行描述的,比如说:一个SPFieldLookup的值类,就是SPFieldLookupValue。具体的可以在WSS3.0的SDK里面查到,只要搜索SPField就可以看到系统目前定义的所有SPField。当然,你要是怕麻烦,也可以写一小段的代码来实现,以下代码列出的当前的web下的所有的内容类型的SPField类型,以及其对应的value的类型。

using (SPSite site = new SPSite(strSiteUrl))
{
foreach (SPField fi in site.RootWeb.Fields)
{
Console.WriteLine("域标题:" + fi.Title.ToString());
Console.WriteLine("域名称:" + fi.InternalName.ToString());
Console.WriteLine("域类型:" + fi.GetType().ToString());
if (fi.FieldValueType != null)
{
Console.WriteLine("域对应的值类型:" + fi.FieldValueType.FullName);
}
Console.WriteLine("------");
}
}

这里还需要注意的一个问题是重写FieldValue的时候,如果是重写的多行的Field的Value,那么值里面的属性顺序是用0开始,逐个递增,构造函数里面要给进数量,调用基类来构造。
2,域的本体:这里就决定了你的域的名字,从哪个基类集成过来,以及你的域的基础性质,只要是SPField的域,一般都可以继承过来重写,然后加以使用,SharePoint同样也提供了专门用来扩展的基类型,具体的可以查SDK得到。这里还需要注意的地方是有一个特殊的SPFieldMultiColumn 是系统专门预留给编程人员使用的扩展接口,里面内容实际上是多值的,而且值是以;#分割的。十分类似于查阅项里面的多值的情况。
3,域在使用时的展现(可带ascx):一般这里都继承于BaseFieldControl,继承下来的这个Control可以当作一个十分类似于WebPart的Control来处理,不过这里需要注意的是,怎样把你在控件里面设定的值,和域本体里面的值进行存取的交互。换句话说,这个Control只是用来便于展现和设定你的Field的值,真实的值还是转化以后存储到了你的Field里面,而不是控件模板里面。
所以这里有一个十分重要的需要重写的属性Value,用来存和取你的数据。还有一个是DefaultTemplateName这个属性,用来定位你的asx的模板的。这里在展现的时候用的ascx控件模板,会用到一个特殊的类型,所有的控件都是放到这个节里面的,并且在后台里面根据控件的ID来取控件的句柄。
4,域在设定时的展现(可带ascx):调用机制比较复杂,负责联动前台的展示界面,以及后台设定的保存,数据的读取,所有的设定都是保存到Field本体里面,所以这里需要和Field本体有一个交互,最重要的函数应该是
public void OnSaveChange(SPField field, bool isNew) 所有在你控件里面设定好的值,都通过这个函数进行存储。
当然,在一开始对预存的值进行读取的操作也是必不可少的,函数采用的是
public void InitializeWithField(SPField field)
这里输入的参数相当于你当前设定的field的句柄,使用的时候用as转化为你定义的域类型就可以了。
在域设定的页面里面,同样采用的是模板的ascx,只是引用的模板不一样而已,具体的可以参看页面的代码。
5,域的定义:这里又是一个头疼的xml,一堆的属性,用来描述你的Field,好让SharePoint能够正确的识别,比较重要的有这么几个节

SharePoint高效编程技巧

刚统计了一下自己的sharepoint代码,发现代码数量也上六位数了,虽然大多是编译器自己生成的内容,但是自己也积累了一些东东,今天查阅新版的2010的SDK发现内部总结了一些,决定结合最佳实践,写一些自己的心得,或许这些心得已经有人写过,不过抒发一下自己的感想,希望大家看后能够有所启发,尽量写出忧患较少的代码,少埋地雷,为系统的稳定高速运行,建立一个好的代码基础,权当是抛砖引玉吧。  (1),关于匿名的调用和使用以及最高委托。   在开发当中不可避免的需要用到匿名用户来处理一些相关的操作,比如你在主页上,读取的信息,通知的,如果你的有些页面是开放匿名的,那么你执行在这些匿名页面上面的代码,就是以匿名方式运行的,当然,大部分时候我们原来喜欢利用最高委托来实现我们需要的效果,相当于,提升部分读取数据的代码域的权限,在执行完成以后,再回到匿名级别。代码很简单,如下:    SPSecurity.RunWithElevatedPrivileges(delegate()   {   using (SPSite site = new SPSite(SPContext.Current.Site.Url))   {   using (SPWeb web = site.RootWeb)   {  //在这里面执行你需要进行的操作。  }  }  }  });    在这种方式下,如果你尝试在匿名委托区里面获取当前的用户,你会发现你得到的是网站集管理员的。    这种方式当然是在你可以控制的范围内的,并且的确可以解决我上面所说的问题,但是你可否想过,如果你在一个IIS的工作进程当中调用sharepoint的对象模型,那么执行的用户又会是什么呢?这里给出一段SDK中的代码,很好的解释了在windown api层次,对windows模拟的用户的测试。    // This sample demonstrates the use of the WindowsIdentity class to impersonate a user.  // IMPORTANT NOTES:  // This sample requests the user to enter a password on the console screen.  // Because the console window does not support methods allowing the password to be masked,  // it will be visible to anyone viewing the screen.  // On Windows Vista and later this sample must be run as an administrator.       using System;  using System.Runtime.InteropServices;  using System.Security.Principal;  using System.Security.Permissions;  using Microsoft.Win32.SafeHandles;  using System.Runtime.ConstrainedExecution;  using System.Security;      public class ImpersonationDemo  {   [DllImport(“advapi32.dll”, SetLastError = true, CharSet = CharSet.Unicode)]   public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,   int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);     [DllImport(“kernel32.dll”, CharSet = CharSet.Auto)]   public extern static bool CloseHandle(IntPtr handle);     // Test harness.   // If you incorporate this code into a DLL, be sure to demand FullTrust.   [PermissionSetAttribute(SecurityAction.Demand, Name = “FullTrust”)]   public static void Main(string[] args)   {   SafeTokenHandle safeTokenHandle;   try   {   string userName, domainName;   // Get the user token for the specified user, domain, and password using the   // unmanaged LogonUser method.   // The local machine name can be used for the domain name to impersonate a user on this machine.   Console.Write(“Enter the name of the domain on which to log on: “);   domainName = Console.ReadLine();     Console.Write(“Enter the login of a user on {0} that you wish to impersonate: “, domainName);   userName = Console.ReadLine();     Console.Write(“Enter the password for {0}: “, userName);     const int LOGON32_PROVIDER_DEFAULT = 0;   //This parameter causes LogonUser to create a primary token.   const int LOGON32_LOGON_INTERACTIVE = 2;     // Call LogonUser to obtain a handle to an access token.   bool returnValue = LogonUser(userName, domainName, Console.ReadLine(),   LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,   out safeTokenHandle);     Console.WriteLine(“LogonUser called.”);     if (false == returnValue)   {   int ret = Marshal.GetLastWin32Error();   Console.WriteLine(“LogonUser failed with error code : {0}”, ret);   throw new System.ComponentModel.Win32Exception(ret);   }   using (safeTokenHandle)   {   Console.WriteLine(“Did LogonUser Succeed? ” + (returnValue ? “Yes” : “No”));   Console.WriteLine(“Value of Windows NT token: ” + safeTokenHandle);     // Check the identity.   Console.WriteLine(“Before impersonation: “   + WindowsIdentity.GetCurrent().Name);   // Use the token handle returned by LogonUser.   using (WindowsImpersonationContext impersonatedUser = WindowsIdentity.Impersonate(safeTokenHandle.DangerousGetHandle()))   {     // Check the identity.   Console.WriteLine(“After impersonation: “   + WindowsIdentity.GetCurrent().Name);   }   // Releasing the context object stops the impersonation   // Check the identity.   Console.WriteLine(“After closing the context: ” + WindowsIdentity.GetCurrent().Name);   }   }   catch (Exception ex)   {   Console.WriteLine(“Exception occurred. ” + ex.Message);   }     }  }  public sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid  {   private SafeTokenHandle()   : base(true)   {   }     [DllImport(“kernel32.dll”)]   [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]   [SuppressUnmanagedCodeSecurity]   [return: MarshalAs(UnmanagedType.Bool)]   private static extern bool CloseHandle(IntPtr handle);     protected override bool ReleaseHandle()   {   return CloseHandle(handle);   }  }  (2),避免重复的构造SPWeb, SPSite这些大对象  我们在写一些测试的程序片段的时候,我们会发现,当我们用在new SPSite的时候,硬盘会一阵狂响,然后w3wp.exe进程就开始吃进内存,有些时候new SPSite大对象必要,有些时候却是没有必要,大家大可在上下文存在的情况下,调用类似如下的方法,从上下文当中,取出这些已经建立好的对象的句柄,然后就可以直接进行使用。  SPWebApplication webApplication = SPWebApplication.Lookup(new Uri(“http://localhost/”);  SPFarm farm = webApplication.Farm;  SPContentDatabase content = webApplication.ContentDatabases[0];    上面取出了应用程序,场,以及内容数据库对应的大对象的句柄,那么对SPSite和SPWeb的取,就不再是问题,具体的可以参看SDK里面这几个类。  这里要强调一下在我们常用的eventhandler里面的的一些和这些大对象的代码。  实际上我们的eventhander在系统当中,是被堆到spv3timer里面来进行处理的,相当于我们每一次对item的操作,其触发的事件,是在后台进行执行的,那么他所操作的代码,就不是在前端进程里面进程处理,所以我们对其中的一些处理就可以优化如下  原始的非常不效率的写法(在eventHandler中):  using (SPSite site = new SPSite(properties.WebUrl))   {   using (SPWeb web = site.OpenWeb())   {   SPList list = web.Lists[properties.ListId];   SPListItem item = list.GetItemByUniqueId(properties.ListItemId);   // Operate on an item.   }   }    改进后的写法:  // Retrieve SPWeb and SPListItem from SPItemEventProperties instead of  // from a new instance of SPSite.  SPWeb web = properties.OpenWeb();  // Operate on the SPWeb object.  SPListItem item = properties.ListItem;  // Operate on an item.    (3)大对象的销毁,销毁,还是销毁    我们知
道,我们可以借助using 来控制对象的作用域,当然,前提是这个对象是继承与IDispose接口,所以我们非常常用的几个大对象,也就大多采用using 的方式来进行使用,当然我们也可以用类似于C++里面的写法,手工的在finally里面进行对象的销毁处理。  做法一:是我们大家一般使用的标准做法,没问题。  using(SPSite oSPsite = new SPSite(“http://server”))  {   using(SPWeb oSPWeb = oSPSite.OpenWeb())   {   str = oSPWeb.Title;   str = oSPWeb.Url;   }  }  做法二:  大家可以看出,在一个里面我们不但new spite还有web ,而域控制的只是web,那么site肯定是溢出了。非常垃圾的写法,  void CombiningCallsLeak()  {   using (SPWeb web = new SPSite(SPContext.Current.Web.Url).OpenWeb())   {   // … New SPSite will be leaked.   } // SPWeb object web.Dispose() automatically called.  }    做法三:  还有这种直接不new就引用上下文的句柄的方式,也是非常致命的,因为他的代码一执行完,上下文的web就被销毁,系统自身的肯定还需要进行一些操作,所以这种写法必然是在代码跑完不出异常,然后页面就莫名奇妙的出错,当然,你如果RP十分好的话,也可能这种问题不出现。  using( SPWeb web = SPControl.GetContextWeb(HttpContext.Current)) { … }    最佳的using做法:  void CombiningCallsBestPractice()  {   using (SPSite siteCollection = new SPSite(SPContext.Current.Web.Url))   using (SPWeb web = siteCollection.OpenWeb())   {   // Perform operations on site.   } // SPWeb object web.Dispose() automatically called; SPSite object    // siteCollection.Dispose() automatically called.  }    最佳的try catch手工做法  SPSite oSPSite = null;  SPWeb oSPWeb = null;    try  {   oSPSite = new SPSite(“http://server”);   oSPWeb = oSPSite.OpenWeb(..);     str = oSPWeb.Title;  }  catch(Exception e)  {   // Handle exception, log exception, etc.  }  finally  {   if (oSPWeb != null)   oSPWeb.Dispose();     if (oSPSite != null)   oSPSite.Dispose();  }    (4),关于大容量的list的处理  对于SPList列表库,在数量超过5k以后,性能就开始急剧衰减的问题,其实很多地方都谈过,衰减的主要体现是在遍历所有item的情况下出现的,对于插入和删除的操作,没有什么影响,曾经写过一个程序来测试一百万条数据的检索和查询,在配置了个别列的索引,仍旧没有根本的改变,现在主要说一下主要推荐采用的方式,query,可以基本上忽略条数的限制,缺点就是对于一些自定义列的查询比较无力,在2010版本里面,提供了可以直连sql的list,从根本上解决了大容量的条目的问题。  下面是典型的query代码    SPQuery query = new SPQuery();  SPListItemCollection spListItems ;   string lastItemIdOnPage = null; // Page position.  int itemCount = 2000     while (itemCount == 2000)  {   // Include only the fields you will use.   query.ViewFields = “”;    query.RowLimit = 2000; // Only select the top 2000.   // Include items in a subfolder (if necessary).   query.ViewAttributes = “Scope=”Recursive””;   StringBuilder sb = new StringBuilder();   // To make the query order by ID and stop scanning the table, specify the OrderBy override attribute.   sb.Append(“”);   //.. Append more text as necessary ..   query.Query = sb.ToString();   // Get 2,000 more items.      SPListItemCollectionPosition pos = new SPListItemCollectionPosition(lastItemIdOnPage);   query.ListItemCollectionPosition = pos; //Page info.   spListItems = spList.GetItems(query);   lastItemIdOnPage = spListItems.ListItemCollectionPosition.PagingInfo;   // Code to enumerate the spListItems.   // If itemCount    itemCount = spListItems.Count;    }    (5),关于对象的cache的问题。   在企业级别的应用当中,由于对象模型本身的速度限制,不得不采用一些缓存的方式来提升整体的性能,在小容量数据的存储时,我们一般采用static变量来处理,并且自己维护一个缓存的队列,来解决,但是大部分的页面应用当中,我们采用cache来进行处理,有些时候大家可能忽略一些线程上面的问题,在大量用户同时访问页面,并且对一群对象进行update操作的时候,有能造成拥堵,大家都知道asp.net是线程安全的,同样,我们的update操作也是,如果用户操作量比较大,那么update就要在不用的用户线程之间进行切换,所以在这里建议采用下面的方式来进行:  public void CacheData()  {   SPListItemCollection oListItems;     lock(_lock)    {   oListItems = (SPListItemCollection)Cache[“ListItemCacheName”];   if(oListItems == null)   {   oListItems = DoQueryToReturnItems();   Cache.Add(“ListItemCacheName”, oListItems, ..);   }   }  }

InfoPath+Workflow+Moss (一)

这两天在和客户打交到,客户提了一些比较有意义的需求,在更新原有的流程上面,对新的模块开始进行设计和开发,空间较大,利润较大,不过老板貌似比较抠门,手下工作积极性貌似都不是很高,提交的东西得过且过,唉,看来还是我领导无方,很多事情还是得自己做,不过有两位同仁挺不错,回想一下,其实这个项目我们叁就能搞定。
本文为本blog原创,转载请注明出处,联系人murdercdh@hotmail.com
如果你刚开始看.net编程,那么请你跳到这里,
如果你刚开始接触Moss,那么请你跳到这里,
如果你刚开始接触WF,那么请你跳到这里。
项目基本完成了,一期的东西基本都做完了,采用的纯Moss下面的开发,也应该总结一些要点和东西,为了不使自己以后完全忘记,或者说:为了让广大困惑的人,能在此找到一些捷径和关键点,在此能有所用,也就欣慰不已。
要点一:INFOPATH的XML处理
在对带有VBA的INFOPATH编码当中,你会发现在支持浏览器的INFOPATH表单里面,能够使用的对象模型和处理十分有限,但是为了快捷,我们采用了它,因为利用现有的XML处理,加上页面当中的规则,基本上可以完成所有的业务逻辑。其实在简单的理解来说,InfoPath表单,其本质数据完全是XML,只是在展现的时候,用特定的控件来展示你XML的数据的,而控件基本都是采用绑定的形式和你的XML数据交互的,并且辅之以相当强大的规则和操作,来完成你的应用逻辑,所以处理方式比较特殊。所有规则能做的,代码都能做,但是目前在展示方面还有挺多的局限,后面会有所提到。
问题一:如何处理你的重复节?
重复节,也许就是我们在INFOPATH里面用的最多的东西,其十分类似于我们在Asp.NET里面的Reperate,可以用来存放重复数据字段,而其在INFOPATH的XML数据的定义中,通常是三级层次结构的,如图,第一层是整个节点,第二层是每个子节点,每个子节点里面可以包含多个属性或者字段,用xpath来解析的话,就是类似于如此:
/my:HQContentType/my:Enclosuregroup
/my:HQContentType/my:Enclosuregroup/my:EnclosureNode
/my:HQContentType/my:Enclosuregroup/my:EnclosureNode/my:Enclosure
最前面的是我的表单的名字。而用来处理节的对象模型,在支持浏览器的InfoPath表单里面,我们用得最多的是:
XmlNamespaceManager 和XPathNavigator ,XmlNamespaceManager是对整个XML的命名控件管理的对象模型,也可以理解为表单的命名空间,而XPathNavigator就是采用XPATH来对xml数据进行操作的对象模型。
读和取的办法如下代码:
< ?prettify linenums=true?>

///
/// 把节里的重复节里的DropDownlist的值连成字符串
///
/// 节里的组
/// DropDownlist的名字
/// 分割字符
/// 正却执行返回结果,异常返回空值
public string RepeatDllValuesTostring(string xpath_Group, string strDll_Name, char splitchar)
{
string returnValue = string.Empty;
try
{
XmlNamespaceManager ns = this.NamespaceManager;
XPathNavigator xpRoot = this.MainDataSource.CreateNavigator();
XPathNavigator xpCompanyGroup = xpRoot.SelectSingleNode(xpath_Group, ns);
XPathNodeIterator xpGroupIter = xpCompanyGroup.SelectChildren(XPathNodeType.Element);
StringBuilder SBvalue = new StringBuilder();
while (xpGroupIter.MoveNext())
{
XPathNavigator xpCurrent = xpGroupIter.Current;
string strValue = xpCurrent.SelectSingleNode("my:" + strDll_Name, ns).Value;
if (!(string.IsNullOrEmpty(strValue) || strValue.Trim().Length <= 0))                     {                         SBvalue.Append(strValue + splitchar);                     }                 }                   if (SBvalue.Length > 0)
{
returnValue = SBvalue.ToString();
returnValue = returnValue.Remove(returnValue.LastIndexOf(splitchar));
}
}
catch (SPException spex)
{
//错误处理
return string.Empty;
}
return returnValue;
}

问题二:如何处理你的附件。
附件节的处理方式,比较多样,默认的如果附件节存储的地方是InfoPath本身的话,InfoPath有自己的一套编码转化方式,实际上还是将附件打成了流,存储到了相应的XML的节里面。试想,如果文件比较大,那么表单的载入将会十分的缓慢,这样做的效率比较低下。但是可以利用附件节做为媒介,运用文档库,把文件上传到Moss里面,在前台留一个简单的url,同样,在InfoPath表单载入的时候,也可以把文档库里面的文件读取到InfoPath的附件节里,提供用户下载查看。
这里提供两个类,一个用来编码,一个用来解码,下面是编码和解码的类,这里有需要注意的是,附件节对应的xml节中,如果读入了文件,要把nil属性给去掉,否则会报错。
< ?prettify linenums=true?>

XPathNavigator xpExcelFileNode = xpExcels.SelectSingleNode("my:AssistFileNode", ns);
xpExcels.AppendChild(xpExcelFileNode);
XPathNodeIterator xpExcelsIterator = xpExcels.SelectChildren(XPathNodeType.Element);
while (xpExcelsIterator.MoveNext())
{
XPathNavigator xptempExcelFileNode = xpExcelsIterator.Current;
XPathNavigator xpExcel = xptempExcelFileNode.SelectSingleNode("my:AssistFile", ns);
if (xpExcel.MoveToAttribute("nil", "http://www.w3.org/2001/XMLSchema-instance"))
{
xpExcel.DeleteSelf();
InfoPathAttachmentEncoder encoder = new InfoPathAttachmentEncoder();
string fileStream = encoder.GetTheBase64String(fo);
xpExcel.SetValue(fileStream);
break;
}
}
public class InfoPathAttachmentEncoder
{
private string base64EncodedFile = string.Empty;
private string fullyQualifiedFileName;
///
/// Creates an encoder to create an InfoPath attachment string.
///
///
public InfoPathAttachmentEncoder(string fullyQualifiedFileName)
{
if (fullyQualifiedFileName == string.Empty)
throw new ArgumentException("Must specify file name", "fullyQualifiedFileName");
if (!File.Exists(fullyQualifiedFileName))
throw new FileNotFoundException("File does not exist: " + fullyQualifiedFileName, fullyQualifiedFileName);
this.fullyQualifiedFileName = fullyQualifiedFileName;
}
public InfoPathAttachmentEncoder()
{
}
public string GetTheBase64String(SPFile spFile)
{
Stream tempFileStream = spFile.OpenBinaryStream();
//BinaryReader br = new BinaryReader(tempFileStream);
// This memory stream will hold the InfoPath file attachment buffer before Base64 encoding.
MemoryStream ms = new MemoryStream();
// Get the file information.
using (BinaryReader br = new BinaryReader(tempFileStream))
{
string fileName = spFile.Name;
uint fileNameLength = (uint)fileName.Length + 1;
byte[] fileNameBytes = Encoding.Unicode.GetBytes(fileName);
using (BinaryWriter bw = new BinaryWriter(ms))
{
// Write the InfoPath attachment signature.
bw.Write(new byte[] { 0xC7, 0x49, 0x46, 0x41 });
// Write the default header information.
bw.Write((uint)0x14);       // size
bw.Write((uint)0x01);       // version
bw.Write((uint)0x00);       // reserved
// Write the file size.
bw.Write((uint)br.BaseStream.Length);
// Write the size of the file name.
bw.Write((uint)fileNameLength);
// Write the file name (Unicode encoded).
bw.Write(fileNameBytes);
// Write the file name terminator. This is two nulls in Unicode.
bw.Write(new byte[] { 0, 0 });
// Iterate through the file reading data and writing it to the outbuffer.
byte[] data = new byte[64 * 1024];
int bytesRead = 1;
while (bytesRead > 0)
{
bytesRead = br.Read(data, 0, data.Length);
bw.Write(data, 0, bytesRead);
}
}
}
// This memorystream will hold the Base64 encoded InfoPath attachment.
MemoryStream msOut = new MemoryStream();
using (BinaryReader br = new BinaryReader(new MemoryStream(ms.ToArray())))
{
// Create a Base64 transform to do the encoding.
ToBase64Transform tf = new ToBase64Transform();
byte[] data = new byte[tf.InputBlockSize];
byte[] outData = new byte[tf.OutputBlockSize];
int bytesRead = 1;
while (bytesRead > 0)
{
bytesRead = br.Read(data, 0, data.Length);
if (bytesRead == data.Length)
tf.TransformBlock(data, 0, bytesRead, outData, 0);
else
outData = tf.TransformFinalBlock(data, 0, bytesRead);
msOut.Write(outData, 0, outData.Length);
}
}
msOut.Close();
return base64EncodedFile = Encoding.ASCII.GetString(msOut.ToArray());
}
///
/// Returns a Base64 encoded string.
///
/// String
public string ToBase64String()
{
if (base64EncodedFile != string.Empty)
return base64EncodedFile;
// This memory stream will hold the InfoPath file attachment buffer before Base64 encoding.
MemoryStream ms = new MemoryStream();
// Get the file information.
using (BinaryReader br = new BinaryReader(File.Open(fullyQualifiedFileName, FileMode.Open, FileAccess.Read, FileShare.Read)))
{
string fileName = Path.GetFileName(fullyQualifiedFileName);
uint fileNameLength = (uint)fileName.Length + 1;
byte[] fileNameBytes = Encoding.Unicode.GetBytes(fileName);
using (BinaryWriter bw = new BinaryWriter(ms))
{
// Write the InfoPath attachment signature.
bw.Write(new byte[] { 0xC7, 0x49, 0x46, 0x41 });
// Write the default header information.
bw.Write((uint)0x14);       // size
bw.Write((uint)0x01);       // version
bw.Write((uint)0x00);       // reserved
// Write the file size.
bw.Write((uint)br.BaseStream.Length);
// Write the size of the file name.
bw.Write((uint)fileNameLength);
// Write the file name (Unicode encoded).
bw.Write(fileNameBytes);
// Write the file name terminator. This is two nulls in Unicode.
bw.Write(new byte[] { 0, 0 });
// Iterate through the file reading data and writing it to the outbuffer.
byte[] data = new byte[64 * 1024];
int bytesRead = 1;
while (bytesRead > 0)
{
bytesRead = br.Read(data, 0, data.Length);
bw.Write(data, 0, bytesRead);
}
}
}
// This memorystream will hold the Base64 encoded InfoPath attachment.
MemoryStream msOut = new MemoryStream();
using (BinaryReader br = new BinaryReader(new MemoryStream(ms.ToArray())))
{
// Create a Base64 transform to do the encoding.
ToBase64Transform tf = new ToBase64Transform();
byte[] data = new byte[tf.InputBlockSize];
byte[] outData = new byte[tf.OutputBlockSize];
int bytesRead = 1;
while (bytesRead > 0)
{
bytesRead = br.Read(data, 0, data.Length);
if (bytesRead == data.Length)
tf.TransformBlock(data, 0, bytesRead, outData, 0);
else
outData = tf.TransformFinalBlock(data, 0, bytesRead);
msOut.Write(outData, 0, outData.Length);
}
}
msOut.Close();
return base64EncodedFile = Encoding.ASCII.GetString(msOut.ToArray());
}
}
public class InfoPathAttachmentDecoder
{
private const int SP1Header_Size = 20;
private const int FIXED_HEADER = 16;
private int fileSize;
private int attachmentNameLength;
private string attachmentName;
private byte[] decodedAttachment;
///
/// Accepts the Base64 encoded string
/// that is the attachment.
///
public InfoPathAttachmentDecoder(string theBase64EncodedString)
{
byte[] theData = Convert.FromBase64String(theBase64EncodedString);
using (MemoryStream ms = new MemoryStream(theData))
{
BinaryReader theReader = new BinaryReader(ms);
DecodeAttachment(theReader);
}
}
private void DecodeAttachment(BinaryReader theReader)
{
//Position the reader to get the file size.
byte[] headerData = new byte[FIXED_HEADER];
headerData = theReader.ReadBytes(headerData.Length);
fileSize = (int)theReader.ReadUInt32();
attachmentNameLength = (int)theReader.ReadUInt32() * 2;
byte[] fileNameBytes = theReader.ReadBytes(attachmentNameLength);
//InfoPath uses UTF8 encoding.
Encoding enc = Encoding.Unicode;
attachmentName = enc.GetString(fileNameBytes, 0, attachmentNameLength - 2);
decodedAttachment = theReader.ReadBytes(fileSize);
}
public void SaveAttachment(string saveLocation)
{
string fullFileName = saveLocation;
if (!fullFileName.EndsWith(Path.DirectorySeparatorChar.ToString()))
{
fullFileName += Path.DirectorySeparatorChar;
}
fullFileName += attachmentName;
if (File.Exists(fullFileName))
File.Delete(fullFileName);
FileStream fs = new FileStream(fullFileName, FileMode.CreateNew);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(decodedAttachment);
bw.Close();
fs.Close();
}
public string Filename
{
get { return attachmentName; }
}
public byte[] DecodedAttachment
{
get { return decodedAttachment; }
}
}

问题三:怎么处理表单的关闭和数据的提交?
在InfoPath里面,怎么向宿主环境提交数据,怎么吧表单提交到宿主里面呢?实现的方法有三种,一种是直接利用InfoPath的条件和规则以及操作无需编码,另外一个是在后台代码里面进行填写,前者局限性比较大,虽然能够完成一定的功能,但是比较局限,而用后台代码的话就不存在这个问题,可以很轻松的控制自己的逻辑和业务。第三种采用两者结合的方式,在尽量少的编码情况下,做到尽量多的东西(懒人的终极选择)。在提交的时候,采用的对象模型为,FileSubmitConnection,给段样例如下,原来还研究了一下ADO数据源的读取和提交,发现在表单的InternalStartup()以后,对其数据源的东西就无法更改了,所以和数据库操作联动的话,采用的还是自定义的assembly来进行操作,简单实用。
< ?prettify linenums=true?>

//获取 提交数据源 并设置提交的参数 提交到的文件夹 文件名
FileSubmitConnection fileSubmit = (FileSubmitConnection)this.DataConnections["FileSubmit"];
if (CurrentWeb.Url.EndsWith("/"))
{
fileSubmit.FolderUrl = HQIssueDocLib.ParentWeb.Url + HQIssueDocLib.RootFolder.Url;
}
else
{
fileSubmit.FolderUrl = HQIssueDocLib.ParentWeb.Url + "/" + HQIssueDocLib.RootFolder.Url;
}
//如果表单不存在
if (xpIsExist.Value.Equals("0"))
{
//提交
if (xpSubmitOrNot.Value.Equals("1"))
{
if (xpRedtape.Value.Length <= 0)                             {                                 xpErrorMessage.SetValue(formSaveSate.EmptyRedFile);                                 xpDispatchTitle.SetValue(string.Empty);                                 return;                             }                             //设置 CanModify为"0"为不可修改,IsSubmit为"1"已提交,IsExist为"1"为已存在,并提交                             xpCanModify.SetValue("0");                             xpIsSubmit.SetValue("1");                             xpIsExist.SetValue("1");                             fileSubmit.Filename.SetStringValue(filename);                             fileSubmit.Execute(); } } 问题四:怎么来处理表单的多视图? 一个InfoPath表单,可以采用多个视图来进行展现,并且绑定相关xml数据源的控件,可以定义新的规则和新的展现方式,这个个人认为是InfoPath良好设计的最佳体现。充分分离了数据和视图,但是目前感觉在WEB这块的INFOPATH表单,展现局限性还是很大,虽然可以自定自己的类似于AciviteX的控件,但是十分麻烦。不过后期肯定会陆续完善,因为微软内部的新Moss测试版本已经到14了。你可以在载入的时候根据读入的xml数据,来选择你所需要展现的视图,回传的时候,同样也可以更新你的视图,具体例子代码如下,思路就是通过检查传入的变量,调用SetDefaultView方法,这两个都是包含在FormLoad的LoadingEventArgs里面的,具体取传入的变量的方法是看InputParameters键值对集合里面的值,和queryString类似。   public bool SetCanModify(LoadingEventArgs loadingEventArgs)         {             try             {                 XmlNamespaceManager ns = this.NamespaceManager;                 XPathNavigator xpRoot = this.MainDataSource.CreateNavigator();                 //表单的标题                 XPathNavigator xpDispatchTitle = xpRoot.SelectSingleNode("my:HQContentType/my:DispatchTitleNode/my:DispatchTitle", ns);                 //值 "0" 没有提交,值 "1"提交                 XPathNavigator xpIsSubmit = xpRoot.SelectSingleNode("my:HQContentType/my:IsSubmit", ns);                 //值 "0"不允许修改 ,"1"允许修改                 XPathNavigator xpCanModify = xpRoot.SelectSingleNode("my:HQContentType/my:CanModify", ns);                 //值 "0"不允许关闭,"1"允许关闭                 XPathNavigator xpIsAllowClose = xpRoot.SelectSingleNode("my:HQContentType/my:StateGroup/my:IsAllowClose", ns);                 //表单是否存在                 XPathNavigator xpIsExist = xpRoot.SelectSingleNode("my:HQContentType/my:StateGroup/my:IsExist", ns);                   //包含openfrom,表示是从表单中的链接打开的                 bool isOpenFromForm = loadingEventArgs.InputParameters.ContainsKey("openfrom");                 //Task5是特殊情况,链接中的参数canmodify是标志允许修改                 bool isSpecialCanModify = loadingEventArgs.InputParameters.ContainsKey("canmodify");                   //对视图和关闭按钮进行设置                 if (xpDispatchTitle != null && xpDispatchTitle.Value.Trim().Length > 0 && xpIsExist != null && xpIsExist.Value.Equals("1"))
{
using (SPWeb web = new SPSite(SPContext.Current.Site.ID).OpenWeb(SPContext.Current.Web.ID))
{
//获得当前项目
SPList HQIssueDocLib = web.Lists["总部发文库"];
SPListItem currentItem = FindItemByFileName(HQIssueDocLib, xpDispatchTitle.Value);
//取Field的显示名
string strAuthor = string.Empty;
string strFileOrigin = GetDisplayNameByStaticName(HQIssueDocLib, "FileOrigin");
string strCanModify = GetDisplayNameByStaticName(HQIssueDocLib, "CanModify");
//如果文件存在,且CanModify域值不为空
if (xpIsExist != null && xpIsExist.Value.Equals("1") && currentItem[strCanModify] != null)
{
//依据不同情况取得文件的创建者的Field的显示名,下属呈文的时候Drafter是我们需要的创建者
if (currentItem[strFileOrigin] != null && currentItem[strFileOrigin].ToString().Equals("下属呈文"))
{
strAuthor = GetDisplayNameByStaticName(HQIssueDocLib, "Drafter");
}
else
{
strAuthor = GetDisplayNameByStaticName(HQIssueDocLib, "Author");
}
//取得当前用户和文件创建者的相关对象
SPFieldUserValue userValue = new SPFieldUserValue(web, currentItem[strAuthor].ToString());
SPUser currentUser = SPContext.Current.Web.CurrentUser;
string currentUserLogName = currentUser.LoginName;
//通过项的CanModify域值设置不同的视图和参数
string strVaue = currentItem[strCanModify].ToString();
switch (strVaue)
{
case "是":
//允许修改,当前用户就是创建者,则采取默认视图
if (currentUserLogName.Equals(userValue.User.LoginName))
{
//并且是从表单里打开,则不允许关闭表单
if (isOpenFromForm)
{
xpIsAllowClose.SetValue("0");
}
else//从文档库打开,允许关闭表单
{
xpIsAllowClose.SetValue("1");
}
xpCanModify.SetValue("1");
loadingEventArgs.SetDefaultView("DefaultView");
}
else//允许修改,但当前用户不是创建者,设置只读视图,并且不允许关闭表单
{
//if (isOpenFromForm)
//{
//    xpIsAllowClose.SetValue("0");
//}
//else
//{
//    xpIsAllowClose.SetValue("1");
//}
xpIsAllowClose.SetValue("0");
xpCanModify.SetValue("0");
loadingEventArgs.SetDefaultView("ReadOnlyView");
}
break;
default:
//这个是特殊的标志为,强制进行修改,是从表单链接打开,不允许关闭
if (isSpecialCanModify)
{
xpCanModify.SetValue("1");
xpIsAllowClose.SetValue("0");
loadingEventArgs.SetDefaultView("DefaultView");
return true;
}
if (isOpenFromForm)//不允许修改,从表单链接打开,不允许关闭
{
xpIsAllowClose.SetValue("0");
}
else//不允许修改,从文档库链接打开,允许关闭
{
xpIsAllowClose.SetValue("1");
}
xpCanModify.SetValue("0");
loadingEventArgs.SetDefaultView("ReadOnlyView");
break;
};
}
}
}
}
catch (SPException spex)
{
return false;
}
return true;
}

(未完待续)
写代码和做技术,不在于你以前在会多少,也不在于你将来想学多少,而在于你今天在多少的时间内能学多少,写多少,想多少。
技术是根本,突然发现其实在公司最大的乐趣不是工作,而是和一些和自己有着共同爱好的人一起写程序,才是真正的乐趣所在。   可惜老板不懂,难留住人,也就难出好产品,想完全找一堆应届毕业生,一上来就写产品,哈哈,Are you dreaming?

InfoPath+Workflow+Moss (二)

要点二:WF和INFOPATH的数据处理
在做InfoPath,WF以及Moss关联的过程中,你必须清楚的了解三者之间是如何配合的,并且了解他们之间是怎么通信,怎么传递,来完成你所需要的流程配置,流程发起,以及流程task处理的问题。在开发Moss里面的InfoPath工作流中,你会关注到如何神奇创建task,响应task,以及处理task,最后清理task,这些都是与Moss本身重扩展了WF以后,新增了更加适合于Moss环境下的流程开发活动密不可分的,主要命名空间为microsoft.sharepoint.WorkflowActions。
问题一,怎么读入读出数据
在INFOPATH的表单当中,如何传入你自己数据呢,或者说,怎样从流程里面传递数据到表单里面去,这里有几种解决办法。
第一,是采用ItemMetadata数据绑定,
第二,是采用DOM模型直接对INFOPATH表单进行操作.
第三,是采用第三者中介来传输数据,比如说临时的列表库,或者临时的sql表,临时的文件,都是可以的。
下面说第一种方法:借助于XML数据源定义文件,INFOPATH可以将自己的数据绑定到数据源里面去,而数据源文件里面提供的只是数据的名字而已,并且,这个数据文件里面的变量的命名,必须带上OWS,这样才能被INFOPATH所识别,并且为MOSS里面的流程所用,,例子如下:
,
在INFOAPTH的表单设计里面选择添加了数据源以后,就可以在辅助数据源里面找到你所定义的变量了,并且在表单的Load以后,都可以从对应的域里面把你绑定的值读出来,因为在绑定的时候,数据已经进去了。
第二种方法比较特殊,实际上对于创建出来的infopath表单,就是一个xml文件而已,注意,这里不是说的xsn表单,xsn表单里面所包含的东西,远比这些要复杂得多,而创建出来的INFOPATH表单,只是一个存储数据的介质而已,所以这里采用DOM模型的话,完全可以对里面的数据进行读和写。

XmlDocument xmldot = new XmlDocument();
Stream strem = newFormTemplateFile.OpenBinaryStream();
xmldot.Load(strem);
strem.Close();
XmlNameTable xmlNameTab = (XmlNameTable)xmldot.NameTable;
XmlNamespaceManager xmldotNs = new XmlNamespaceManager(xmlNameTab);
xmldotNs.AddNamespace("my", "http://schemas.microsoft.com/office/infopath/2003/myXSD/2008-12-15T08:42:02");
//获取相关的节点
XPathNavigator xpdotRoot = xmldot.CreateNavigator();
//给节点DispatchTitle赋值和是否提交的节点的值
XPathNavigator xpDispatchTitle = xpdotRoot.SelectSingleNode("my:HQContentType/my:DispatchTitleNode/my:DispatchTitle", xmldotNs);
XPathNavigator xpIsSubmit = xpdotRoot.SelectSingleNode("my:HQContentType/my:IsSubmit", xmldotNs);
XPathNavigator xpCanModify = xpdotRoot.SelectSingleNode("my:HQContentType/my:CanModify", xmldotNs);
XPathNavigator xpIsExist = xpdotRoot.SelectSingleNode("my:HQContentType/my:StateGroup/my:IsExist", xmldotNs);

第三种方法就不用多说了,地球人都会。
问题二,怎么关联表单和流程,
对于刚开始开发的人来说,都会有一个疑问,Moss是怎么样知道你在什么时候task需要打开哪一个task表单的呢?其实这个问题并不难,关键在于以下几个点,一个是你的流程部署的时候的workflow的xml文件描述里面,这个文件是用来在feature激活的时候,告诉moss你的这个东西是什么并且你的这个东西是长什么样子的。
关键的是在workflow节下面的metadata节,你会看到里面有你的每一步的Task0_FormURN对应的表单的编号,这个编号你可以在你的INFOPATH设计,属性里面修改。这个编号对于每一个表单来说都是唯一的。这里附带说一下,后面的,ExtendedStatusColumnValues节,是用来扩充流程的状态的,在具体的流程代码里面操作如下:
在create_task活动里面,在创建任务的时候,就可以借助任务的属性TaskType来设定需要调用哪一个表单,这个值是一个序号,根据你在xml里面定义的序列来定义的从0开始。这里还要补充的就是,利用任务的ExtendedProperties就可以将你前面绑定了的值给进去了,注意,这里的值是没有ows_的。
Task2_Id = Guid.NewGuid();
Task2_Properties.TaskType = 1;
//给 Task2 的 ItemMetadata 的 历史记录 描述 赋值
Task2_Properties.ExtendedProperties[“HistoryRecord”] = string.IsNullOrEmpty(HistoryRecords) ? string.Empty : HistoryRecords;
Task2_Properties.ExtendedProperties[“Description”] = Task1_Properties.ExtendedProperties[“SubmitDescription1”] == null ? string.Empty : Task1_Properties.ExtendedProperties[“SubmitDescription1”];
Task2_Properties.ExtendedProperties[“ItemLinkUrl”] = string.IsNullOrEmpty(strItemLinkUrl) ? string.Empty : strItemLinkUrl;
如下:
urn:schemas-microsoft-com:office:infopath:Task1-headquarters:-myXSD-2008-12-02T12-38-25
urn:schemas-microsoft-com:office:infopath:Task2-headquarters:-myXSD-2008-12-03T02-24-54
urn:schemas-microsoft-com:office:infopath:Task3-headquarters:-myXSD-2008-12-05T03-10-50
urn:schemas-microsoft-com:office:infopath:Task4-headquarters:-myXSD-2008-12-05T05-41-56
urn:schemas-microsoft-com:office:infopath:Task5-headquarters:-myXSD-2008-12-15T08-42-02
问题三,怎么对Init ,asso,task类型的表单进行操作。
首先要解决的是Init表单里面的数据,asso表单类里面的数据和流程交互的问题,这两类单子比较特殊,Init表单是流程在启动时候初始输入进去的预定义,为流程的开始运行提供额外的初始化数据,asso表单为流程提供全局的辅助变量,只要能够得到当前流程的相关信息,就能取到对应的辅助设置数据。但是他们之间存在有不同。
在流程中,你可以借助于流程this.workflowProperties.InitiationData,或者this.workflowProperties. AssociationData来读取其中的内容,这两个串读出来的话,都是xml形式的,可以用DOM对象来进行处理,但是这里如果仔细的人会发现,在InitiationData其中的数据,会包含AssociationData里面的数据,简而言之,就是InitiationData>=AssociationData,当然,这只是对于编码当中的情况来说的,在设计和规划的时候,两者不能等同或者包含。在理解上是合理的,相当于,在流程依据模板创建的时候,会自动的将模板里面设定的辅助数据加入到流程的初始化数据当中去。而流程的初始化数据就不仅仅是assoc数据。
在表单中,可以借助于列表的绑定相关流程,来取到流程设定的asso的数据,但是要取到流程当中的init数据,就必须采用传值绑定的方法,具体的可以见问题一。下面给一段如何在表单里面读取asso数据的方法,大致的思路是,找到列表对应的asso表单,然后用dom读入表单,之后采用dom来读取对应节的数据,这里还提供另外一种方式。这里在读取的时候采用的是dom对象来对数据进行处理,同样可以采用一种特殊的方式。大家应该都记得,可以把infopath表单的schema保存下来,然后再把schema表单序列化为类对象,这里可以参考柴同学的step by step,英文原版的地址应该在这里,大家可以找找。

 
// 获取要启动工作流的  AssociationData ,并变成  XmlDocument
 
                    XmlDocument xdot = new XmlDocument();
                   foreach (SPWorkflowAssociation wfAssoc in list.WorkflowAssociations)
                    {
                        // search workflowassociation by name
                        if ((wfAssoc.BaseTemplate.Name.ToString().Equals("审批工作流")))
                        {
                            xdot.LoadXml(wfAssoc.AssociationData);
                            break;
                        }
                    }
                    //给 followApproverUsers 绑定的 重复项  添加项和值
                    XmlNameTable ntable = (XmlNameTable)xdot.NameTable;
                    XmlNamespaceManager Outns = new XmlNamespaceManager(ntable);
                    Outns.AddNamespace("my", "http://schemas.microsoft.com/office/infopath/2003/myXSD/2008-12-03T01:36:02");
                    XPathNavigator xdotRoot = xdot.CreateNavigator();
                    XPathNavigator xpdotSecondApprovalUsers = xdotRoot.SelectSingleNode("my:Association/my:SecondApprovalUsers", Outns);
                    if (xpdotSecondApprovalUsers.MoveToChild(XPathNodeType.Element))
                    {
                        do
                        {
                            if (!string.IsNullOrEmpty(xpdotSecondApprovalUsers.SelectSingleNode("my:DisplayName", Outns).Value) && 
                                 !string.IsNullOrEmpty(xpdotSecondApprovalUsers.SelectSingleNode("my:AccountId", Outns).Value) && 
                                   !xpdotSecondApprovalUsers.SelectSingleNode("my:AccountId", Outns).Value.ToLower().Contains("system"))
                            {
                                xpuser.SelectSingleNode("my:DisplayName", ns).SetValue(xpdotSecondApprovalUsers.SelectSingleNode("my:DisplayName", Outns).Value);
                                xpuser.SelectSingleNode("my:AccountId", ns).SetValue(xpdotSecondApprovalUsers.SelectSingleNode("my:AccountId", Outns).Value);
                                xpuser.SelectSingleNode("my:AccountType", ns).SetValue(xpdotSecondApprovalUsers.SelectSingleNode("my:AccountType", Outns).Value);
                                xpfollowApproverUsers.AppendChild(xpuser);
                            }
                        } while (xpdotSecondApprovalUsers.MoveToNext(XPathNodeType.Element));
                    }

最后就是task表单里面的数据了,前面已经说过task表单的数据传递了,这里要说的是一些基本的操作,借助于任务的ExtendedProperties,可以很方便的在流程当中取到所有在表单里面的数据,当然,首先你要指导表单里面的对应域的名称,前面已经说过,就是你的绑定的ItemMetadata.xml
问题四,在外部操作和读取InfoPath表单。
于本质来说,InfoPath表单本身就是一个xml的文件,但是其有别于普通的xml文件,他有其自己的命名空间,以及相关xsd定义,在设计阶段,大家可以看到xsn文件,如果打成一个压缩包的话,大家可以看出里面的一堆的内容,具体包里面的内容在这里就不多说了,我只需要我们自己的哪个空白的xml文件,带有架构,却没有数据,这样我们就可以把这个文件读取出来,然后打成流来进行处理了,代码如下。

XmlDocument xmldot = new XmlDocument();
Stream strem = newFormTemplateFile.OpenBinaryStream();
xmldot.Load(strem);
strem.Close();
XmlNameTable xmlNameTab = (XmlNameTable)xmldot.NameTable;
XmlNamespaceManager xmldotNs = new XmlNamespaceManager(xmlNameTab);
xmldotNs.AddNamespace("my", "http://schemas.microsoft.com/office/infopath/2003/myXSD/2008-12-15T08:42:02");
 
XPathNavigator xpdotRoot = xmldot.CreateNavigator();

这里有两个需要注意的地方,
(1),如果你要想在浏览器当中打开infopath表单,而这个表单不是通过你的鼠标点击创建的,而是通过你的代码创建的,你会发现你无法打开infopath表单,那么你需要知道以下的内容,任何带编码的infoath表单,在moss的文档库中解析,个人认为大致过程是这样的,在你的库中所保存的东西,不过是一个xml的文件,除了xml什么都没有,通过比较,只知道需要这样两个东西。
item[“模板链接”] = 模板库当中的连接地址后缀为.xsn;
item[“HTML 文件链接”] = “InfoPath.Document.2”;
实际上这里infopath的东西还是通过发布到了moss站点里面的模板来进行处理的,因为表单的后台逻辑的编码dll,最终还是在发布的时候打包到了xsn的表单当中,所以最终还是调用了模板来加载处理你的infopath表单。
(2),如果你的infopath表单打开的时候采用了超级连接,虽然连接到了你的xml数据文件,但是却无法在浏览器里面打开,这里需要添加打开的参数,?openin=browser,这样浏览器里面就能加载你的infopath表单了。
问题五,如何手工启动流程。
启动流程,一直是在moss里面比较麻烦的问题,因为流程的执行,在moss当中跨越了站点,跨越了库,所以原有的绑定列表的触发方式变得力不从心,代码要求在其他的地方也能随时的启动流程,在研究moss里面重写的workflow以后,发现对流程的启动是可以通过代码来完全实现的,并且可以不用借助于传统的workflowRuntime,实际上在moss对WF进行二次封装的时候,也屏蔽了对运行时里面的一些东西,具体的因素很多。

/// 
        /// 通过工作流模板名来启动工作流
        /// 
        /// 模板名
        /// 要启动的工作流的项(SPListItem)
        /// 返回是否成功的指示值
        public bool StartWorkFlow(string WFBaseTemplateName, SPListItem ListItem)
        {
             try
            {
                SPList list = ListItem.ParentList;
                string m_wfBaseTemplateName = string.Empty;
                for (int i = 0; i <  ListItem.Workflows.Count; i++)
                {
                    m_wfBaseTemplateName = ListItem.Workflows[i].ParentAssociation.BaseTemplate.Nam                    if (m_wfBaseTemplateName.Equals(WFBaseTemplateName))
                    {
                        //已经被启动就返回
                        return false;
                    }
                }
                //在当前list的WorkflowAssociations中找到要 启动的工作流  SPWorkflowAssociation
                m_wfBaseTemplateName = string.Empty;
                foreach (SPWorkflowAssociation wfAssoc in list.WorkflowAssociations)
                {
                    // search workflowassociation by name
                    m_wfBaseTemplateName = wfAssoc.BaseTemplate.Name;
                    if ((m_wfBaseTemplateName.Equals(WFBaseTemplateName)))
                    {
                        //启动工作流                        
                        ListItem.Web.Site.WorkflowManager.StartWorkflow(ListItem, wfAssoc, wfAssoc.AssociationData, true);
                        break;
                    }
                }
            }
            catch (SPException ex)
            {
                return false;
            }
            return true;
        }

(未完待续)
下一期 ASPX+WF+MOSS
写代码和做技术,不在于你以前在会多少,也不在于你将来想学多少,而在于你今天在多少的时间内能学多少,写多少,想多少。

SharePoint里面的Search结构框架

最好的说明是能够让你很快就明白的说明在说的是什么东西的东西,不论他是按照图片还是声音,还是文字。大多数时候我看英文翻译过来的中文书的感觉,就是大家读本文的第一句话所感受到的,所以更多的时候还是看英文原文吧,虽然N多的单词都是在猜意思,总比被中文绕得不知所云的好。
言归正传,最近小看了下SharePoint所提供的爬网服务的构架,感觉很好很强大,很容易操作和理解,听说新版本的企业级的搜索要当作独立的产品模块来卖,功能更加强大和YY,其实07里面的这些企业级别的功能就已经是按模块来收费的了,不是很好很强大,也许是大多应用都做的不深的缘故吧,挂羊头卖狗肉的事情还是很多了。当然,很多时候也是出于无奈。
13913589041324
企业级别的爬网包括了两大引擎,一个是查询引擎,一个是索引引擎。辅之以协议加工器,筛选器,内容索引,属性存储,搜索相关的配置数据,以及传说中的词法拆解器。当然,这些术语都是胡乱翻译过来的,具体代表什么,还得在应用中才能有深刻的体会,如果有经验的人士,看个构架图,应该也能看到个30%。
爬网,大多时候都是在没有人知道的情况下悄悄进行的,搜索引擎利用事先定义好的规则,创建一堆一堆的管道,从网络和各种数据源中检索有用的资源,并且创建快照和索引,数据筛选器负责对数据进行析取筛选,协议解析器对讲述来的数据进行包装加工,变成一堆堆分门别类的有组织有结构有分类的索引和快照,以方便数据的快速检索和定位,大海捞针,抽其纲要,吸其神髓,概览之,细节突破。
在爬网的过程当中,索引引擎把文档和文档的关键属性分离存储,文档的属性重新按照属性值分类存储,文档则分散存储,为了加速常用的查询,一些简单的全字符查询的索引会直接的存储下来。实际的内容项会存储在内容索引当中,用于内容查询。属性的分离存储同样可以用来维护和加强文档在爬网时的安全级别控制。
对于搜索关键字的拆解,爬完引擎专门提供了一个拆解器,他可以根据对应的语言类别来拆解关键字。这些都是大家在用google里面很常用的技术。
检索的过程说起来很简单,就是按照大家所提供的关键字,从索引里面迅速的检索出匹配的信息,当然,这个筛选匹配的过程就比较复杂了,不过我们有属性和索引在,所以对于信息的筛选和检索,没问题,二次校验,应该不会出错。
对于爬网数据的来源,可以从图上看出,
可以是如下的几种类型:
外部网站,
sp的网站,
exchange邮件服务器,
业务数据目录,
共享文件夹,
和自定义的内容。
10290355794349
企业级别搜索的schema有两种类型的属性组成,一种是爬网的属性,一种是托管的属性,他们之间有一定的映射关系。当爬网内容的时候,索引器从内容项目中萃取爬网的属性,这些属性根据他们的分类进行分组,分类的过程是有筛选器和协议加工器来完成的。
托管属性是一个个的用户搜索经验的集合,里面都是由爬网属性值组成的,所以必须有和他对应的文档属性的映射,所有的托管属性都是由共享服务提供程序管理和创建的。
爬网属性默认的有一下几种分类:
HTML
Lotus Notes
PDF
XML
Office
Exchange
People
Portal
SharePoint
如果想用编程的对象模型来看,没问题,看如下的代码:

try
{
string strURL = @"http://XXXX/ssp/admin/";
SearchContext context;
using (SPSite m_site = new SPSite(strURL))
{
context = SearchContext.GetContext(m_site);
}
Schema m_Schema = new Schema(context);
//Schema siteSchema = new Schema(SearchContext.GetContext("SharedServices1"));
//Schema sspSchema = new Schema(SearchContext.GetContext(new SPSite(strURL)));
ManagedPropertyCollection managedCategories = m_Schema.AllManagedProperties;
if (managedCategories.Count > 0)
{
foreach (ManagedProperty mp in managedCategories)
{
Console.WriteLine("托管的属性名称为: " + mp.Name.ToString());
Console.WriteLine("id为:" + mp.ID + "描述是: " + mp.Description.ToString());
Console.WriteLine("-------------");
}
}
CategoryCollection categories = sspSchema.AllCategories;
if (categories.Count > 0)
{
foreach (Category cat in categories)
{
Console.WriteLine(cat.Name.ToString());
foreach (CrawledProperty cpro in cat.GetAllCrawledProperties())
{
Console.WriteLine("爬网属性名称为: " + cpro.Name.ToString());
Console.WriteLine("id为:" + cpro.Propset + "变量类型是: " + cpro.VariantType.ToString());
Console.WriteLine("-------------");
}
}
}
}
catch (Exception ex1)
{
Console.WriteLine(ex1.ToString());
}
static void CreateMappingBetweenCrawledProAndManagePro(CrawledProperty crapro, ManagedProperty manpro, SearchContext context)
{
try
{
Mapping maping = new Mapping(crapro.Propset, crapro.Name, crapro.VariantType, manpro.PID);
MappingCollection mappingColect = manpro.GetMappings();
mappingColect.Add(maping);
manpro.SetMappings(mappingColect);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message.ToString());
}
}

对于映射的创建,主要思路就是创建一个mapping,然后添加到托管属性的映射集合中,然后更新托管属性的映射集合。
简要的说了那么多,其实这里只是一个抛砖引玉的说明而已,里面涉及到的东西远不止这些,在moss里面提供的东西,大多都可以轻点鼠标完成,但是对于深层次应用,还是需要施以对象模型来加以实现,所以对SDK的研读,是很有裨益的。我们的口号是有事就找SDK。