Excel开发过程中有时候会遇到各种奇怪的问题,下面就列出一些本人在开发中遇到的一些比较典型的问题,并给出了解决方法,希望对大家有所帮助。

一 插件调试不了以及错误导致崩溃的问题


在开发机器上,有时可能会装有多个版本的.NET运行时,有时候也可能装有多个版本的Visual Studio,本人的开发机器上就安装了3个版本的Visual Studio,在最新的Visual Studio 2013出来之后,就迫不及待的把工程文件迁移到2013上来了,但是在设置了启动程序之后,调试的时候,断点根本不能命中,也调试不进去。即使使用Attached to process方式也不行。

Can‘t Debug SharedAddin Programe

该问题是由于在安装有多个.NET版本的机器上,由于某些原因,会导致Excel不知道为Com Add-in加载哪个版本的Framework导致的。注:这里仅针对是用.NET技术开发Shared Add-in插件的情况,由于VSTO 中的安装部署文件.vsto中注明了程序集的版本,所以不存在该问题。

解决方法是新建一个名为EXCEL.EXE.config的配置文件,放到和EXCEL.EXE同级的目录下,配置如下,在startup节点下新建supprotedRuntime节点,指定运行时的版本号,即可解决该问题:

Excel Config file

 

在Shared Add-in插件开发的初期,在测试机器上测试的时候,有时会由于代码错误,导致Excel直接崩溃掉的问题。在开发机器上,我们只需要将在该配置文件中,将runtime节点下的legacyUnhandledExceptionPolicy的Enable属性设置为true,这样,Excel在出现问题的时候就会弹出提示框,有助于帮助查找错误出现的原因。

这其实也是相较于VSTO,Shared Add-in应用程序的缺点之一,VSTO的一个插件是加载到各自的App Domain中的,出现问题不会影响其他的插件。

二 若干Excel方法调用会导致出错


由于需要兼容不同版本的Excel,但是随着版本的变化,Excel暴漏出来的API方法的参数也会发生变化。有时候采用直接调用的方式在遇到版本不同时会抛出异常,这种COM类型的异常一般很难处理和恢复。在有些时候,通过采用“晚绑定”的方式,采用反射调用方法就可以解决这一问题。

以条件着色功能为例。在使用插件获取并输出数据的时候,通常需要根据条件对单元格进行着色,比如,输出股票的涨跌和涨跌幅的时候,人们一般习惯将涨的标注为红色,跌的标注为绿色。Excel中对Range进行单元格条件可以使用FormatCoondition对象,该对象有如下方法,以添加涨跌幅的条件着色为例,代码如下。

object missing = Type.Missing;
FormatCondition conditionFall = (FormatCondition)tmpRange.FormatConditions.Add(XlFormatConditionType.xlCellValue, XlFormatConditionOperator.xlLess, "=0", missing);
conditionFall.Font.ColorIndex = 10;//Green

FormatCondition conditionRise = (FormatCondition)tmpRange.FormatConditions.Add(XlFormatConditionType.xlCellValue, XlFormatConditionOperator.xlGreater, "=0", missing);
conditionRise.Font.ColorIndex = 3;//Green

该方法在Excel07及以上版本中没有问题,但是在03下就会抛出异常。由于个版本的API中,参数个数不一样。所以采用反射的方法,将参数以数组的方式传入即可解决该问题。

下面这个方法封装了FoormationCoondition的Add方法。

/// <summary>
///兼容Excel 2003-Excel2010的代码条件着色,如果直接使用FormatConditions.Add方法,则不能兼容
///详情可以参考:http://social.msdn.microsoft.com/Forums/en-US/vsto/thread/8a91d154-f766-427a-963c-16dfa39e154a
///使用方法如: tempRange.AddConditionValue(XlFormatConditionOperator.xlLess, "=0");
/// </summary>
/// <param name="R">待进行条件着色的Range区域</param>
/// <param name="ConditionOperator">找色条件,如,大于,小于</param>
/// <param name="Formula">具体的值</param>
/// <returns></returns>
public static FormatCondition AddConditionValue(Range R, XlFormatConditionOperator ConditionOperator, string Formula)
{
    return (FormatCondition)R.FormatConditions.GetType().InvokeMember("Add", BindingFlags.InvokeMethod, null, (object)R.FormatConditions, new object[] { XlFormatConditionType.xlCellValue, ConditionOperator, Formula });
}

使用该方法,上面的代码改写为:

FormatCondition conditionFall = AddConditionValue(tempRange, XlFormatConditionOperator.xlLess, "=0");
conditionFall.Font.ColorIndex = 10;//Green

FormatCondition conditionRise = AddConditionValue(tempRange, XlFormatConditionOperator.xlGreater, "=0");
conditionRise.Font.ColorIndex = 3;//Red

Excel中遇到同一方法由于不同API版本参数不同导致出现问题,都可以采取上面的采用反射方法调用来解决问题。

三 Excel自定义函数中包含.xla或者.xll文件路径的问题


一般地通常的插件都会开发相应的自定义函数 (UDF),除了使用C#类库编写自定义函数,大部分都是将自定义函数写在Excel的.xla或者xll文件中,有时候也会使用自定义函数编写一些标准的模板供用户调用。当这些模板中包含有自定义函数,当用户将该模板保存到其他位置之后,再次打开,这些单元格中的函数就会包含.xla或者.xll的路径。比如"C:\Program Files\Installation folder\MyUDFs.xla!MyUDF"等,原因是Excel在内部会包含自定义函数文件的完整路径。

这会导致一些问题,比如说,改自定义函数不能够正确的执行,不能求值等等。

就解决方法就是在vba中,在SheetOpen事件中,将所有包含函数的路径进行重新定向到系统指标的自定义函数类库的目录下即可。VBA代码如下,主要的方法为_Workbook.ChangeLink 方法,VBA的代码如下。将该部分代码写到vba类库中即可。

Private Sub ExcelApp_WorkbookOpen(ByVal Wb As Workbook)
     ReLink Wb
End Sub

Public Sub ReLink(ByVal oBook As Workbook)
    If IsEmpty(oBook.LinkSources(xlLinkTypeExcelLinks)) Then Exit Sub
    
    Dim lk As Variant
    
    For Each lk In oBook.LinkSources(xlLinkTypeExcelLinks)
        If lk Like "*" & ThisWorkbook.Name And lk <> ThisWorkbook.FullName Then
                oBook.ChangeLink lk, ThisWorkbook.FullName, xlLinkTypeExcelLinks
        End If
    Next lk
End Sub

四 自定义函数加载的问题


还是UDF加载的问题,如果是采用.xla或者xll承载自定义函数的话,在插件启动的时候需要加载改文件到Excel中,这样Excel才能够使用这里面的自定义函数,并在Excel 2007及以上版本中会给出函数的智能提示。

一般地,在Shared Add-ins中加载自定义函数我们可以在OnConnection方法中,调用如下方法:

MSExcel.AddIn excelFunc = null;
try
{
    excelFunc = applicationObject.AddIns.Add(“your .xla file path“, true);
}
catch (System.IO.IOException exception)
{
    LogHelper.Error("IOException:未发现xla文件在所设置路径", exception);
}
catch (Exception exception)
{
    LogHelper.Error(exception);
}

在大多数情况下,如果用户直接打开Excel应用程序,插件加载就会执行OnConnect方法,就会执行加载.xla函数的逻辑。

但是,更多的情况是,用户会直接双击一个之前保存过的,包含有我们的.xla文件中的自定义函数的excel文件,通过这种方式打开excel,插件的加载.xla在某些情况下回出现加载不了的情况。出现该问题的原因在于,我们再加载.xla文件的时候,必须要确保当前Excel中有一个空白的Sheet页处于打开状态。在用户通过双击现有的excel文件的时候,在文档打开之前,并没有可用的Sheet页处于打开状态,这是我们的插件加载.xla文件的时候就会出现加载不上的问题。

解决方法就是在OnConnect中调用RegisterOpenEvent方法注册WorkbookActivate事件,在该事件回调方法中去注册.xla文件。这样在文件被打开的时候就可以加载.xla或者.xll函数了,在加载完函数之后,我们需要注销该事件以防止重复加载。如下

private void RegisterOpenEvent()
{
    applicationObject.WorkbookActivate += new MSExcel.AppEvents_WorkbookActivateEventHandler(applicationObject_WorkbookActivate);
}

/// <summary>
/// This active evevt was designed to be trigged only once to load xla file
/// </summary>
/// <param name="Wb"></param>
void applicationObject_WorkbookActivate(MSExcel.Workbook Wb)
{
    MSExcel.AddIn excelFunc = null;
    try
    {
        excelFunc = applicationObject.AddIns.Add(RuntimeHelper.MapRootPath(EmOfficeCore.Helper.PathHelper.Instance.EMFuncPath), true);
    }
    catch (System.IO.IOException exception)
    {
        LogHelper.Error("IOException:未发现xla文件在所设置路径", exception);
    }
    catch (Exception exception)
    {
        LogHelper.Error(exception);
    }
    if (excelFunc.Installed == true)
    {
        applicationObject.WorkbookActivate -= new MSExcel.AppEvents_WorkbookActivateEventHandler(applicationObject_WorkbookActivate);
    }
    else
    {
        applicationObject.WorkbookActivate -= new MSExcel.AppEvents_WorkbookActivateEventHandler(applicationObject_WorkbookActivate);
        excelFunc.Installed = true;
    }
}

这样就能解决加载不上的问题。

五 资源释放不掉的问题


在使用Shared Add-ins开发插件的时候,通常如果资源释放处理不好,会存在关闭Excel后,任务管理器中的Excel.exe这个进程没有关闭。这是由于我们在.NET中引用了非托管代码 ,但是有没有手动释放,导致资源没有被回收导致的。该问题在StackOverflow上有很详细的讨论,按照msdn上的解决方法。我们只需要在OnDisConnection 方法中对资源进行释放即可:

/// <summary>
///     Implements the OnDisconnection method of the IDTExtensibility2 interface.
///     Receives notification that the Add-in is being unloaded.
/// </summary>
/// <param term='disconnectMode'>
///      Describes how the Add-in is being unloaded.
/// </param>
/// <param term='custom'>
///      Array of parameters that are host application specific.
/// </param>
/// <seealso class='IDTExtensibility2' />
public void OnDisconnection(Extensibility.ext_DisconnectMode disconnectMode, ref System.Array custom)
{
    try
    {
        GC.Collect();
        Marshal.FinalReleaseComObject(applicationObject);
        GC.Collect();
    }
    catch (Exception ex)
    {
        LogHelper.Error(ex);
    }
}

六 正确的创建和使用Excel实例


在应用程序开发中,通常我们需要在Winform中导出到Excel或者对Excel文件进行读写,如果您选择使用Excel API的话(当然更好的方式是直接使用OpenXML或者相似的技术直接生成Excel文件),通常的做法是直接创建一个Excel实例:

Microsoft.Office.Interop.Excel.Application m_objExcel = new Microsoft.Office.Interop.Excel.Application();

但是这种方式过于简单粗暴, 他相当于直接在后台创建一个Excel的实例。当您导出多次的时候会创建多个这样的应用程序,并且在关闭程序的时候很可能导致这些资源不能释放,最明显的是您会发现在资源管理器中会出现很多个Excel的进程没有被关闭。

还有一个场景就是插件的自动升级。 通常我们的插件需要升级,如果发现新的版本,则回去下载,然后对现有的dll进行覆盖。通常,改升级程序时一个独立于excel的插件,覆盖之前通常需要关闭当前打开的Excel才能进行覆盖。在检测到升级并下载完新的升级包时,需要提示用户关闭Excel,点击确定的时候,一般的做法是强制终止任务管理器中的Excel进行,再执行覆盖,这样做的问题在于:Excel会认为插件导致了Excel的意外关闭,在下次打开的时候,会直接提示改插件存在严重问题,是否禁用。通常一般的用户会去点击禁用。这样显然不好。

打开或者获取Office产品实例的方法有很9种,比较友好的方式是,如果当前已经有Excel的实例在运行,则不要去new新的实例,而是通过一些方法直接从现有的实例中去获取和创建,比如可以通过Marshal.GetActiveObject方法传入想要获取实例的ProgID即可。对于上面的方法,更友好的版本是:

public Microsoft.Office.Interop.Excel.Application StartExcel()
{
    Microsoft.Office.Interop.Excel.Application instance = null;

    try
    {
        instance = (Microsoft.Office.Interop.Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
    }
    catch (System.Runtime.InteropServices.COMException ex)
    {
        instance = new Microsoft.Office.Interop.Excel.Application();
    }

    return instance;
}

该方法首先尝试使用GetActiveObject从当前活动的Excel实例获取实例,如果没有活动的Excel,则再新建一个Excel实例。这样就避免了肯能的资源浪费。

在升级程序中,我们可以直接调用该方法返回的实例,然后调用它的Quit方法就可以。

七 Excel 2003上安装部署不上的问题


使用Shared Com Add-in创建的程序在Office 2003上有可能安装不上, 该问题在前一篇有关Excel安装部署中 提到过,需要安装两个针对Office 2003的补丁。这篇文章Deploying a Shared COM add-in for Office 2003 (Visual Studio 2008 SP1) [and how to work around a known issue which causes the add-in to fail to load if KB908002 is not installed.] 有详细介绍该补丁如何在部署时打包及安装,这里就不在赘述了。