项目基本完成了,一期的东西基本都做完了,采用的纯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数据进行操作的对象模型。

读和取的办法如下代码:

/// <summary>

/// 把节里的重复节里的DropDownlist的值连成字符串

/// </summary>

/// <param name="xpath_Group">节里的组</param>

/// <param name="strDll_Name">DropDownlist的名字</param>

/// <param name="splitchar">分割字符</param>

/// <returns>正却执行返回结果,异常返回空值</returns>

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属性给去掉,否则会报错。

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;

/// <summary>

/// Creates an encoder to create an InfoPath attachment string.

/// </summary>

/// <param name="fullyQualifiedFileName"></param>

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());

}

/// <summary>

/// Returns a Base64 encoded string.

/// </summary>

/// <returns>String</returns>

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;

/// <summary>

/// Accepts the Base64 encoded string

/// that is the attachment.

/// </summary>

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来进行操作,简单实用。

//获取 提交数据源 并设置提交的参数 提交到的文件夹 文件名

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;

}

MOSS工作流 InfoPath+WorkFlow+Moss 开发要点相关推荐

  1. 学习:InfoPath + Workflow + MOSS(转)

    在MOSS 2007种利用InfoPath 2007结合Workflow Foundation可以高效的做出非常强大的工作流应用.因为在SDK中这部分的内容有点不流畅,读起来比较费劲.所以我想以我的一 ...

  2. InfoPath + Workflow + MOSS

    在MOSS 2007种利用InfoPath 2007结合Workflow Foundation可以高效的做出非常强大的工作流应用.因为在SDK中这部分的内容有点不流畅,读起来比较费劲.所以我想以我的一 ...

  3. MOSS 2010:Visual Studio 2010开发体验(26)——工作流开发概述

    这一篇开始,我将介绍在MOSS 2010中进行工作流开发的最佳实践.这一篇主要介绍有关的几个概念 1.什么是工作流 工作流开发是这几年都比较火的一个领域.工作流,顾名思义就是工作的流程,而软件系统所要 ...

  4. 工作流(Workflow)简介

    当今社会分工越来越细,在一个单位内部也越来越强调专业化,大部分工作都需要多个部门和员工合作完成.一个制度良好的单位往往对各种工作的工作流程以文件的形式固定下来,即使是管理不太正规的单位也有约定俗成的工 ...

  5. 工作流(Workflow) -- 工作流简介

    工作流(Workflow) – 工作流简介 数据库 Activiti的后台是有数据库的支持,所有的表都以ACT_开头. 第二部分是表示表的用途的两个字母标识. 用途也和服务的API对应. ACT_RE ...

  6. CRM开发要点(一)

    从本文开始陆续整理下CRM的开发要点. CRM的UI基本框架如下图所示. A顶部标题区域:B左侧导航区域:C中间工作区域. AB连在一起SAP称为L形区域,这一块的内容是由系统管理的,用户不能自定义. ...

  7. WWF(Windows Workflow Foundation)开发环境的建立。 .NET 技术前瞻,WWF,Windows,Workflow,Foundation...

    1.安装WindowsSDK for Vista,有1.14G之巨,可以从MS的网站上下载,不过需要验证WINDOWS,可以去讯雷区用WindowsSDK为关键字搜索下载. 2.安装Virsul St ...

  8. 体感(Kinect)开发要点总结一

    体感(Kinect)开发要点总结一 Kinect有两类摄像头,近红外摄像头和普通的视频摄像头.视频摄像头提供了一般摄像头类似的彩色影像.       Kinect的彩色摄像头默认每秒产生30副Colo ...

  9. Contiki开发要点

    Contiki开发要点 一.Protothread变量定义 Adam Dunkels的论文中提到,基于Contiki的protothread开发时要特别注意变量的生存周期,分配在栈中的自动变量,随着函 ...

最新文章

  1. 告别2013,迎接2014
  2. 原生js已载入就执行函数_手写CommonJS 中的 require函数
  3. xgboost原理及应用
  4. gRPC in ASP.NET Core 3.x - gRPC 简介(2)
  5. 火狐怎么放大页面?火狐浏览器页面放大技巧
  6. 图文详解linux/windows mysql忘记root密码解决方案
  7. Linux操作系统的常用基本指令
  8. C# 正则表达式 匹配IP地址
  9. github操作实用命令
  10. python简明教程中文pdf-简明Python教程-中文版.pdf
  11. socket 服务器端和客户端通信,面向TCP的
  12. 网易邮箱发送显示服务器出错,网易邮件发送不出去的错误代码详解 (MI:SFQ错误等)...
  13. 浙江2段线能上什么计算机学校,二段线考生看过来!这些浙江省内热门高校还有热门专业可捡漏...
  14. epoll 为什么用红黑树?
  15. 基于Qt的网络五子棋游戏对战
  16. 我们为什么要结婚?(
  17. 前端canvas画海报
  18. 淘宝玉伯引发Web前后端研发模式讨论
  19. 大战在即!手机芯片巨头“All in”智能汽车,5G只是冰山一角
  20. jQuery取值和赋值的基本方法

热门文章

  1. App引流推广:能够提高用户的转化的技术
  2. php源码单号生成,PHP生成唯一订单号
  3. 中软国际java 笔试 面试题
  4. 2017年上海最新落户政策重磅出炉!你达标了吗?(明年就毕业了希望一切顺利)
  5. 项目组合管理(PPM)
  6. 神经元网络算法的思想,神经元算法大全图解
  7. 【深入Java虚拟机】之七:Javac编译与JIT编译
  8. 【操作系统】-- 动态分区分配算法(首次适应算法FF、最佳适应算法BF、最坏适应算法WF、循环首次适应算法NF)
  9. ArcGIS JSAPI2.0在IIS上的安装
  10. 用代码恶搞基友的小玩意儿