Excel ExcelDNA C# / 尝试复制 Bloomberg BDH() 的行为(在网络请求后编写数组)

8

我希望复制Bloomberg BDH的行为。

BDH发起了一个网络请求并写入一个数组(但未返回数组样式)。在这个网络请求期间,函数返回“#N/A Requesting”。 当网络请求完成后,BDH()函数将把数组结果写入工作表。

例如,在ExcelDNA中,我成功地使用线程写入了工作表。

如果您在DNA文件中使用下面的代码,则

=WriteArray(2;2)

的结果将是

第1行 > #N/A Requesting Data (0,1)

第2行 > (1,0) (1,1)

最后一个问题是用值替换#N/A Requesting Data并复制公式。 当您取消注释//xlActiveCellType.InvokeMember("FormulaR1C1Local"时,您接近结果,但没有正确的行为

文件.dna

 <DnaLibrary Language="CS" RuntimeVersion="v4.0">
<![CDATA[

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using ExcelDna.Integration;


    public static class WriteForXL
    {

        public static object[,] MakeArray(int rows, int columns)
        {
            if (rows == 0 && columns == 0)
            {
                rows = 1;
                columns = 1;
            }


            object[,] result = new string[rows, columns];
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < columns; j++)
                {
                    result[i, j] = string.Format("({0},{1})", i, j);
                }
            }

            return result;
        }

        public static object WriteArray(int rows, int columns)
        {
            if (ExcelDnaUtil.IsInFunctionWizard())
                return "Waiting for click on wizard ok button to calculate.";

            object[,] result = MakeArray(rows, columns);

            var xlApp = ExcelDnaUtil.Application;
            Type xlAppType = xlApp.GetType();
            object caller = xlAppType.InvokeMember("ActiveCell", BindingFlags.GetProperty, null, xlApp, null);
            object formula = xlAppType.InvokeMember("FormulaR1C1Local", BindingFlags.GetProperty, null, caller, null);

            ObjectForThread q = new ObjectForThread() { xlRef = caller, value = result, FormulaR1C1Local = formula };

            Thread t = new Thread(WriteFromThread);
            t.Start(q);            

            return "#N/A Requesting Data";
        }

        private static void WriteFromThread(Object o)
        {
            ObjectForThread q = (ObjectForThread) o;

            Type xlActiveCellType = q.xlRef.GetType();

            try
            {
                for (int i = 0; i < q.value.GetLength(0); i++)
                {
                    for (int j = 0; j < q.value.GetLength(1); j++)
                    {
                        if (i == 0 && j == 0)
                            continue;

                        Object cellBelow = xlActiveCellType.InvokeMember("Offset", BindingFlags.GetProperty, null, q.xlRef, new object[] { i, j });
                        xlActiveCellType.InvokeMember("Value", BindingFlags.SetProperty, null, cellBelow, new[] { Type.Missing, q.value[i, j] });             
                    }
                }                               
            }
            catch(Exception e)
            {                
            }
            finally
            {
                //xlActiveCellType.InvokeMember("Value", BindingFlags.SetProperty, null, q.xlRef, new[] { Type.Missing, q.value[0, 0] });
                //xlActiveCellType.InvokeMember("FormulaR1C1Local", BindingFlags.SetProperty, null, q.xlRef, new [] { q.FormulaR1C1Local });               
            }
        } 

public class ObjectForThread
        {
            public object xlRef { get; set; }
            public object[,] value { get; set; }
            public object FormulaR1C1Local { get; set; }
        }

    }

    ]]>

</DnaLibrary>

@Govert

BDH已成为金融行业的标准。即使是Ctrl+Shift+Enter,人们也不知道如何操作数组。

BDH函数使彭博社如此受欢迎(对路透社不利)。

但我会考虑使用您的方法或RTD。

感谢您在Excel DNA中的所有工作。


BDH()在完成后会在单元格(0,0)中留下什么,是一个值还是一个公式? - Govert
如果您想要CAC 40指数的历史记录=BDH("CAC Index";"PX_LAST";"20/05/2010";"20/05/2012"),则在单元格(0,0)中会有一个值,在公式=BDH("CAC Index";"PX_LAST";"20/05/2010";"20/05/2012";"cols=2;rows=478")中会有多个值。BDH遵守Excel的所有标准,除非数据覆盖现有工作表中的数据。 - Farandole
好的 - 所以在您的函数中,只需将修改后的公式写回单元格(0,0),并让函数检测修改后的参数以返回您想要显示的值。我猜这就是当BDH看到“cols=2;rows=478”作为最后一个参数时所做的。 - Govert
Govert;您能否进一步详细说明一下?我已经使用ExcelDna示例实现了与上述相似的功能。在我的示例中,我返回“#Retriving Data”,然后生成线程来从后端进程获取数据;然后使用原始Excel范围将值粘贴到调用者位置。但是在这样做时,它会丢失公式。我想保持公式不变。我该怎么做? - Ocean
4个回答

7
我假设您已经尝试过Excel-DNA ArrayResizer示例,该示例小心地避免了您遇到的许多问题。我想了解您认为使用数组公式编写方法的缺点是什么。
现在,关于您的函数:
首先,您无法安全地将'caller' Range COM对象传递给另一个线程-而应该传递一个带有地址的字符串,并从其他线程获取COM对象(使用对工作线程上的ExcelDnaUtil.Application的调用)。大多数情况下,您会很幸运。
更好的方法是从工作线程获取Excel以在主线程上运行宏-通过调用Application.Run。 Excel-DNA ArrayResizer示例展示了如何完成此操作。
其次,您几乎肯定不想要ActiveCell,而是要使用Application.Caller。 ActiveCell可能与正在运行公式的单元格毫不相关。
下一步 - Excel将在每次重新设置公式时重新计算您的函数 - 因此,在启用您的finally子句中的公式设置时,将使您进入无限循环。 您不能同时为单元格设置值和公式 - 如果单元格具有公式,则Excel将使用该公式计算值。如果您设置了值,则公式会被删除。 不清楚您实际要在[0,0]单元格中留下什么 - 我记得Bloomberg以某种方式修改了那里的公式,使其记住写入的范围有多大。您可以尝试向函数添加一些参数,告诉您的函数是否要重新计算或返回实际值作为其结果。
最后,您可能需要重新考虑Bloomberg BDH函数是否是您想要执行的好示例。它会破坏您的表格的依赖关系计算,这对于性能和维护电子表格模型的一致性都有影响。

1
你不能同时为单元格设置值和公式。猜测的技巧是首先返回公式的结果为#N/A Requesting Data (0,1),当你让Excel重新计算公式'cell.Calculate()',然后返回“完成”或其他内容。保持一个静态标志,指示应该显示哪个...有点hack但我想它可能会起作用。(感谢ExcelDNA!) - gjvdkamp

2
我的问题是:
- 编写动态数组 - 通过Web服务异步检索数据
在与Govert讨论后,我选择将结果作为数组而不是复制Bloomberg函数(编写一个数组但返回单个值)。
最终,为了解决我的问题,我使用了http://excel-dna.net/2011/01/30/resizing-excel-udf-result-arrays/和重塑resize()函数。
此代码不是RTD。
下面的代码适用于.dna文件。
<DnaLibrary RuntimeVersion="v4.0"  Language="C#">
<![CDATA[
    using System;
    using System.Collections.Generic;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.ComponentModel;
    using ExcelDna.Integration;

    public static class ResizeTest
    {
        public static object[,] MakeArray(int rows, int columns)
        {
            object[,] result = new string[rows, columns];
            for (int i = 0; i < rows; i++)
            {
                for (int j = 0; j < columns; j++)
                {
                    result[i,j] = string.Format("({0},{1})", i, j);
                }
            }

            return result;
        }

        public static object MakeArrayAndResize()
        {         
            // Call Resize via Excel - so if the Resize add-in is not part of this code, it should still work.
            return XlCall.Excel(XlCall.xlUDF, "Resize", null);
        }
    }

    public class Resizer
    {
        static Queue<ExcelReference> ResizeJobs = new Queue<ExcelReference>();
        static Dictionary<string, object> JobIsDone = new Dictionary<string, object>();

        // This function will run in the UDF context.
        // Needs extra protection to allow multithreaded use.
        public static object Resize(object args)
        {
            ExcelReference caller = XlCall.Excel(XlCall.xlfCaller) as ExcelReference;
            if (caller == null)
                return ExcelError.ExcelErrorNA;

            if (!JobIsDone.ContainsKey(GetHashcode(caller)))
            {
                BackgroundWorker(caller);
                return ExcelError.ExcelErrorNA;
            }
            else
            {
                // Size is already OK - just return result
                object[,] array = (object[,])JobIsDone[GetHashcode(caller)];
                JobIsDone.Remove(GetHashcode(caller));
                return array;
            }
        }

        /// <summary>
        /// Simulate WebServiceRequest
        /// </summary>
        /// <param name="caller"></param>
        /// <param name="rows"></param>
        /// <param name="columns"></param>
        static void BackgroundWorker(ExcelReference caller)
        { 
            BackgroundWorker bw = new BackgroundWorker();
            bw.DoWork += (sender, args) =>
            {
                Thread.Sleep(3000);
            };
            bw.RunWorkerCompleted += (sender, args) =>
            {
                // La requete
                Random r = new Random();
                object[,] array = ResizeTest.MakeArray(r.Next(10), r.Next(10));

                JobIsDone[GetHashcode(caller)] = array;
                int rows = array.GetLength(0);
                int columns = array.GetLength(1);
                EnqueueResize(caller, rows, columns);
                AsyncRunMacro("DoResizing");
            };

            bw.RunWorkerAsync();
        }

        static string GetHashcode(ExcelReference caller)
        {
            return caller.SheetId + ":L" + caller.RowFirst + "C" + caller.ColumnFirst;
        }


        static void EnqueueResize(ExcelReference caller, int rows, int columns)
        {
            ExcelReference target = new ExcelReference(caller.RowFirst, caller.RowFirst + rows - 1, caller.ColumnFirst, caller.ColumnFirst + columns - 1, caller.SheetId);
            ResizeJobs.Enqueue(target);
        }

        public static void DoResizing()
        {
            while (ResizeJobs.Count > 0)
            {
                DoResize(ResizeJobs.Dequeue());
            }
        }

        static void DoResize(ExcelReference target)
        {
            try
            {
                // Get the current state for reset later

                XlCall.Excel(XlCall.xlcEcho, false);

                // Get the formula in the first cell of the target
                string formula = (string)XlCall.Excel(XlCall.xlfGetCell, 41, target);
                ExcelReference firstCell = new ExcelReference(target.RowFirst, target.RowFirst, target.ColumnFirst, target.ColumnFirst, target.SheetId);

                bool isFormulaArray = (bool)XlCall.Excel(XlCall.xlfGetCell, 49, target);
                if (isFormulaArray)
                {
                    object oldSelectionOnActiveSheet = XlCall.Excel(XlCall.xlfSelection);
                    object oldActiveCell = XlCall.Excel(XlCall.xlfActiveCell);

                    // Remember old selection and select the first cell of the target
                    string firstCellSheet = (string)XlCall.Excel(XlCall.xlSheetNm, firstCell);
                    XlCall.Excel(XlCall.xlcWorkbookSelect, new object[] {firstCellSheet});
                    object oldSelectionOnArraySheet = XlCall.Excel(XlCall.xlfSelection);
                    XlCall.Excel(XlCall.xlcFormulaGoto, firstCell);

                    // Extend the selection to the whole array and clear
                    XlCall.Excel(XlCall.xlcSelectSpecial, 6);
                    ExcelReference oldArray = (ExcelReference)XlCall.Excel(XlCall.xlfSelection);

                    oldArray.SetValue(ExcelEmpty.Value);
                    XlCall.Excel(XlCall.xlcSelect, oldSelectionOnArraySheet);
                    XlCall.Excel(XlCall.xlcFormulaGoto, oldSelectionOnActiveSheet);
                }
                // Get the formula and convert to R1C1 mode
                bool isR1C1Mode = (bool)XlCall.Excel(XlCall.xlfGetWorkspace, 4);
                string formulaR1C1 = formula;
                if (!isR1C1Mode)
                {
                    // Set the formula into the whole target
                    formulaR1C1 = (string)XlCall.Excel(XlCall.xlfFormulaConvert, formula, true, false, ExcelMissing.Value, firstCell);
                }
                // Must be R1C1-style references
                object ignoredResult;
                XlCall.XlReturn retval = XlCall.TryExcel(XlCall.xlcFormulaArray, out ignoredResult, formulaR1C1, target);
                if (retval != XlCall.XlReturn.XlReturnSuccess)
                {
                    // TODO: Consider what to do now!?
                    // Might have failed due to array in the way.
                    firstCell.SetValue("'" + formula);
                }
            }
            finally
            {
                XlCall.Excel(XlCall.xlcEcho, true);
            }
        }

        // Most of this from the newsgroup: http://groups.google.com/group/exceldna/browse_thread/thread/a72c9b9f49523fc9/4577cd6840c7f195
        private static readonly TimeSpan BackoffTime = TimeSpan.FromSeconds(1); 
        static void AsyncRunMacro(string macroName)
        {
            // Do this on a new thread....
            Thread newThread = new Thread( delegate ()
            {
                while(true) 
                { 
                    try 
                    {
                        RunMacro(macroName);
                        break; 
                    } 
                    catch(COMException cex) 
                    { 
                        if(IsRetry(cex)) 
                        { 
                            Thread.Sleep(BackoffTime); 
                            continue; 
                        } 
                        // TODO: Handle unexpected error
                        return; 
                    }
                    catch(Exception ex) 
                    { 
                        // TODO: Handle unexpected error
                        return;
                    } 
                }
            });
            newThread.Start();
        }

        static void RunMacro(string macroName)
        {
            object xlApp = null;       
            try
            {
                xlApp = ExcelDnaUtil.Application;
                xlApp.GetType().InvokeMember("Run", BindingFlags.InvokeMethod, null, xlApp, new object[] {macroName});
            }
            catch (TargetInvocationException tie)
            {
                throw tie.InnerException;
            }
            finally
            {
                Marshal.ReleaseComObject(xlApp);
            }
        }

        const uint RPC_E_SERVERCALL_RETRYLATER = 0x8001010A; 
        const uint VBA_E_IGNORE = 0x800AC472; 
        static bool IsRetry(COMException e) 
        { 
            uint errorCode = (uint)e.ErrorCode; 
            switch(errorCode) 
            { 
                case RPC_E_SERVERCALL_RETRYLATER: 
                case VBA_E_IGNORE: 
                    return true; 
                default: 
                    return false; 
            }
        }
    } 
]]>
</DnaLibrary>

这段代码似乎无法工作。MakeArray仅在单元格中打印“(0,0)”。MakeArrayAndResize调用了Resize线程,但未将任何值写入单元格。 - mike01010

0

我认为你需要将请求实现为RTD服务器。普通的用户定义函数不会异步更新。
然后,你可以通过一个用户定义函数来隐藏RTD服务器的调用,这可以通过Excel-DNA完成。


-2

所以最终你使用了数组公式,对吧?就像你说的,用户不熟悉数组公式,他们不知道ctrl+shift+enter。我认为数组公式对他们来说是一个大问题。

对我来说,我也有同样的问题。我正在尝试为它建立一个原型。请看https://github.com/kchen0723/ExcelAsync.git


这个项目仍在进行中,需要添加一些注释。 - kissinger chen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接