Search code examples
c#.netexcelexcel-dna

Excel ExcelDNA C# / Try to copy Bloomberg BDH() behavior (writing Array after a web request)


I want to copy Bloomberg BDH behavior.

BDH makes a web request and write an array (but doesn't return an array style). During this web request, the function returns "#N/A Requesting". When the web request finished, the BDH() function writes the array result in the worksheet.

For example, in ExcelDNA, I succeed to write in the worksheet with a thread.

The result if you use the code below in a DNA file, the result of

=WriteArray(2;2)

will be

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

Line 2 > (1,0) (1,1)

The last issue is to replace #N/A Requesting Data with the value and copy the formula. When you uncomment //xlActiveCellType.InvokeMember("FormulaR1C1Local", you are near the result but you don't have the right behavior

File .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>

@To Govert

BDH has become a standard in finance industry. People do not know how to manipulate an array (even the Ctrl+Shift+Enter).

BDH is the function that made Bloomberg so popular (to the disadvantage of Reuters).

However I will think of using your method or RTD.

Thanks for all your work in Excel DNA


Solution

  • My issue was :

    • writing dynamic array

    • data are retrieved asynchronous via a webservice

    After discussing with Govert, I chose to take a result as an array and not to copy Bloomberg functions (write an array but return a single value).

    Finally, to solve my issue, I used http://excel-dna.net/2011/01/30/resizing-excel-udf-result-arrays/ and reshape the resize() function.

    This code is not RTD.

    The code belows works in a .dna file

    <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>