好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

细谈在C#中读写Excel系列文章之四

细谈在C#中读写Excel系列文章之四

作为这一系列文章的最后一篇,向大家介绍下如何在Silverlight中解压和创建Excel OpenXml ZIP压缩包。由于Silverlight对本地客户端文件系统访问的安全级别要求比较高,不太容易像Windows应用程序那样可以随意地读写目录和文件,我们不得不考虑使用一些其它的办法。如使用Silverlight的OOB(Out of Browser)模式,可以允许Silverlight程序读写本地的部分目录和文件,下面这篇文章介绍了如何在Silverlight OOB模式下调用COM组件来操作Excel。

http://www.codeproject.com/Articles/83996/Silverlight-4-Interoperability-with-Excel-using-th

  又回到使用COM组件的方式了,不过这个不是我们要讨论的问题,了解一下也无妨。先说说为什么我们要如此变态地在Silverlight中使用Excel OpenXML。

  这其实是一个需求,用户的数据存放在SharePoint List中,想通过一个程序从List读取数据然后填充到一个Excel模板中提供下载。这个需求本身其实非常简单,关键是用户要求程序不能在客户端安装,服务器端不能有Custom Code,这两点要求几乎扼杀了我们所有可以使用的方法,第一不能使用诸如Windows Form或WPF application,第二不能创建SharePoint Feature或Webpart,当然就更别提创建单独的ASP.NET应用程序了,客户根本就没有提供空间去部署站点。另外Silverlight的OOB模式也不允许,因为OOB模式也是要在本地安装的,尽管它不同于传统意义上的Windows程序安装。这样,我们只有一条路可走,那就是创建Silverlight应用程序然后通过SharePoint的Silverlight Webpart部署到页面上,在Silverlight中直接调用Excel Services把从List中读取到的数据填充到Excel中。那跟Excel OpenXML有什么关系?我们不是已经往Excel里写入数据了吗?对,没错!如果你只是单纯往Excel模板中写入数据根本不需要再做任何操作,可是修改Excel文件的样式呢?

  还记得在上一篇文章中的那个图吗?如果单元格中没有部分内容加粗,而只是单纯的换行或空格,我们可以直接通过Excel的公式或表达式来实现。

=CONCATENATE( "    Short-term investments (including securities loaned  " ,  CHAR ( 10 ),  "      of $9,999 and $8,888  " )

= "    Short-term investments (including securities loaned)  "  &  CHAR ( 10 )& "      of   "  & TEXT( "9999  " ,  "  $#,##0  " ) &  "   and   "  & TEXT( "8888  " ,  "  $#,##0  " )

  上面两行代码分别使用了Excel中的Concatenate()函数和&连接符来填充单元格内容,其中CHAR(10)表示的就是回车符。但是我们无法在Excel中通过函数设置字符串的样式,Excel所有内置的函数都不能修改样式。或许我们可以尝试通过VBA来修改样式呢?在单独的Excel文件中这个办法是可行的,我们只需要在Workbook的Open事件或SheetChange事件中写入VBA代码,当事件被调用的时候样式会被自动修改。不过Excel Services不支持带有VBA或宏的Excel文件,当你尝试通过Excel Services读取一个带有VBA代码或宏的Excel文件时会抛出异常。所以,尝试通过VBA来修改样式是行不通的,尤其是那些特殊的样式,如上图中单元格内数字加粗和上标等。

  因此,我们会考虑通过Excel OpenXML方式将已经填充好数据的Excel文件进行样式修改。按照前面几篇文章的介绍,修改Excel内容需要首先将其解压到一个临时目录,然后修改临时目录中相应的XML文件,最后再重新打包成一个Excel文件。可是在Silverlight中不太容易操作本地文件系统,因此我们只能考虑在文件Stream中完成操作了。

  我们需要一个能支持在Silverlight工程中操作ZIP文件的类库,之前的那个开源类库 http://www.icsharpcode.net/OpenSource/SharpZipLib/ 是使用较早的.NET Framework编写的,有许多类型和对象在Silverlight Framework中找不到无法编译通过。幸好这里我找到有人将其修改成Silverlight版本了,非常感谢!互联网是强大的。

http://web-snippets.blogspot.com/2008/03/unpacking-zip-files-in-silverlight-2.html

  我这里也提供一个下载吧,以免原作者的空间打不开。 SLSharpZipLib_Solution.zip

  这里还有一个关于ShareZipLib示例的WiKi站点,可以研究下这个类库都能干些什么。

http://wiki.sharpdevelop.net/SharpZipLib_Updating.ashx#Updating_a_zip_file_in_memory_1

  来看一个实际应用的例子。

 ///   <summary> 
 ///   Format exported Excel file with a stream.
  ///   </summary> 
 ///   <param name="zipfileStream">  A stream of Excel zip file.  </param> 
 ///   <returns>  Return a MemoryStream of updated Excel zip file.  </returns> 
 public   Stream FormatExcelWithStream(Stream zipfileStream)
{
      //   copy the current stream to a new stream 
    MemoryStream msZip =  new   MemoryStream();
      long  pos =  0  ;
    pos  =  zipfileStream.Position;
    zipfileStream.CopyTo(msZip);
    zipfileStream.Position  =  pos;

    XElement eleSheet  =  null  ;

      //   load sharedStrings xml document for updating 
    XElement eleSharedStrings =  null  ;
      using  (Stream mainStream = FindEntryFromZipStream(zipfileStream,  "  xl/sharedStrings.xml  "  ))
    {
          if  (mainStream ==  null  )
        {
              return   msZip;
        }
        eleSharedStrings  =  XElement.Load(mainStream);
    }

      //   distinct sheet xml document for searching 
    IEnumerable<ExcelFormattingSetting> noduplicates = ExcelFormattingSettings.Distinct( new   ExcelFormattingSettingCompare());
      foreach  (ExcelFormattingSetting sheet  in   noduplicates)
    {
        zipfileStream.Position  =  0  ;
          using  (Stream stream =  FindEntryFromZipStream(zipfileStream, sheet.SheetName))
        {
              if  (stream !=  null  )
            {
                eleSheet  =  XElement.Load(stream);
                  //   update sharedStrings.xml document 
                 UpdateSharedStringsXMLDoc(sheet.SheetName, eleSharedStrings, eleSheet);
            }
        }
    }

      //   update to stream 
    MemoryStream msEntry =  new   MemoryStream();
    eleSharedStrings.Save(msEntry);
      //   The zipStream is expected to contain the complete zipfile to be updated 
    ZipFile zipFile =  new   ZipFile(msZip);

    zipFile.BeginUpdate();

      //   To use the entryStream as a file to be added to the zip,
      //   we need to put it into an implementation of IStaticDataSource. 
    CustomStaticDataSource sds =  new   CustomStaticDataSource();
    sds.SetStream(msEntry);

      //   If an entry of the same name already exists, it will be overwritten; otherwise added. 
    zipFile.Add(sds,  "  xl/sharedStrings.xml  "  );

      //   Both CommitUpdate and Close must be called. 
     zipFile.CommitUpdate();
      //   Set this so that Close does not close the memorystream 
    zipFile.IsStreamOwner =  false  ;
    zipFile.Close();

      return   msZip;
}


  ///   <summary> 
 ///   Find a specific stream with the entry name of the Excel zip file package.
  ///   </summary> 
 ///   <param name="inputStream">  The Excel zip file stream.  </param> 
 ///   <param name="entryName">  Entry name in the Excel zip file package.  </param> 
 ///   <returns>  Return the sepcific stream.  </returns> 
 private  Stream FindEntryFromZipStream(Stream inputStream,  string   entryName)
{
    ZipInputStream zipStream  =  new   ZipInputStream(inputStream);
    ZipEntry zippedFile  =  zipStream.GetNextEntry();

      //  Do until no more zipped files left 
     while  (zippedFile !=  null  )
    {
          byte [] buffer =  new   byte [ 2048  ];
          int   bytesRead;

        MemoryStream memoryStream  =  new   MemoryStream();
          //   read through the compressed data 
         while  ((bytesRead = zipStream.Read(buffer,  0 , buffer.Length)) !=  0  )
        {
            memoryStream.Write(buffer,   0  , bytesRead);
        }
        memoryStream.Position  =  0  ;

          if   (zippedFile.Name.Equals(entryName))
        {
              return   memoryStream;
        }

        zippedFile  =  zipStream.GetNextEntry();
    }

      return   null  ;
}


  ///   <summary> 
 ///   Update formatted strings to the sharedStrings.xml document in Excel zip file.
  ///   </summary> 
 ///   <param name="sheetName"></param> 
 ///   <param name="navSharedStrings"></param> 
 ///   <param name="navSheet"></param> 
 private   void  UpdateSharedStringsXMLDoc( string   sheetName, XElement eleSharedStrings, XElement eleSheet)
{
    XNamespace nsSharedStrings  =  eleSharedStrings.GetDefaultNamespace();
    XNamespace nsSheet  =  eleSheet.GetDefaultNamespace();
      int   i;
      string   sContent;

      //   update each formatting settings to the sharedStrings xml document 
     foreach  (ExcelFormattingSetting setting  in  ExcelFormattingSettings.Where(s =>  s.SheetName.Equals(sheetName)))
    {
          //   find out which si element need to update from the sheet xml document. 
         var  siIndex = eleSheet.Element(nsSheet +  "  sheetData  "  )
                                .Descendants(nsSheet  +  "  c  "  )
                                .Where(d  => d.Attribute( "  r  " ).Value ==  setting.ExcelPositionString).FirstOrDefault();
          if  (siIndex !=  null  )
        {
              if  ( int .TryParse(siIndex.Value,  out   i))
            {
                  var  siEntry = eleSharedStrings.Elements(nsSharedStrings +  "  si  "  ).ElementAt(i);
                  if  (siEntry !=  null  )
                {
                      var  child = siEntry.Element(nsSharedStrings +  "  t  "  );
                      if  (child !=  null  )
                    {
                        setting.OriginalText  =  child.Value;
                        sContent  =  setting.ProcessFormatting(setting.processFormatting);
                          //   note, cannot set empty content to the new XElement. 
                         if  (!sContent.Equals( string  .Empty))
                        {
                            XElement newElement  = XElement.Parse( "  <si xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">  "  + setting.ProcessFormatting(setting.processFormatting) +  "  </si>  "  );
                            siEntry.ReplaceWith(newElement);
                        }
                    }
                }
            }
        }
    }
} 

  不能在原Stream上进行操作。首先将文件的Stream Copy出来一份。函数FindEntryFromZipStream()通过entryName来返回ZIP压缩包中指定的部分,返回类型为Stream。将找到的Stream交给UpdateSharedStringsXMLDoc()方法进行修改,该方法会逐一读取所有工作表XML文件,找到需要修改的部分的si节点的序号(该序号存放在工作表XML文件的sheetData->c->r节点中),然后修改sharedStrings.xml文件的内容。注意sharedStrings.xml文件加载的XElement对象只有一个,传入该对象到UpdateSharedStringsXMLDoc()方法进行修改,之后将其存入到一个MemoryStream对象中。接下来的代码便是将该MemoryStream重新加到ZIP包里,使用了类库提供的方法,注意如果指定的entryName在原ZIP包中存在则会直接替换,否则就添加。CustomStaticDataSource类是自定义的一个类,按照要求该类必须继承自IStaticDataSource接口。

 public   class   CustomStaticDataSource : IStaticDataSource
{
      private   Stream _stream;
      //   Implement method from IStaticDataSource 
     public   Stream GetSource()
    {
          return   _stream;
    }

      //   Call this to provide the memorystream 
     public   void   SetStream(Stream inputStream)
    {
        _stream  =  inputStream;
        _stream.Position  =  0  ;
    }
} 

  所有的操作都在一个Stream里完成,不需要临时目录存放解压后的文件。这里是上面整个类的完整下载, ExcelFormattingAdjustor.zip

   例子中将所有需要修改样式的单元定义成常量,然后在自定义类ExcelFormattingSetting中存储设置样式的一些参数,如原内容、工作表名称、样式替换字符串,以及如何修改样式的委托等。将所有ExcelFormattingSetting类的实例存放到一个集合里,遍历该集合对所有的设置项进行修改。你可以在ExcelFormattingSetting类的ProcessFormatting()方法中定义如何替换这些样式字符串,如果遇到需要特殊处理的情况,就在该类的实例中定义一个匿名函数,在匿名函数中进行处理。

  更多的应用还在不断尝试中,如果能够提供一个功能丰富且成熟的类库,我们完全可以脱离COM组件来操作Excel,包括创建一个全新的Excel文件、读取数据生成报表、导出数据到Excel文件并自定义样式等等。但所有这一切都应该归功于OpenXML,它使得Office文件从一个自封闭的环境中解脱出来了,基于XML结构的文件是开放的,因此我们做的所有工作其实就是在操作XML,如此简单!不是吗?

 

分类:  .NET

标签:  C# ,  Excel ,  Office

作者: Leo_wl

    

出处: http://www.cnblogs.com/Leo_wl/

    

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权信息

查看更多关于细谈在C#中读写Excel系列文章之四的详细内容...

  阅读:39次