标签:title net svg link control 知识库 自定义 over using
Office文档的web应用叫WOPI Host或者WOPI Server。
把查看编辑操作Office文档的web应用叫WOPI Client或者叫WOPI applications。
所以,Office Web Apps充当的就是WOPI Client的角色。
SharePoint,Exchange,自己开发的文档管理系统充当的就是WOPI Host的角色。
再看一下,浏览器,server,client三者的请求顺序及关系

其实网上有关office web app的整合已经有相关的文章了,典型的是如何整合Office Web Apps至自己开发的系统(一) 和如何整合Office
Web Apps至自己开发的系统(二),微软官网也有相应的demo。
这里在简单描述一下原理吧:office web apps(owas)扮演者一个客服端,它会访问我们asp.NET 站点的文件然后呈现出来。而我们常用的API主要有如下3个:
GET api/wopi/files/{name}?access_token={access_token}
GET api/wopi/files/{name}/contents?access_token={access_token}
POST api/wopi/files/{name}/contents?access_token={access_token}
至于每个API做什么 这里就不多说,第一个是owas 检查文件,传递的信息是json数据格式,第二个是owas获取文件流,第三个是owas post的文件流(保存修改文件)。首先我们来看看第一个API的实现:
- [Route("files/{name}/")]
- public CheckFileInfo GetFileInfo(string name, string access_token)
- {
- Validate(name, access_token);
- var fileInfo = _fileHelper.GetFileInfo(name);
- bool updateEnabled = false;
- if (bool.TryParse(WebConfigurationManager.AppSettings["updateEnabled"].ToString(), out updateEnabled))
- {
- fileInfo.SupportsUpdate = updateEnabled;
- fileInfo.UserCanWrite = updateEnabled;
- fileInfo.SupportsLocks = updateEnabled;
- }
- return fileInfo;
- }
[Route("files/{name}/")]
public CheckFileInfo GetFileInfo(string name, string access_token)
{
Validate(name, access_token);
var fileInfo = _fileHelper.GetFileInfo(name);
bool updateEnabled = false;
if (bool.TryParse(WebConfigurationManager.AppSettings["updateEnabled"].ToString(), out updateEnabled))
{
fileInfo.SupportsUpdate = updateEnabled;
fileInfo.UserCanWrite = updateEnabled;
fileInfo.SupportsLocks = updateEnabled;
}
return fileInfo;
}
这里的 Validate(name, access_token) 方法主要是验证请求的文件名name与参数access_token是否一致,主要是验证是否是非法访问,返回一个CheckFileInfo对象,CheckFileInfo的定义如下:
- public class CheckFileInfo
- {
- public CheckFileInfo()
- {
- this.SupportsUpdate = false;
- this.UserCanWrite = false;
- }
- public string BaseFileName { get; set; }
- public string OwnerId { get; set; }
- public long Size { get; set; }
- public string SHA256 { get; set; }
- public string Version { get; set; }
- public bool SupportsUpdate { get; set; }
- public bool UserCanWrite { get; set; }
- public bool SupportsLocks { get; set; }
- }
public class CheckFileInfo
{
public CheckFileInfo()
{
this.SupportsUpdate = false;
this.UserCanWrite = false;
}
public string BaseFileName { get; set; }
public string OwnerId { get; set; }
public long Size { get; set; } //in bytes
public string SHA256 { get; set; } //SHA256: A 256 bit SHA-2-encoded [FIPS180-2] hash of the file contents
public string Version { get; set; } //changes when file changes.
public bool SupportsUpdate { get; set; }
public bool UserCanWrite { get; set; }
public bool SupportsLocks { get; set; }
}
现在在来看看第二个api的实现,主要返回对应文件的数据流:
- [Route("files/{name}/contents")]
- public HttpResponseMessage Get(string name, string access_token)
- {
- try
- {
- Validate(name, access_token);
- var file = HostingEnvironment.MapPath("~/App_Data/" + name);
- var responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
- var stream = new FileStream(file, FileMode.Open, FileAccess.Read);
- responseMessage.Content = new StreamContent(stream);
- responseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
- return responseMessage;
- }
- catch (Exception ex)
- {
- var errorResponseMessage = new HttpResponseMessage(HttpStatusCode.InternalServerError);
- var stream = new MemoryStream(UTF8Encoding.Default.GetBytes(ex.Message ?? ""));
- errorResponseMessage.Content = new StreamContent(stream);
- return errorResponseMessage;
- }
- }
[Route("files/{name}/contents")]
public HttpResponseMessage Get(string name, string access_token)
{
try
{
Validate(name, access_token);
var file = HostingEnvironment.MapPath("~/App_Data/" + name);
var responseMessage = new HttpResponseMessage(HttpStatusCode.OK);
var stream = new FileStream(file, FileMode.Open, FileAccess.Read);
responseMessage.Content = new StreamContent(stream);
responseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
return responseMessage;
}
catch (Exception ex)
{
var errorResponseMessage = new HttpResponseMessage(HttpStatusCode.InternalServerError);
var stream = new MemoryStream(UTF8Encoding.Default.GetBytes(ex.Message ?? ""));
errorResponseMessage.Content = new StreamContent(stream);
return errorResponseMessage;
}
}
而第三个api是将返回的数据流保存到物理文件:
- [Route("files/{name}/contents")]
- public async void Post(string name, [FromUri] string access_token)
- {
- var body = await Request.Content.ReadAsByteArrayAsync();
- var appData = HostingEnvironment.MapPath("~/App_Data/");
- var fileExt = name.Substring(name.LastIndexOf(‘.‘) + 1);
- var outFile = Path.Combine(appData,name);
- File.WriteAllBytes(outFile, body);
- }
[Route("files/{name}/contents")]
public async void Post(string name, [FromUri] string access_token)
{
var body = await Request.Content.ReadAsByteArrayAsync();
var appData = HostingEnvironment.MapPath("~/App_Data/");
var fileExt = name.Substring(name.LastIndexOf(‘.‘) + 1);
var outFile = Path.Combine(appData,name);
File.WriteAllBytes(outFile, body);
}
现在我们再来看看如何请求owas,也就是对应的url是怎么产生的。例如我的owas server是owas.contoso.com,那么我们在配置好owas后就可以访问http://owas.contoso.com/hosting/discovery 如图:

这里我们以excel为例 大家看到上面有view、edit、mobileview三个action,这里的app是一个excel,我们知道我们物理文件的后缀找到相应的app,在根据我们系统的配置采用edit还是view action,如果是pdf 我们只能采用对应的view,如果请求是mobile发起的话, 那么我们只能用mobileview。 找到相应的action后我们就获取到对应的urlsrc属性,这里我们实际需要的url地址是 http://owas.contoso.com/x/_layouts/xlviewerinternal.aspx这个东东。那么获取这个url的代码如下:
- public class LinkController : ApiController
- {
-
-
-
-
-
-
- public Link GetLink([FromUri] FileRequest fileRequest)
- {
- if (ModelState.IsValid)
- {
- var xml = WebConfigurationManager.AppSettings["appDiscoveryXml"];
- var wopiServer = WebConfigurationManager.AppSettings["appWopiServer"];
- bool updateEnabled = false;
- bool.TryParse(WebConfigurationManager.AppSettings["updateEnabled"], out updateEnabled);
- WopiAppHelper wopiHelper = new WopiAppHelper(HostingEnvironment.MapPath(xml), updateEnabled);
-
- var result = wopiHelper.GetDocumentLink(wopiServer + fileRequest.name);
-
- var rv = new Link
- {
- Url = result
- };
- return rv;
- }
-
- throw new ApplicationException("Invalid ModelState");
- }
- }
-
- public class WopiAppHelper
- {
- string _discoveryFile;
- bool _updateEnabled = false;
- WopiHost.wopidiscovery _wopiDiscovery;
-
- public WopiAppHelper(string discoveryXml)
- {
- _discoveryFile = discoveryXml;
-
- using (StreamReader file = new StreamReader(discoveryXml))
- {
- XmlSerializer reader = new XmlSerializer(typeof(WopiHost.wopidiscovery));
- var wopiDiscovery = reader.Deserialize(file) as WopiHost.wopidiscovery;
- _wopiDiscovery = wopiDiscovery;
- }
- }
-
- public WopiAppHelper(string discoveryXml, bool updateEnabled)
- : this(discoveryXml)
- {
- _updateEnabled = updateEnabled;
- }
-
- public WopiHost.wopidiscoveryNetzoneApp GetZone(string AppName)
- {
- var rv = _wopiDiscovery.netzone.app.Where(c => c.name == AppName).FirstOrDefault();
- return rv;
- }
-
- public string GetDocumentLink(string wopiHostandFile)
- {
- var fileName = wopiHostandFile.Substring(wopiHostandFile.LastIndexOf(‘/‘) + 1);
- var accessToken = GetToken(fileName);
- var fileExt = fileName.Substring(fileName.LastIndexOf(‘.‘) + 1);
- var netzoneApp = _wopiDiscovery.netzone.app.AsEnumerable()
- .Where(c => c.action.Where(d => d.ext == fileExt).Count() > 0);
-
- var appName = netzoneApp.FirstOrDefault();
-
- if (null == appName) throw new ArgumentException("invalid file extension " + fileExt);
-
- var rv = GetDocumentLink(appName.name, fileExt, wopiHostandFile, accessToken);
-
- return rv;
- }
-
- string GetToken(string fileName)
- {
- KeyGen keyGen = new KeyGen();
- var rv = keyGen.GetHash(fileName);
-
- return HttpUtility.UrlEncode(rv);
- }
-
- const string s_WopiHostFormat = "{0}?WOPISrc={1}&access_token={2}";
-
- const string s_WopiHostFormatPdf = "{0}?PdfMode=1&WOPISrc={1}&access_token={2}";
-
- public string GetDocumentLink(string appName, string fileExtension, string wopiHostAndFile, string accessToken)
- {
- var wopiHostUrlsafe = HttpUtility.UrlEncode(wopiHostAndFile.Replace(" ", "%20"));
- var appStuff = _wopiDiscovery.netzone.app.Where(c => c.name == appName).FirstOrDefault();
-
- if (null == appStuff)
- throw new ApplicationException("Can‘t locate App: " + appName);
-
- var action = _updateEnabled ? "edit" : "view";
- if (appName.Equals("WordPdf"))
- {
- action = "view";
- }
- if (HttpContext.Current.Request.Browser.IsMobileDevice)
- {
- action = "mobileView";
- }
- var appAction = appStuff.action.Where(c => c.ext == fileExtension && c.name == action).FirstOrDefault();
-
- if (null == appAction)
- throw new ApplicationException("Can‘t locate UrlSrc for : " + appName);
-
- var endPoint = appAction.urlsrc.IndexOf(‘?‘);
- var endAction = appAction.urlsrc.Substring(0, endPoint);
-
- string fullPath = null;
-
- if (fileExtension.Contains("pdf"))
- {
- fullPath = string.Format( s_WopiHostFormatPdf, endAction, wopiHostUrlsafe, accessToken);
- }
- else
- {
- fullPath = string.Format(s_WopiHostFormat, endAction, wopiHostUrlsafe, accessToken);
- }
-
- return fullPath;
- }
- }

public class LinkController : ApiController
{
/// <summary>
/// Provides a link that can be used to Open a document in the relative viewer
/// from the Office Web Apps server
/// </summary>
/// <param name="fileRequest">indicates the request type</param>
/// <returns>A link usable for HREF</returns>
public Link GetLink([FromUri] FileRequest fileRequest)
{
if (ModelState.IsValid)
{
var xml = WebConfigurationManager.AppSettings["appDiscoveryXml"];
var wopiServer = WebConfigurationManager.AppSettings["appWopiServer"];
bool updateEnabled = false;
bool.TryParse(WebConfigurationManager.AppSettings["updateEnabled"], out updateEnabled);
WopiAppHelper wopiHelper = new WopiAppHelper(HostingEnvironment.MapPath(xml), updateEnabled);
var result = wopiHelper.GetDocumentLink(wopiServer + fileRequest.name);
var rv = new Link
{
Url = result
};
return rv;
}
throw new ApplicationException("Invalid ModelState");
}
}
public class WopiAppHelper
{
string _discoveryFile;
bool _updateEnabled = false;
WopiHost.wopidiscovery _wopiDiscovery;
public WopiAppHelper(string discoveryXml)
{
_discoveryFile = discoveryXml;
using (StreamReader file = new StreamReader(discoveryXml))
{
XmlSerializer reader = new XmlSerializer(typeof(WopiHost.wopidiscovery));
var wopiDiscovery = reader.Deserialize(file) as WopiHost.wopidiscovery;
_wopiDiscovery = wopiDiscovery;
}
}
public WopiAppHelper(string discoveryXml, bool updateEnabled)
: this(discoveryXml)
{
_updateEnabled = updateEnabled;
}
public WopiHost.wopidiscoveryNetzoneApp GetZone(string AppName)
{
var rv = _wopiDiscovery.netzone.app.Where(c => c.name == AppName).FirstOrDefault();
return rv;
}
public string GetDocumentLink(string wopiHostandFile)
{
var fileName = wopiHostandFile.Substring(wopiHostandFile.LastIndexOf(‘/‘) + 1);
var accessToken = GetToken(fileName);
var fileExt = fileName.Substring(fileName.LastIndexOf(‘.‘) + 1);
var netzoneApp = _wopiDiscovery.netzone.app.AsEnumerable()
.Where(c => c.action.Where(d => d.ext == fileExt).Count() > 0);
var appName = netzoneApp.FirstOrDefault();
if (null == appName) throw new ArgumentException("invalid file extension " + fileExt);
var rv = GetDocumentLink(appName.name, fileExt, wopiHostandFile, accessToken);
return rv;
}
string GetToken(string fileName)
{
KeyGen keyGen = new KeyGen();
var rv = keyGen.GetHash(fileName);
return HttpUtility.UrlEncode(rv);
}
const string s_WopiHostFormat = "{0}?WOPISrc={1}&access_token={2}";
//HACK:
const string s_WopiHostFormatPdf = "{0}?PdfMode=1&WOPISrc={1}&access_token={2}";
public string GetDocumentLink(string appName, string fileExtension, string wopiHostAndFile, string accessToken)
{
var wopiHostUrlsafe = HttpUtility.UrlEncode(wopiHostAndFile.Replace(" ", "%20"));
var appStuff = _wopiDiscovery.netzone.app.Where(c => c.name == appName).FirstOrDefault();
if (null == appStuff)
throw new ApplicationException("Can‘t locate App: " + appName);
var action = _updateEnabled ? "edit" : "view";
if (appName.Equals("WordPdf"))
{
action = "view";
}
if (HttpContext.Current.Request.Browser.IsMobileDevice)
{
action = "mobileView";
}
var appAction = appStuff.action.Where(c => c.ext == fileExtension && c.name == action).FirstOrDefault();
if (null == appAction)
throw new ApplicationException("Can‘t locate UrlSrc for : " + appName);
var endPoint = appAction.urlsrc.IndexOf(‘?‘);
var endAction = appAction.urlsrc.Substring(0, endPoint);
string fullPath = null;
////HACK: for PDF now just append WordPdf option...
if (fileExtension.Contains("pdf"))
{
fullPath = string.Format( s_WopiHostFormatPdf, endAction, wopiHostUrlsafe, accessToken);
}
else
{
fullPath = string.Format(s_WopiHostFormat, endAction, wopiHostUrlsafe, accessToken);
}
return fullPath;
}
}
相应的配置如下:

appDiscoveryXml 是我们owas(http://owas.contoso.com/hosting/discovery)产生的数据文件,appWopiServer 表示我们的owas将要访问interface地址。updateEnabled主要是表示owas是否可以修改我们的文档,如果是true 我们上面的action 采用edit,为false采用view。appHmacKey只是数据加密的一个key。生成的url如图:

注意这里的配置是updateEnabled=true 表示owas是可以编辑文件的,如图:

当我们点击在浏览器编辑 结果如图:

修改后可以直接保存:


点击确认后就可以直接保存。 pptx的编辑模式如下:

这里的docx文件的编辑模式一直都在报错搞了很久也没搞定,错误信息如下,如果大家知道还请指导指导:

pdf是没有编辑模式的,现在再来看看excel的只读模式(view)如下:

这里的菜单中并不包含“在浏览器中编辑”,其中第15行是我刚才修改的新数据。docx和pptx的只读模式就不贴图了,在mobile的运行结果如下(我这里是用Android手机访问我的站点,由于是通过wifi来访问自己的电脑上的站点,这里需要把计算机的全名改为IP地址)。

注意上面的url是192.168.1.25XXX,这里的ip是owas.contoso.com的IP。这里总结一下的测试结果如下:
|
view |
edit |
mobileview |
remark |
word |
通过 |
未通过 |
通过 |
在http和https协议下view都通过,edit view没有通过,mobileview只测试了http协议 |
excel |
通过 |
通过 |
通过 |
在http和https协议下view和edit都通过,mobileview只测试了http协议 |
ppt |
通过 |
通过 |
通过 |
在http和https协议下view和edit都通过,mobileview只测试了http协议 |
pdf |
通过 |
不存在edit action |
未通过 |
view在http协议下通过,在https在协议下未通过,mobileview 未通过 |
这里我把问题的重心放在word的edit上面,对于pdf 在owas采用https以及在mobile上不能访问的原因未未做调查。知道这些问题的革命前辈还请不吝赐教。源码下载地址:http://download.csdn.Net/detail/dz45693/7215395
https://code.msdn.microsoft.com/office/Building-an-Office-Web-f98650d6
自定义开发的系统整合 office Web apps
标签:title net svg link control 知识库 自定义 over using
原文地址:http://blog.csdn.net/duanchuanttao/article/details/54602948