首页 > 其他分享 >浅谈Excel开发:六 Excel 异步自定义函数

浅谈Excel开发:六 Excel 异步自定义函数

时间:2023-07-26 21:34:12浏览次数:43  
标签:RTD 请求 自定义 request 函数 Excel UDF 浅谈

上文介绍了Excel中的自定义函数(UDF ),它极大地扩展了Excel插件的功能,使得我们可以将业务逻辑以Excel函数的形式表示,并可以根据这些细粒度的自定义函数,构建各种复杂的分析报表。

普通的UDF自定义函数的基本执行逻辑是,Excel接受用户输入的函数表达式,然后通过UDF函数的处理逻辑进行处理,在处理过程中,Excel 的UI界面会一直等待函数体执行完成之后更新单元格数据。和大多数同步应用一样,同步的UDF函数会阻塞Excel UI线程,并且不方便动态扩展计算能力,在处理逻辑比较复杂、进行耗时的计算逻辑的时候,会造成较差的用户体验。所以我们需要开发异步的UDF函数。

一 问题的提出


通常,当用户在Excel中输入自定义函数的时候,我们希望实现以下表现:
  1. 开启另外一根线程或者在线程池中处理函数计算逻辑(不同于Excel UI线程)。

  2. 同时立即返回“Calculating”,“Fetching”或“Loading”等值提示用户正在计算。

  3. 待计算完成之后,通知Excel重新计算该单元格。

  4. 返回真正的计算结果。

    以上步骤中,难点在于第三步,一般的函数在第二步返回值之后,整个过程就结束了。由于函数计算不在Excel主线程中,而刷新Excel单元格重新计算时要在Excel 主线程中进行,并且要刷新哪一个单元格还需要指明,而且在异常处理上比较麻烦。

    在Excel 2010中,提供了直接编写异步UDF的能力,不过只有在使用C/C++开发的XLL Addin中才能使用,并且不向下兼容。要编写适用于大多数版本的Excel(03及以上版本)必须使用Excel已经提供的编程机制。我们可以利用在上文介绍的Excel RTD函数来解决以上问题,Excel RTD机制是Excel 2002就引入的,因此向上兼容性良好。使用Excel RTD来实现异步UDF函数的基本原理是,当用户在单元格输入函数时:

  5. 将函数以RTD函数请求,记录下TopicID,以及请求的表达式及参数,并返回“Calculating”提示用户正在处理

  6. 开辟另外线程处理该TopicID对应的Excel请求,并计算出结果,保存。

  7. 调用UpdateNotify方式,请求Excel重新计算单元,在重新计算的单元格中,根据TopicID,以及计算的结果值,返回给Excel

  8. Excel根据返回的计算结果值,刷新单元格。

根据以上分析,为Excel编写异步UDF函数的可用的解决方案为:

  • 在Excel 2010及以上版本,可以利用C/C++调用Excel API,开发XLL类型插件实现。

  • 在Excel2002 及以上版本,可以利用Excel RTD函数,来实现

  • 通过第三方类库,如Excel-DNA中引入的ReactiveExtension来实现。

    考虑到Excel个版本最大限度的兼容性以及本系列文章主要关注.NET 下的Excel插件开发,所以将重点介绍第2种方法。

二 如何使用RTD实现异步UDF


[正如前文所述](https://www.yycoding.xyz/post/2013/8/29/excel-realtime-data-function-introduce),RTD函数主要是用来作为实时数据更新来使用的,但是我们可以利用RTD函数的这种特殊的Push-Pull机制来开发异步的UDF函数。异步UDF函数的主要实现框架如下图:

Excel Async RTD

基于RTD的异步UDF函数的实现大致流程如上:

  1. 用户输入UDF函数,在VBA函数层面上,将UDF函数的函数名,及参数作为RTD函数的参数数组,在内部发起RTD函数调用。
  2. 在RTD函数的ConnectData方法中,将该次请求Excel分配的TopicID,以及请求的参数,包括函数名,参数拼接成Formula的样式,作为Key保存到一个全局的Dictionary中,Value为一个实体类,该实体类记录了此次的TopicID,以及该Key的请求返回的值Result(Object类型的)以及其他一些字段。
  3. ConnectData返回Loading,或者其他提示符,在单元格中显示,提示正在后台运算。
  4. 同时,另开一个线程对全局的请求Dictionary里面的请求进行处理,这个过程涉及到同步的处理,因为有可能在处理请求的时候,又有新的请求添加进来。
  5. 取出key,解析方法名及参数,使用反射或者一些高效率的方法获取该请求对应的值,存储到key对应的Value的实体的Result字段中。
  6. 同时调用RTD的UpdateNotify方法,通知Excel有数据要更新。
  7. Excel调用RTD的RefreshData方法,然后将全局请求的Dictionary中有结果值的数据项及TopicID,放到二维数组中,并返回。
  8. Excel单元格显示为该请求实际返回的结果值。

三、实现


下面就按照以上的逻辑编写一个简单的异步UDF的实现。

1. 实现UDF集中处理实体类

在最开始之前,我们要定义一个基于Dictionary自定义一个RequestDictionary实体类,用于存储所有的请求以及请求结果,该类继承自Dictionary<String, Request>并实现了IDictionary<string, Request>方法。
internal sealed class RequestsDictionary : Dictionary<String, Request>, IDictionary<string, Request>
{

}
在该类中,我们定义了一个枚举型的状态变量,分为Idle,Processing,Waiting三个状态,并定义了一个一步集合AsyncList,用来保存与处理的请求集合。
private AsyncList<Request> processRequestCollection;
private int state;
public int State { get { return state; }
然后,我们的实例内部,对pocessRequestCollection进行处理,处理完成之后,回调 RequestProcessCompletedHandle事件。
public event EventHandler<ProcessCompletedArgs> RequestProcessCompletedHandle;
我们重写了该类的Add方法,当在RTD中往该实体类中添加请求时:
public new void Add(string key, Request value)
{
    if (!base.ContainsKey(key))
    {
        base[key] = value;
        if (Interlocked.Exchange(ref this.state, (int)ProcessState.Waiting) == (int)ProcessState.Idle)
        {
            this.processRequestCollection.Add(value);
            OnRequestAdded(null);
        }
        else
        {
            Interlocked.Exchange(ref this.state, (int)ProcessState.Waiting);
            this.processRequestCollection.Add(value);
        }
    }
}
如果当前状态属于空闲,则将请求加载到待处理集合中,并将状态改为等待,并发起请求处理命令。后面如果有请求过来了,直接添加到待处理集合中。而不再次发起处理请求,只有当前状态变为idle了才会再次对集合中的待处理命令进行处理。
private void OnRequestAdded(Request[] formulas)
{
    RequestProcessor processor = new RequestProcessor(realProcessMethod);
    string[] refeshedFormulas;
    processor.BeginInvoke(formulas, out refeshedFormulas, new AsyncCallback(RequestProcessCompletedCallback), processor);
}
在OnRequestAdd方法中,我们使用异步代理,发起了一个真正的对请求处理的方法realProcessMethod方法。该方法的实现如下:
bool realProcessMethod(Request[] request, out string[] refeshedIds)
{
    if (request == null)
    {
        Thread.Sleep(10);
        refeshedIds = null;
        return true;
    }
    else
    {
        try
        {
            List<string> requestList = new List<string>(request.Length);
            refeshedIds = new string[request.Length];
            for (int i = 0; i < request.Length; i++)
            {
                requestList.Add(request[i].Formula);
                refeshedIds[i] = request[i].Formula;
            }
            //GetValues method get the process the real request;
            Dictionary<string, object> dictionaryWithValues = GetValues(requestList);
            Request requestInstance;
            foreach (KeyValuePair<string, Object> item in dictionaryWithValues)
            {
                requestInstance = this[item.Key];
                requestInstance.Result = item.Value;
                requestInstance.InCache = true;
            }
            return true;
        }
        catch
        {
            refeshedIds = null;
            return false;
        }
    }
}
该方法中,我们所有请求的ID放到refreshId中,当计算完成之后,我们就可以通知Excel仅刷新这些refreshId的数据了。方法中,我们真正处理请求的是在GetValue方法中,该方法接受所有请求的表达式,然后返回以该表达式为key,结果值为value的Dictionary对象。这里为了演示,我们让每个方法请求睡眠1秒,以模拟耗时操作,并直接返回函数中的参数作为返回值。在真实的场景中,我们通常将requestList拿出来解析,解析出函数名词,函数参数,然后利用反射,或者是约定的处理方式对这些请求进行计算并求得结果,就拿前面我们看过的天气预报的例子来说,如果用户在Excel中输入 =YY_Weather_Condition(“Shanghai”, “2013-09-28”),那么我们的requestList中就有一个名为 =YY_Weather_Condition(“Shanghai”, “2013-09-28”)的字符串,解析第一部分,就知道是函数名称,括号里面的就是参数, 然后动态的去调用相应的方法,或者直接对该逻辑进行判断处理,一般滴,我们的函数都是约定好的,所以可以直接采用if的方式进行判断然后进行处理。
bool realProcessMethod(Request[] request, out string[] refeshedIds)
{
    if (request == null)
    {
        Thread.Sleep(10);
        refeshedIds = null;
        return true;
    }
    else
    {
        try
        {
            List<string> requestList = new List<string>(request.Length);
            refeshedIds = new string[request.Length];
            for (int i = 0; i < request.Length; i++)
            {
                requestList.Add(request[i].Formula);
                refeshedIds[i] = request[i].Formula;
            }
            //GetValues method get the process the real request;
            Dictionary<string, object> dictionaryWithValues = GetValues(requestList);
            Request requestInstance;
            foreach (KeyValuePair<string, Object> item in dictionaryWithValues)
            {
                requestInstance = this[item.Key];
                requestInstance.Result = item.Value;
                requestInstance.InCache = true;
            }
            return true;
        }
        catch
        {
            refeshedIds = null;
            return false;
        }
    }
}
本例中,我们对之前的天气函数进行改造,将其改造为异步的UDF,所以这里的GetValue方法我们去获取天气情况:
//Fake method 
//获取天气情况
private Dictionary<string, object> GetValues(List<string> requestList)
{
    Dictionary<string, object> results = new Dictionary<string, object>();
    for (int i = 0; i < requestList.Count; i++)
    {
        string request = requestList[i];
        string functionName = request.Substring(0, request.LastIndexOf('('));
        string[] parameters = request.Substring(request.LastIndexOf('(') + 1, request.Length - request.LastIndexOf('(') - 2).Split(',');
        Object returnValue = "Paramters Error!";
        //Thread.Sleep(1000);
        if (functionName.Equals("YY_Weather_Condition") ||
            functionName.Equals("YY_Weather_Temperature") ||
            functionName.Equals("YY_Weather_WindSpeed"))
        {

            if (parameters.Length == 2)
            {
                string city = parameters[0];
                string date = parameters[1];
                returnValue = GetWeather(functionName, city, date);
            }
        }
        else //return parameters
        {
            returnValue = request.Substring(request.LastIndexOf(','));
        }
        results.Add(requestList[i], returnValue);
    }
    return results;
}

private Object GetWeather(string functionName, string city, string date)
{
    Object result = null;
    Weather weather = new Weather(city, Weather.TemperatureUnits.Celcius);
    switch (functionName)
    {
        case "YY_Weather_Condition":
            result = weather.Condition.Text;
            break;
        case "YY_Weather_Temperature":
            result = weather.Condition.Temperature;
            break;
        case "YY_Weather_WindSpeed":
            result = weather.Wind.Speed;
            break;
    }
    return result;
}
该方法执行完成之后,会调用RequestProcessCompletedCallback这个回调方法, 在该回调方法中:
void RequestProcessCompletedCallback(IAsyncResult asyncResult)
{
    RequestProcessor processor = (RequestProcessor)asyncResult.AsyncState;
    string[] refeshedFormulas;
    bool isCompleted = processor.EndInvoke(out refeshedFormulas, asyncResult);
    if (isCompleted)
    {
        if (Interlocked.Exchange(ref this.state, (int)ProcessState.Idle) == (int)ProcessState.Waiting)
        {
            Interlocked.Exchange(ref this.state, (int)ProcessState.Processing);
            OnRequestAdded(this.processRequestCollection.PopupRest());
        }
        else  // which means no entry added yet
        {
            this.processRequestCollection.Clear();
        }
        OnRequestCompleted(new ProcessCompletedArgs(refeshedFormulas));
    }
}

private void OnRequestCompleted(ProcessCompletedArgs args)
{
    if (this.RequestProcessCompletedHandle != null)
        this.RequestProcessCompletedHandle(this, args);
}
我们查看当前的状态, 如果当前处于等待状态,则将状态改为空闲表示我们的已经处理完了。 然后将后面添加的请求继续进行处理,否则,表示所有的请求都处理完了,清空本地的待处理的请求队列。最后调用回调方法,传入待刷新的单元格formulas通知Excel刷新相应的单元格。

该类是异步UDF 的核心处理逻辑, 因为要涉及到不停的处理所有添加的请求,对请求集合的处理,一边在添加,一边在读取处理,为了减轻加锁对系统性能造成的影响,方法中大量使用了无锁编程的技巧,这一点对于提升性能有很大帮助,详细使用方法在后面讲到性能优化的时候我会再写一篇文章详细讲解,当然了,在CLR Via C# 4th中对该技术有详细的介绍。为了实现该类能够对请求进行不停的处理,将之前的使用timer控件的定时检查,然后处理逻辑,转换成了效率更高的三状态处理机制,极大的提高了性能和响应性。

写完了我们的请求处理类之后,我们需要在RTD 函数中对接收的请求进行统一的处理。和[第四篇](https://www.yycoding.xyz/post/2013/8/19/excel-user-define-function-introduce)文章介绍如何创建RTD函数一样,我们新建一个RTD的类,然后实现IRtdServer接口及相应的方法。

2. 对RTD 函数进行改造

第一部分写完处理函数之后,接下来需要编写RTD 函数了,将普通的函数请求添加到之前的处理类中来。如何创建RTD 函数这里就不多讲了,您可以直接看前面的文章,这里不再赘述。

[Guid("BB007B7A-2647-4A09-A230-0CD8A2BDD489")]
[ProgId("YYAsyncRTD.Func")]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class YYAsyncRTD : IRtdServer
{
    private SynchronizationContext ExcelContext;
    private static RequestsDictionary Requests = new RequestsDictionary();
    private static IRTDUpdateEvent xlRTDUpdater;

    public int ServerStart(IRTDUpdateEvent CallbackObject)
    {
        this.ExcelContext = new SynchronizationContext();
        xlRTDUpdater = CallbackObject;
        Requests.RequestProcessCompletedHandle += new EventHandler<ProcessCompletedArgs>(Requests_RequestProcessCompletedHandle);
        return 1;
    }

    public void ServerTerminate()
    {
        Requests.Clear();
        xlRTDUpdater = null;
    }
}
首先我们定义了三个对象,RequestsDictionary类型的Requests对象用来存储和处理所有传进来的请求,IRTDUpdateEvent对象用来保存ServerStart中的CallBack对象,用于后面调用UpdateNotify方法。这里还定义了一个同步上下文环境,用来保存Excel主线程的上下文环境,因为我们再调用RTD的UpdateNotify方法时,必须在主线程上操作。

在ServerStart函数中,我们注册了RequestsDictionary对象的处理完成事件。在该回调方法中我们向Excel发出消息,提示调用UpdateNotify方法:
void Requests_RequestProcessCompletedHandle(object sender, ProcessCompletedArgs e)
{
    if (e.Formulas != null && e.Formulas.Length != 0)
    {
        ExcelContext.Send(delegate(object obj) { xlRTDUpdater.UpdateNotify(); }, null);
    }
}

一切初始化好之后,我们开始编写ConnectData方法:

public dynamic ConnectData(int TopicID, ref Array Strings, ref bool GetNewValues)
{
    string formula = GetFormula(Strings);
    Request tmpRequest;
    if (Requests.ContainsKey(formula))
    {
        tmpRequest = Requests[formula];
        tmpRequest.IsValid = true;
        if (tmpRequest.InCache)
        {
            return tmpRequest.Result.ToString();
        }
        else
        {
            return "Fetching...";
        }
    }
    else
    {
        Request requestObj = new Request();
        requestObj.Formula = formula;
        requestObj.TopicID = TopicID;
        requestObj.InCache = false;
        requestObj.IsValid = true;
        Requests.Add(formula, requestObj);
        return "Fetching...";
    }
}
在该方法中,我们对于每一个请求,新建了一个Request对象,并将该请求的TopicID,formula记下来,然后加入到处理集合中,然后返回Processing… 提示用户后台正在处理。

在Refresh方法中,我们遍历集合中所有已经处理完成了的请求,然后赋给二维数组返回供Excel刷新使用:
public Array RefreshData(ref int TopicCount)
{
    object[,] valueArray = new object[2, Requests.Count];

    int count = 0;
    foreach (Request item in Requests.Values)
    {
        if (item.IsValid && item.InCache)
        {
            valueArray[0, count] = item.TopicID;
            valueArray[1, count++] = item.Result;
        }
    }
    TopicCount = count;
    return valueArray;
}
在Refresh方法中,我们遍历集合中所有已经处理完成了的请求,然后赋给二维数组返回供Excel刷新使用。

现在我们的RTD 函数写完了,用户如果要查询天气,输入=YY_Weather_Condition(“Shanghai”, “2013-09-28”),我们可以在VBA中发起响应的RTD 调用=RTD(“YYAsyncRTD.Func”,,“YY_Weather_Condition”,”2”,“Shanghai”,“ 2013-09-28”),我们约定RTD中,第一个参数为函数名称,第二个参数为该函数的参数个数,后面的都是函数的具体参数。这样我们就可以在请求的逻辑处还原YY_Weather_Condition(“Shanghai”, “2013-09-28”)这一请求。这样我们的函数就是异步的UDF 函数了。

3. 实际效果

写完上面的实现之后,将该项目设置为面向com可见,然后编译整个类库,我们在该RTD中可以异步调用我们之前的天气函数了。比如我们可以在单元格中输入函数:
=RTD("YYAsyncRTD.Func",,"YY_Weather_Condition",2, "Shanghai",TODAY())
该函数首先返回Fetching通知Excel正在处理,然后待处理完成之后,将Shanghai今天的天气情况刷新返回出来。我录制了一个动画,效果如下:

Async UDF

可以看到,整个用户界面要比同步的UDF函数要好很多,而且在单元格Fetching的时候,Excel单元格还是能够响应其他的操作,比如选中其他单元格等等,Excel的UI界面一点儿没有阻塞。

四、总结


本文首先介绍了异步自定义函数相较于传统的同步UDF的具有良好扩展性,用户体验等优点,然后介绍了在Excel中实现异步UDF的步骤及解决方案,最后通过自定义Dictionary类,然后基于RTD函数实现了一个简单的异步UDF函数,并通过该异步UDF函数实现了对第四篇文章中的天气函数的调用。Excel 异步UDF函数编写较为复杂,希望本文对您了解及如何编写Excel异步UDF函数有所帮助。

标签:RTD,请求,自定义,request,函数,Excel,UDF,浅谈
From: https://www.cnblogs.com/ifwz/p/17583588.html

相关文章

  • 浅谈Excel开发:七 Excel 自定义任务窗体
    前面花了三篇文章讲解了Excel中的UDF函数,RTD函数和异步UDF函数,这些都是Excel开发中的重中之重。本文现在开始接着第二篇文章的菜单系统开始讲解Excel中可供开发的界面元素,本文要讲解的是Excel中的自定义任务面板(CustomeTaskPanel,CTP)。自定义任务面板在Office2003中就引入了......
  • 浅谈Excel开发:八 Excel 项目的安装部署
    前面几篇文章讲解了Excel开发的几个比较主要的也是比较重要的方面,比如菜单系统,Excel对象模型,自定义函数,RTD函数,异步自定义函数,用户自定义任务面板等,在实际开发中我们还会遇到各种“千奇百怪”的问题,以及开发中的一些注意事项和技巧等,后面有空我会写文介绍。当我们的Excel外接应用......
  • excel 表格操作
    1.工作簿工作表工作簿:表示整个excle文件工作表:工作簿(里面可以有很多工作表)2.3.4.5.6.7.8.......
  • bartender通过查询的方式打印Excel数据
    使用bartender打印标签,根据弹出的对话框提示输入material值,如57616136. 设置如下:(下图二选一) ......
  • 视频直播系统源码,vue自定义模拟滚动条
    视频直播系统源码,vue自定义模拟滚动条vscroll自定义滚动条模板 <template> <divclass="vui__scrollbar"ref="ref__box"@mouseenter="handleMouseEnter"@mouseleave="handleMouseLeave"v-resize="handleResize">  <div:......
  • excel导出报错
    Causedby:java.lang.NoSuchMethodError:org.apache.poi.ss.usermodel.Cell.getCellType()Lorg/apache/poi/ss/usermodel/CellType;错误原因日志1现象,hutool5.7.3版本开发导出excel,发现有的无法导出,报错,错误日志如上2原因,poi版本冲突或有其他easy-poi版本过低,低于4.0.0导致......
  • 高性能、高扩展、高稳定:解读 EasyMR 大数据组件自定义可扩展能力
    随着互联网技术的不断发展以及大数据时代的兴起,企业对于数据分析和洞察的需求日益增长。大多数企业都积累了大量的数据,需要从这些数据中快速灵活地提取有价值的信息,以便为用户提供更好的服务或者帮助企业做出更明智的决策。然而在不同的数据场景中,企业往往会选择不同的大数据组件......
  • 前端Vue仿支付宝自定义可滑动轮播分页宫格菜单组件,可支持九宫格 十二宫格 十五宫格
    背景介绍随着技术的不断发展,传统的开发方式使得系统的复杂度越来越高。在传统开发过程中,一个小小的改动或者一个小功能的增加可能会导致整体逻辑的修改,造成牵一发而动全身的情况。为了解决这个问题,我们采用了组件化的开发模式。通过组件化开发,可以有效地实现单独开发,单独维护,而且......
  • iptables——自定义链创建及引用
    当默认链中的规则非常多时,不方便我们管理,此时可以考虑用自定义链。比如将所有针对80端口的web入站规则都写入到IN_WEB自定义链中,将所有针对sshd的出站规则都写入到OUT_SSH自定义链中。创建自定义链#在filter表中创建IN_WEB自定义链iptables-tfilter-NIN_WEB引用自定......
  • python读取excel为什么是小数
    Python读取Excel为什么是小数在使用Python读取Excel文件时,经常会遇到一个问题:为什么读取的数据会以小数的形式显示,而不是原始的整数或文本呢?这个问题涉及到Python读取Excel的原理以及Excel中的数据类型的转换。Excel中的数据类型在Excel中,每个单元格都有自己的数据类型。常见的......