您的位置:首页 > 其它

Office文档在线编辑的实现-转

2008-05-24 12:39 776 查看
http://www.cnblogs.com/jianyi0115/

Office xp之后的版本支持通过webdav协议(http的扩展)直接编辑服务器上的文件。

IIS(6.0)支持webdav,这在IIS管理器的web服务扩展中可以看到.利用IIS作为webdav的服务器端,可以很容易的实现office(word,excel等)的在线编辑.

可以简单的实验一下:

确保IIS的webdav扩展安装并被启用了,建立一个虚拟目录test,在其中放一个word文档a.doc,然后打开word, 文件->打开->输入word文档的访问url(http://localhost/test/a.doc),

修改一下文档内容,保存一下,发生了什么? 文档被保存到服务器上了.

在IE中,可以通过js创建Word.Application,来打开,修改服务器上的文档.

wApp = new ActiveXObject("Word.Application.11");

wApp.Visible = true ;

wApp.Documents.Open( url );

if( trackRevisions ){ //可以实现痕迹保留呢

wApp.ActiveDocument.TrackRevisions = true ;

wApp.ActiveDocument.ShowRevisions = false ;

}else

{

wApp.ActiveDocument.TrackRevisions = false ;

wApp.ActiveDocument.ShowRevisions = false ;

}

wApp.ActiveDocument.Application.UserName= Global_CurrentUserName;

另外,安装office时,会同时按装一个ActiveX组件:Sharepoint.OpenDocuments,可么用此组件来激活word,编辑服务器上的文档:
var __OpenDocuments = null ;

function Document_Edit2( url )

{

if( __OpenDocuments == null )

{

try{

__OpenDocuments = new ActiveXObject("SharePoint.OpenDocuments.3"); //for office 2007

}catch(e){}

if( __OpenDocuments == null || typeof(__OpenDocuments) == "#ff0000" )

{

try{

__OpenDocuments = new ActiveXObject("SharePoint.OpenDocuments.2"); //for office 2003

}catch(e){}

}

if( __OpenDocuments == null || typeof(__OpenDocuments) == "undefined" )

{

alert( "请安装Word(2003或更高版本)" );

return ;

}

}

// openDocObj.ViewDocument("http://www.abc.com/documents/sample.doc");, "Word.Document"

//openDocObj.CreateNewDocument("http://www.abc.com/documents/sampleTemplate.dot", "http://www.abc.com/documents/");

var result = __OpenDocuments.EditDocument( url , "Word.Document" );

if( result == false )

{

alert( "无法打开文档." );

}

}

可以看到,基于IIS的webdav支持,可以非常简单的实现office文档的在线编辑, 但有一个问题:这样,文档是存放在文件系统上,我们很多系统中,

文档是存放在数据库中的,这样一来,如何实现呢???

I tried a lot and found the solution. It will be in the next article .

讲述了如何通过iis的webdav支持实现客户端的office直接编辑服务器上的文件,

本篇将讲解如何实现客户端的office直接编辑数据库中的二进制形式保存的office文件。

实现的关键:模拟IIS,自己实现一个webdav的服务器端。

首先,我们简单了解一下webdav:

webdav,中文可以翻译为网络分布式协作协议,它解决了http协议中一个问题:http无法实现版本和单访问控制。

什么是单访问控制呢?假设我们有一个页面编辑某条数据,这个页面可以同时被多个用户使用,那么最终的数据是最后一个用户提交的数据,

而其他用户是不知道的.我们的99%的web程序都存在此问题,当然通过编码可以解决,但http协议本身并没有提供对这种情形的支持。

webdav协议在标准的http协议的基础上,扩展了以下请求动作(verb):

PUT:用于客户端推送二进制文件。(好像http有这个verb)

LOCK:用户锁定一个资源,保证资源的单访问

UNLOCK:解锁一个资源

OPTIONS:获取服务器可以支持的请求类型

DELETE:删除服务器文件

PROPFIND:查询文件属性

其他动作: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH

要详细地了解webdav,大家可以google一下,或访问http://en.wikipedia.org/wiki/WebDAV

笔者在实现这个解决方案的时候,是采用fiddler,debug IE的http请求,才搞懂了IIS本身的实现机制,为了形象化,可以看一下webdav请求相应的

数据:

发起一个OPTIONS请求

OPTIONS /PMDemo/Test/待办事务.doc HTTP/1.1

User-Agent: Fiddler

Host: localhost

响应如下:

HTTP/1.1 200 OK

Date: Wed, 27 Dec 2006 11:34:03 GMT

Server: Microsoft-IIS/6.0

MicrosoftOfficeWebServer: 5.0_Pub

X-Powered-By: ASP.NET

MS-Author-Via: DAV

Content-Length: 0

Accept-Ranges: bytes

DASL: <DAV:sql>

DAV: 1, 2

Public: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH

Allow: OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK

Cache-Control: private

搞清楚了这些,下面我们的任务就是如何在asp.net中实现一个wevdav服务器.

显然,这要求我们需要在底层截获http请求,幸运的是asp.net中支持这种技术:HttpHandler.它可以让我们自己的代码来处理http请求.

首先,我们在web.config中做如下配置:

<httpHandlers>

<remove verb="*" path="*"/>

<add verb="GET,PUT,UNLOCK,LOCK,OPTIONS" path="*.doc,*.xml" type="Webdav.WebdavProtocolHandler,Webdav"/>

</httpHandlers>
通过这个配置,使我们的WebdavProtocolHandler可以来处理webdav请求.

WebdavProtocolHandler类是一个标准的httphandler,实现了IHttpHandler接口,它按照客户端的请求类型,返回符合wevdav协议的数据.

WebdavProtocolHandler类需要按照不同的webdav请求动作,做不同的处理,那么怎么来实现这个类呢?

这里就要用到一个设计模式:命令模式.

首先定义一个接口:

public interface IVerbHandler

{

void Process( HttpContext context );

}

实现对Options请求的处理:

class OptionsHandler : IVerbHandler

{

#region IVerbHandler 成员

public void Process(System.Web.HttpContext context)

{

context.Response.AppendHeader("DASL", "<DAV:sql>");

context.Response.AppendHeader("DAV", "1, 2");

context.Response.AppendHeader("Public", "OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH");

context.Response.AppendHeader("Allow", "OPTIONS, TRACE, GET, HEAD, DELETE, PUT, COPY, MOVE, PROPFIND, PROPPATCH, SEARCH, LOCK, UNLOCK");

}

#endregion

}
webdav的请求verb多达15个以上,大多数情况下,我们并不需要一个完整的webdav支持,故我们只要对其中的几个进行实现即可。

实现对LOCK的支持:

class LockHandler : IVerbHandler

{

#region IVerbHandler 成员

public void Process(System.Web.HttpContext context)

{

context.Response.ContentType = "text/xml";

string token = Guid.NewGuid().ToString() + ":" + DateTime.Now.Ticks.ToString() ;

context.Response.AppendHeader("Lock-Token", "<opaquelocktoken:" + token + ">");

string xml = @"<?xml version=""1.0""?>

<a:prop xmlns:a=""DAV:""><a:lockdiscovery>

<a:activelock><a:locktype><a:write/></a:locktype>

<a:lockscope><a:exclusive/></a:lockscope><owner xmlns=""DAV:"">Administrator</owner><a:locktoken>

<a:href>opaquelocktoken:{0}</a:href></a:locktoken>

<a:depth>0</a:depth><a:timeout>Second-180</a:timeout></a:activelock></a:lockdiscovery>

</a:prop>";

context.Response.Write( String.Format( xml , token ) );

context.Response.End();

}

#endregion

}

注意这篇文章的主题:实现在线编辑。并没有版本控制等其他内容,大家仔细看以上的代码,服务器并没有真正实现"锁定",只是假装告诉客户端,你要的资源已经给你锁定了,你可以放心的编辑了。当然,有兴趣的朋友可以实现真正的锁定,无非可以通过给数据做一个状态字段来实现。但注意,要考虑一些复杂的情况,如自动解锁(用户打开一个文档,然后关机了,文档岂不永远锁定了?)等等。

接着,我们实现UNLOCK,同样是假的:

class UnLockHandler : IVerbHandler

{

#region IVerbHandler 成员

public void Process(System.Web.HttpContext context)

{

}

#endregion

}

下面,我们将实现两个最重要的请求动作的处理:Get和Put, office请求打开一个服务器上的文件时,采用get请求,office保存一个文件到服务器上时,发送put请求。

首先,我们要考虑一种数据项标识的传递策略,即:客户端发起访问数据库的office文件行,那么如何确认数据行的主键?

有两种策略:

1)通过不同的文件名 , 如,请求http://localhost/weboffice/1.doc 这个请求主键 为1的文件。

2)通过文件路径, 如,请求http://localhost/weboffice/1/文件名.doc 这个请求主键为1的文件。

我们将采用策略2。

再返回到我们对web.config做的配置:

<add verb="GET,PUT,UNLOCK,LOCK,OPTIONS" path="*.doc,*.xml" type="Webdav.WebdavProtocolHandler,Webdav"/>

这个配置允许WebdavProtocolHandler处理所有对doc和xml的请求处理,为什么要允许xml呢,因为office2003之后,支持xml格式,可以直接在

数据库重以xml的格式存放office文件。

接着,我们要确认我们的数据存储结构,即,office文件在数据库中时如何存放的。

我们有一个附件表:Document

CREATE TABLE [dbo].[Document] (

[DocumentId] [int] IDENTITY (1, 1) NOT NULL ,

[Name] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,

[Description] [text] COLLATE Chinese_PRC_CI_AS NULL ,

[CreateTime] [datetime] NULL ,

[Size] [int] NULL ,

[CreatorId] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,

[CreatorName] [char] (10) COLLATE Chinese_PRC_CI_AS NULL ,

[CreateYear] [int] NULL ,

[ContentType] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,

[DeptId] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,

[DeptName] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,

[Content] [image] NULL ,

[ModifyTime] [datetime] NULL ,

[OwnerType] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,

[TemplateAble] [bit] NULL

) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO
设计一个文裆实体:

[Serializable]

public class Document

{

public Document()

{ }

static public Document FromPostFile(System.Web.HttpPostedFile file , User user )

{

Document doc = new Document(file);

doc.CreateTime = DateTime.Now;

doc.CreatorId = user.Id;

doc.CreatorName = user.Name;

doc.DeptId = user.OrgId;

doc.DeptName = user.OrgName;

return doc;

}

public Document(System.Web.HttpPostedFile file)

{

string[] strs = file.FileName.Split( '\\' );

this.Name = strs[strs.Length - 1];

Size = file.ContentLength;

//读取文件的数据

this.Content = new byte[Size];

Stream fileDataStream = file.InputStream;

fileDataStream.Read( this.Content , 0, Size );

ContentType = file.ContentType;

}

private int _DocumentId;

/// <summary>

/// 任务名

/// </summary>

private string _Name;

/// <summary>

/// 任务描述

/// </summary>

private string _Description;

/// <summary>

/// 报表创建时间

/// </summary>

private DateTime _CreateTime = DateTime.Now ;

private int _Size = 0 ;

private byte[] _Data;

/// <summary>

/// 创建人Id

/// </summary>

private string _CreatorId;

/// <summary>

/// 创建人名

/// </summary>

private string _CreatorName;

private int _CreateYear;

private string _ContentType;

/// <summary>

/// 部门ID(便于统计)

/// </summary>

private string _DeptId;

/// <summary>

/// 部门名

/// </summary>

private string _DeptName;

// Property DocumentId

public int DocumentId

{

get

{

return _DocumentId;

}

set

{

this._DocumentId = value;

}

}

// Property Name

public string Name

{

get

{

return _Name;

}

set

{

this._Name = value;

}

}

// Property Description

public string Description

{

get

{

return _Description;

}

set

{

this._Description = value;

}

}

// Property CreateTime

public DateTime CreateTime

{

get

{

return _CreateTime;

}

set

{

this._CreateTime = value;

}

}

private DateTime _ModifyTime = DateTime.Now;

public DateTime ModifyTime

{

get

{

return _ModifyTime;

}

set

{

this._ModifyTime = value;

}

}

// Property Size

public int Size

{

get

{

return _Size;

}

set

{

this._Size = value;

}

}

// Property Data

public byte[] Content

{

get

{

return _Data;

}

set

{

this._Data = value;

}

}

// Property CreatorId

public string CreatorId

{

get

{

return _CreatorId;

}

set

{

this._CreatorId = value;

}

}

// Property CreatorName

public string CreatorName

{

get

{

return _CreatorName;

}

set

{

this._CreatorName = value;

}

}

// Property CreateYear

public int CreateYear

{

get

{

return _CreateYear;

}

set

{

this._CreateYear = value;

}

}

// Property ContentType

//application/msword

//text/plain

public string ContentType

{

get

{

return _ContentType;

}

set

{

this._ContentType = value;

}

}

// Property DeptId

public string DeptId

{

get

{

return _DeptId;

}

set

{

if (this._DeptId != value)

this._DeptId = value;

}

}

// Property DeptName

public string DeptName

{

get

{

return _DeptName;

}

set

{

this._DeptName = value;

}

}

private string _Type;

public string OwnerType

{

get

{

return _Type;

}

set

{

this._Type = value;

}

}

private bool _TemplateAble;

/// <summary>

/// 是否可以作为模版

/// </summary>

public bool Templateable

{

get

{

return _TemplateAble;

}

set

{

this._TemplateAble = value;

}

}

public override string ToString()

{

return Encoding.UTF8.GetString(this.Content);

}

public static Document FromString(string s, User user)

{

Document doc = new Document();

doc.CreateTime = DateTime.Now;

doc.CreatorId = user.Id;

doc.CreatorName = user.Name;

doc.DeptId = user.OrgId;

doc.DeptName = user.OrgName;

doc.Content = Encoding.UTF8.GetBytes(s);

doc.Size = doc.Content.Length;

doc.ContentType = "text/plain";

return doc;

}

public static string ByteToString( byte[] bytes )

{

return Encoding.UTF8.GetString( bytes );

}

public static byte[] StringToByte(string s)

{

return Encoding.UTF8.GetBytes(s);

}

public string GetExtendName()

{

string[] arr = this.Name.Split( '.' );

if (arr.Length < 1) return "";

else return arr[ arr.Length - 1 ];

}

}

考虑到数据操作逻辑的可变性,不同的项目里面附件表设计的不同,这里引入一个数据操作接口:

public interface IWebdavDocumentHandler

{

Document GetDocument(int id);//获取文档数据

void ModifyDocContent(int docId, byte[] data);//修改文档内容

}

具体的实现这里就不写了。

好了,我们的数据访问逻辑已经有了,那么首先看get动作处理的实现:

class GetHandler : IVerbHandler

{

#region IVerbHandler 成员

public void Process(System.Web.HttpContext context)

{

int id = WebdavProtocolHandler.GetDocumentId( context ); //获取到主键

IWebdavDocumentHandler docSvr = new DefaultWebdavDocumentHandler(); //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式

Document doc = docSvr.GetDocument(id);

if (doc == null)

{

context.Response.Write("文档不存在!");

return;

}

context.Response.Clear();

context.Response.ContentType = doc.ContentType;

//下载文件名限制32字符 16 汉字

int maxlength = 15;

string fileName = doc.Name; //att.FileName ;

if (fileName.Length > maxlength)

{

fileName = "-" + fileName.Substring(fileName.Length - maxlength, maxlength);

}

fileName = HttpUtility.UrlEncode(fileName, System.Text.Encoding.UTF8); //必须编码,不然文件名会出现乱码

context.Response.AppendHeader("Content-Disposition", "attachment;filename=" + fileName + "");

if (doc.Content != null && doc.Content.Length > 0)

context.Response.BinaryWrite(doc.Content);

context.Response.End();

}

#endregion

}
很简单吧,跟我们普通实现文档下载的代码一样。

put动作的实现:

class PutHandler : IVerbHandler

{

#region IVerbHandler 成员

public void Process(System.Web.HttpContext context)

{

int docId = WebdavProtocolHandler.GetDocumentId(context);

Document doc = GetDocFromInput(context.Request);

doc.DocumentId = docId;

IWebdavDocumentHandler docSvr = new DefaultWebdavDocumentHandler(); //修改此处代码,实现不同的数据操作逻辑,可引入工厂模式

docSvr.ModifyDocContent( doc.DocumentId , doc.Content );

}

private Document GetDocFromInput(System.Web.HttpRequest request )

{

Document doc = new Document();

//读取文件的数据

doc.Content = new byte[ request.ContentLength ];

doc.Size = request.ContentLength;

Stream fileDataStream = request.InputStream;

fileDataStream.Read( doc.Content , 0, doc.Size );

doc.ContentType = request.ContentType;

return doc;

}

#endregion

}

OK,主要的动作都实现了,下面,我们需要WebdavProtocolHandler将各命令处理对象整合到一起:

public class WebdavProtocolHandler : IHttpHandler

{

public static int GetDocumentId( HttpContext context )//按照前面确定的主键策略返回主键

{

string url = context.Request.Url.ToString();

string[] arr = url.Split( '/' );

string id = arr[arr.Length - 2];

return Convert.ToInt32( id );

}

public void ProcessRequest(HttpContext context)

{

HttpRequest Request = context.Request;

context.Response.AppendHeader("OpenWebDavServer", "1.0");

string verb = Request.HttpMethod;

//Log.Write(verb);

IVerbHandler vh = GetVerbHandler( verb );

if( vh == null )

return ;

vh.Process(context);

}

private IVerbHandler GetVerbHandler(string verb)

{

switch (verb)

{

case "LOCK" :

return new LockHandler();

case "UNLOCK":

return new UnLockHandler();

case "GET":

return new GetHandler();

case "PUT":

return new PutHandler();

case "OPTIONS":

return new OptionsHandler();

default :

return null;

}

}

public bool IsReusable

{

get { return false; }

}

}

到这里呢,已经基本上算game over了,基于以上代码设计,可以完全实现office文档的在线编辑。若要通过链接直接打开编辑,可以

采用Office文档在线编辑的实现之一的Document_Edit2函数触发office编辑。

哦,IIS还需要做一点小配置:

1)将.doc , .xml 加入到站点虚拟目录的isapi映射, 不要选中 "确认文件是否存在",动作要选全部动作,

2)禁用IIS本身的Webdav扩展,

3)删除虚拟目录HTTP头中的自定义HTTP头: MicrosoftOfficeWebServer,如果有的话。

this is the real end.

Tag标签: office,在线编辑
posted on 2007-07-15 10:38 jianyi 阅读(3174) 评论(42) 编辑 收藏 所属分类: Office开发企业级开发



FeedBack:

#1楼 2007-07-15 13:05 风云
不错,好文章!

回复 引用 查看

#2楼 2007-07-15 13:22 Join miao
好长。。。

回复 引用 查看

#3楼 2007-07-15 13:23 Join miao
辛苦了

回复 引用 查看

#4楼 [楼主] 2007-07-15 13:58 jianyi0115
@Join miao

哈哈,代码都贴上来了,所以长了。

回复 引用 查看

#5楼 221.219.62.* 2007-07-15 17:30 天天基金网 [未注册用户]
hao

回复 引用 查看

#6楼 2007-07-15 19:20 过江
好.

回复 引用 查看

#7楼 220.207.104.* 2007-07-15 19:56 goo [未注册用户]
您好!

我按照你提供的方法编写代码,并编译通过,但当word打开一个文件时,是只读状态,无法编辑后再保存到服务器。

测试环境为:Asp.net 2.0,windows xp sp2, office 2003

回复 引用 查看

#8楼 [楼主] 2007-07-15 22:04 jianyi0115
@goo

删除虚拟目录HTTP头中的自定义HTTP头: MicrosoftOfficeWebServer

这个做了吗?

若有MicrosoftOfficeWebServer头的话,office会认为是IIS默认的webdav

实现,会发送额外的命令,而我们对这些命令是没有处理的。

回复 引用 查看

#9楼 125.74.85.* 2007-07-15 23:05 laoda [未注册用户]
微软有个ocx控件可以嵌入office,有网友扩展了它让他可以二进制形式保存回服务器,因为是控件所以可以嵌入到网页中,可控制性比较强,因该比这样做好点。

回复 引用 查看

#10楼 125.74.85.* 2007-07-15 23:10 laoda [未注册用户]
该ocx该控件微软提供了c++源代码,所以可方便的进行扩展.

回复 引用 查看

#11楼 [楼主] 2007-07-15 23:17 jianyi0115
@laoda

知道有这样一个ocx,我的方案是纯.net实现,直接激活客户端的office,微软的

sharepoint的文档管理也是这么处理的。

回复 引用 查看

#12楼 2007-07-16 07:32 布尔
原来一直在用金格的控件,可以考虑自己来实现了,呵呵,不过又想到一个问题,比如要做签章是不是还得用控件呢

回复 引用 查看

#13楼 58.214.240.* 2007-07-16 08:35 goo [未注册用户]
@jianyi0115

没有HTTP头: MicrosoftOfficeWebServer

我的调用代码为:

<html xmlns="http://www.w3.org/1999/xhtml%22 >

<head runat="server">

<title>无标题页</title>

<script language="javascript" type="text/javascript">

var __OpenDocuments = null ;

function documentEdit( url )

{

if( __OpenDocuments == null ) {

try{

__OpenDocuments = new ActiveXObject("SharePoint.OpenDocuments.3"); //for office 2007

}

catch(e){}

if( __OpenDocuments == null || typeof(__OpenDocuments) == "undefined" ) {

try{

__OpenDocuments = new ActiveXObject("SharePoint.OpenDocuments.2"); //for office 2003

}

catch(e){}

}

if( __OpenDocuments == null || typeof(__OpenDocuments) == "undefined" ) {

alert( "请安装Word(2003或更高版本)" );

return ;

}

}

var result = __OpenDocuments.EditDocument( url , "Word.Document" );

if( result == false ) {

alert( "无法打开文档." );

}

}

</script>

</head>

<body>

<form id="form1" runat="server">

<div>

<input type="button" value="打开文档" onclick="documentEdit('http://localhost/Office/1/1.doc');return false;" />

</div>

</form>

</body>

</html>
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: