How to write array in Excel User Defined Function - c#

I have an Excel Addin created using Excel-DNA and have a UDF as part of the Excel Add-In. Lets say that the function is in Cell A10 and I go to delete Column Z. I have found that this action causes the function to execute again.
Is there a way to prevent this behavior from occurring? Does this have to do with the calculation model or volatility?
EDIT 1
I am using the following code to implement behavior like the Bloomberg BDH functionality - i.e. that the function is written in the first cell and the rest of the array is written via a thread.
I have 2 problems:
I cannot determine how to have a message flash in the cell where the function is written. For example, if the function is run in Cell A1, the message "Processing..." is displayed until the value of the cell has been written.
The recalculation of functions occurs with this implementation.
I have seen implementations where items 1 & 2 above are working well, but cannot determine how to do so. Does anybody have any ideas
public class ArrayWriter
{
#region Methods
#region WriteArray
[ExcelFunction(IsHidden=true)]
public object WriteArray(object[,] arrayToWrite)
{
object caller = null;
object formula = null;
AddInFacade facade;
// if not in a function
if (!ExcelDnaUtil.IsInFunctionWizard())
{
facade = new AddInFacade();
if (arrayToWrite != null)
{
// if writing more than one cell, use threads
if (arrayToWrite.GetLength(0) > 1 || arrayToWrite.GetLength(1) > 1)
{
var xlApp = ExcelDnaUtil.Application as Application;
Type xlAppType = xlApp.GetType();
caller = xlApp.Caller;
//caller = xlAppType.InvokeMember("ActiveCell", BindingFlags.GetProperty, null, xlApp, null);
formula = xlAppType.InvokeMember("FormulaR1C1Local", BindingFlags.GetProperty, null, caller, null);
// create instance of ObjectForThread and set all properties of the class
ObjectForThread threadObject = new ObjectForThread()
{
xlRef = caller,
value = arrayToWrite,
};
// create a new thread calling the method WriteFromThread and start the thread
Thread threadWriter = new Thread(() => WriteFromThread(threadObject));
threadWriter.Start();
}
else
{
facade.SetMouseCursor(XlMousePointer.xlDefault);
}
}
else
{
arrayToWrite = new object[1, 1];
arrayToWrite[0, 0] = "No data was returned.";
facade.SetMouseCursor(XlMousePointer.xlDefault);
}
}
return arrayToWrite[0,0];
}
#endregion
#region WriteFromThread
private void WriteFromThread(Object boxedThreadObject)
{
AddInFacade facade = new AddInFacade();
ObjectForThread unboxedThreadObject = (ObjectForThread)boxedThreadObject;
Object cellBelow;
Type typeCellReference = unboxedThreadObject.xlRef.GetType();
try
{
for (int i = 0; i < unboxedThreadObject.value.GetLength(0); i++)
{
for (int j = 0; j < unboxedThreadObject.value.GetLength(1); j++)
{
// do not write the first cell as this is what is returned by the function
if (i > 0 || j > 0)
{
cellBelow = typeCellReference.InvokeMember("Offset", BindingFlags.GetProperty, null, unboxedThreadObject.xlRef, new object[] { i, j });
typeCellReference.InvokeMember("Value", BindingFlags.SetProperty, null, cellBelow, new[] { Type.Missing, unboxedThreadObject.value[i, j] });
}
}
}
}
catch(Exception ex)
{
string szError = ex.Message;
}
finally
{
// attempt to kill all COM references
unboxedThreadObject.xlRef = null;
unboxedThreadObject.value = null;
//Set the mouse cursor to the default cursor since the entire array has now been written
facade.SetMouseCursor(XlMousePointer.xlDefault);
unboxedThreadObject = null;
cellBelow = null;
facade = null;
}
}
#endregion
#endregion
#region ObjectForThread Class
public class ObjectForThread
{
public object xlRef { get; set; }
public object[,] value { get; set; }
}
#endregion
}

The only way to change this behavior is to change the Application.Calculation property. But you don't want to do that. This property should only be changed momentarily, and afterward, it should be reset to the previous value. Otherwise, you're writing an addin that doesn't play well in a shared sandbox.

Related

How to write 2D data containing excel errors using c# VSTO

I have Excel functions where I am writing 2D data using VSTO. In some cases, I need to write an Excel error like #N/A. How do I do this? I have attempted to use XlCVError.xlErrNA but it just writes the value 2042.
Codewise, I have the following:
public void WriteChanges(int startRow, int startCol, int endRow, int endCol, int rows, int columns, IItems items)
{
object[,] data = new object[rows, columns];
var startCell = (Range)this.sheet.Cells[startRow, startCol];
var endCell = (Range)this.sheet.Cells[endRow, endCol];
var rangeToWrite = this.sheet.Range[startCell, endCell];
var error = new ExcelError();
foreach (var item in items)
{
int row = item.Row;
int col = item.Column;
if (!(item.Value is eExcelErrror))
{
data[row, col] = item.Value;
}
else
{
data[row, col] = this.error.ToVstoError((eExcelError)item.Value);
}
}
rangeToWrite.Value = data;
}
public enum eExcelError
{
ExcelErrorValue = 2015,
ExcelErrorNA = 2042
}
public class ExcelError
{
public object ToVstoNativeError(eExcelError value)
{
switch (value)
{
case eExcelError.ExcelErrorNA:
return this.VstoNoData();
case eExcelError.ExcelErrorValue:
default:
return this.VstoErrorValue();
}
}
public object VstoNoData()
{
return XlCVError.xlErrNA;
}
public object VstoErrorValue()
{
return XlCVError.xlErrValue;
}
}
I don't think it is possible to set Excel errors into a cell... which kind of makes sense. If you were the user and you saw a #DIV/0 error in a cell, but there was no formula, or you saw a #NAME error and you inserted the defined name into the names manager and it still didn't work, that would be very confusing.
What I would recommend doing is cell.Value = "#REF". At least this way the user can see that this is just a string and not an actual error from Excel that they may fall into a rabbit hole chasing.
Reference: https://learn.microsoft.com/en-us/dotnet/api/microsoft.office.interop.excel.range.errors?view=excel-pia#Microsoft_Office_Interop_Excel_Range_Errors (note: get only)

How to unit test private properties?

I'm pretty new to TDD and I have a hard time to understand how to test private members of the class (I know! It's private, shouldn't be tested - but please keep reading). We might have a public function which sets private property and other public function that returns "something" based on that private property.
Let me show you a basic example:
public class Cell
{
public int X { get; set; }
public int Y { get; set; }
public string Value { get; set; }
}
public class Table
{
private Cell[,] Cells { get; }
public Table(Cell[,] cells)
{
Cells = cells;
}
public void SetCell(int x, int y, string value)
{
Cells[x, y].Value = value;
}
public void Reset()
{
for (int i = 0; i < Cells.GetLength(0); i++)
{
for (int j = 0; j < Cells.GetLength(1); j++)
{
Cells[i, j].Value = "";
}
}
}
public bool AreNeighborCellsSet(int x, int y)
{
bool areNeighborCellsSet = false;
// checking...
return areNeighborCellsSet;
}
}
In this example Cells are private, because there's no reason to make them public. I don't need to know what's the value of particular Cell outside this class. I just need an information if neighbor cells are empty.
1. How can I test Reset method?
Technically I should create a Table with mocked array of cells. Call Reset and then assert if every cell has empty Value. But I can't actually check if they are empty or not.
2. In this case I would call Assert many times (for every cell) - is it a good practice? I've read that "It's not!", but Reset resets all cells, so I have to somehow check every cell.
EDIT:
Option 2:
public class Table
{
private Cell[,] Cells { get; }
public Table(int height, int width, ICellFactory cellFactory)
{
Cells = new ICell[height, width];
for (int i = 0; i < Cells.GetLength(0); i++)
{
for (int j = 0; j < Cells.GetLength(1); j++)
{
Cells[i, j].Value = cellFactory.Create(i, j);
}
}
}
// Rest is the same...
}
Your class have three public methods
void SetCell
void Reset
bool AreNeighborCellsSet
So all functionality should be tested only through those methods and with possible help of constructor input arguments.
I am afraid you are not doing TDD, because you are trying to test already implemented logic (for loop of internal member). With TDD you should write unit tests by using only public API of class under test.
When you test Reset method you should think how it affect on results of other public methods. Table class has only one method which return some value we can observe - bool AreNeighborCellsSet - so seems like this is the only method against which we can execute our asserts.
For Reset method you need to set cells so that AreNeighborCellsSet returns true. Then execute Reset and assert that now AreNeighborCellsSet returns false.
[Test]
public void AfterResetGivenCellShouldNotHaveNeighbors()
{
// Arrange
var cell = new Cell { X = 1, Y = 1, Value = "central" };
var neighborCell = new new Cell { X = 1, Y = 2, Value = "neighbor" };
var table = new Table(new[] { cell, neighborCell });
// table.AreNeighborCellsSet(cell.X, cell.Y) - should return true at this moment
// Act
table.Reset();
// Assert
table.AreNeighborCellsSet(cell.X, cell.Y).Should().BeFalse();
}
This is a good example of TDD (Test-Driven Development), where problems with testing is good sign that something wrong with design.
Actually, I think, in your case you don't need Reset method at all - just create a new instance of Table every time you need to reset it.
The answer of Ignas my be a workaround for the problem but I feel a need to clarify some design issues here:
Basically there is no need to check if loop iterates through whole collection. That is tested by the framework team in MS.
What you need to do is to check if your new type (in this case Cell) behaves properly.
In my opinion you're violating the SRP. There is really no need for Table class to know how to reset this particular implementation of Cell. If some day you decide to create a cell able to contain a picture let's say, you'll most likely feel a need to clear it in some other way than by setting an empty string to it's Value property.
Start with abstracting Cell to an interface. Then just add method Reset() to the Cell and call it in the loop in Table class for every cell.
That would allow you to create tests for your implementation of Cell and there you can check if after calling Reset() cell's value truly becomes null or empty or whatever you need :-)
There are ways to test private properties with no need for changing your code or adding extra code to your tested class, you can use testing tools that allows you to do so.
for example i used Typemock to change the logic of the Table c'tor to create a populated table and to get the private property Cells after calling the reset method:
public void TestMethod1()
{
var handle = Isolate.Fake.NextInstance<Table>(Members.CallOriginal, context =>
{
var tempcells = context.Parameters[0] as Cell[,];
for (int i = 0; i < tempcells.GetLength(0); i++)
{
for (int j = 0; j < tempcells.GetLength(1); j++)
{
tempcells[i, j] = cellFactory.Create(i, j);
}
}
context.Parameters[0] = tempcells;
//calling the original ctor with tempcells as the parameter
context.WillCallOriginal();
});
// calling the ctor with the custom logic
var testTable = new Table(new Cell[2,2]);
testTable.Reset();
// calling the private property
var resTable = Isolate.Invoke.Method(testTable, "get_Cells") as Cell[,];
// for asserting
var emptyCell = new Cell { Value = string.Empty };
for (int i = 0; i < 2; i++)
{
for(int j=0; j<2; j++)
{
Assert.AreEqual(emptyCell.Value, resTable[i, j].Value);
}
}
}
As Zegar mentioned in comments there could be several code design considerations, and probably writing tests first aka using TDD would help not to even run into such situations, however I think there is a simple workaround as well.
You are passing array as reference into Table class and not overriding it, therefore you can access the array outside of Table class even though you are modifying it inside the class.
You don't need to do many asserts, you just need to arrange an expected array for assertion. Use FluentAssertions and specifically ShouldBeEquivalentTo() which is a very nice solution for arrays comparison. Nuget package.
Sample test below.
[TestMethod]
public void TestMethod1()
{
// Arrange
var expectedCells = new Cell[2, 2];
expectedCells[0, 0] = new Cell { Value = string.Empty };
expectedCells[0, 1] = new Cell { Value = string.Empty };
expectedCells[1, 0] = new Cell { Value = string.Empty };
expectedCells[1, 1] = new Cell { Value = string.Empty };
var cells = new Cell[2,2];
cells[0,0] = new Cell { Value = "00" };
cells[0,1] = new Cell { Value = "01" };
cells[1,0] = new Cell { Value = "10" };
cells[1,1] = new Cell { Value = "11" };
var table = new Table(cells);
// Act
table.Reset();
// Assert
cells.ShouldBeEquivalentTo(expectedCells); // using FluentAssertions
}
To summarize and answer your questions.
Test cells array you pass into the constructor.
Ideally you want to have a single assert per test, if possible.

C# Invoke with out parameter using .net 3.5

I want to invoke a function with two out parameters and bool as return value. Now my problem is I see that those two parameters are changed when I debug, but still they are back to length = 0 (the way they are initialized) when the function returns.
I've seen that there's a great solution for the .net-framework 4, but unfortunaly I have to use the .net-framework 3.5.
Here's my code:
public delegate bool GetAllCheckedItemsDelegate(out int[] cTypes, out int[] cFiles);
public bool GetAllCheckedItems(out int[] cTypes , out int[] cFiles )
{
if (ListView.InvokeRequired)
{
cTypes = new int[0];
cFiles = new int[0];
return (bool)ListView.Invoke(new GetAllCheckedItemsDelegate(GetAllCheckedItems), new object[] { cTypes, cFiles });
}
else
{
cTypes = new int[ListView.CheckedItems.Count];
cFiles = new int[ListView.CheckedItems.Count];
for (int i = 0; i < ListView.CheckedItems.Count; i++)
{
// ......code......
}
return (ListView.CheckedItems.Count > 0);
}
}
I do not really like "out" keyword, so what about of using a class (Row) that contains information:
using SCG = System.Collections.Generic;
using System.Linq;
public class Row {
public int CheckedType { get; set; }
public int CheckedFile { get; set; }
}
...
public delegate SCG.IEnumerable<Row> GetAllCheckedItemsDelegate();
public bool GetAllCheckedItems() {
if (ListView.InvokeRequired) {
var rows = ListView.Invoke(new GetAllCheckedItemsDelegate(GetAllCheckedItems)
, new object[] {});
return rows.Count() > 0;
} else {
var rows = new SCG.List<Row>();
for (int i = 0; i < ListView.CheckedItems.Count; i++) {
// create and set row
var row = new Row { CheckedType = x, CheckedFile = y };
...
rows.Add(row);
}
return rows.AsReadOnly();
}
}
return (bool)ListView.Invoke(..., new object[] { cTypes, cFiles });
That modifies the object[] elements. C# does not provide syntax to get it to update the arguments of the method, there's an extra level of indirection that you can't bridge with code. It is up to you to copy the references back. Nothing to worry about, you are not actually copying the array content, just the references:
var args = new object[] { null, null };
var dlg = new GetAllCheckedItemsDelegate(GetAllCheckedItems);
var retval = (bool)ListView.Invoke(dlg, args);
cTypes = (int[])args[0];
cFiles = (int[])args[1];
return retval;
Nothing pretty about it of course. Do keep in mind that you are certainly doing something very unpretty, you have no guarantee whatsoever that you get the items you expect to get. This code runs at a very unpredictable moment in time, quite disjoint from the code that's running on the UI thread. If the user is busy checking items while the worker is running then you get a quite random selection. A snapshot of the list box state that very quickly turns stale.
This almost always requires you to disable the list box while the worker is running so you provide a guarantee that the result you compute matches the list. That in turn means that it is no longer useful to write this code at all, you might as well obtain the list before you start the thread. Which is the real solution.
I've found a solution that works for me. Still I'm open to different (and better) approaches if you think that this is not a good way to solve this.
public delegate bool BoolDelegate();
public bool GetAllCheckedItems(out int[] cTypes , out int[] cFiles )
{
if (ListView.InvokeRequired)
{
int[] cTypesHelpVar = new int[0];
int[] cFilesHelpVar = new int[0];
bool ret = (bool)ListView.Invoke((BoolDelegate) (() => GetAllCheckedItems(out cTypesHelpVar, out cFilesHelpVar)));
cTypes = cTypesHelpVar;
cFiles = cFilesHelpVar;
return ret;
}
else
{
cTypes = new int[ListView.CheckedItems.Count];
cFiles = new int[ListView.CheckedItems.Count];
for (int i = 0; i < ListView.CheckedItems.Count; i++)
{
//.... code ....
}
return (ListView.CheckedItems.Count > 0);
}
}

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
I presume you have tried the Excel-DNA ArrayResizer sample, which carefully avoids many of the issue you are running into. I'd like to understand what you see as the disadvantages of the array-formula-writing approach.
Now, about your function:
Firstly, you can't safely pass the 'caller' Range COM object to another thread - rather pass a string with the address, and get the COM object from the other thread (using a call to ExcelDnaUtil.Application on the worker thread). Most of the time you'll get lucky, though.
The better way to do this is from the worker thread to get Excel to run a macro on the main thread - by calling Application.Run. The Excel-DNA ArrayResizer sample shows how this can be done.
Secondly, you almost certainly don't want the ActiveCell, but rather Application.Caller. The ActiveCell might well have nothing to do with the cell where the formula is running from.
Next - Excel will recalculate your function every time you set the Formula again - hence putting you in an endless loop when you enable the Formula set in your finally clause. You cannot set both the Value and the Formula for a cell - if a cell has a Formula then Excel will use the formula to calculate the Value. If you set the Value, the Formula gets removed.
It's not clear what you want to actually leave in the [0,0] cell - IIRC Bloomberg modifies the formula there in a way that makes it remember how large a range was written to. You could try to add some parameters to your function that tell your function whether to recalculate or whether to return an actual value as its result.
Finally, you might want to reconsider whether the Bloomberg BDH function is a good example for what you want to do. It breaks the dependency calculation of your sheet, which has implications both for performance and for maintaining consistency of the spreadsheet model.
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>
I think you need to implemented the request as a RTD server. Normal user defined functions will not update asynchronously.
Then you may hide the call of the RTD server via a user defined function, which can be done via Excel-DNA.
So finally you use Array formula, right? As you said, users are not familiar with array formula, they do not know ctrl+shift+enter. I think array formula is a big problem for them.
For me, I have the same issue. I am trying to build a prototype for it. see
https://github.com/kchen0723/ExcelAsync.git

Can a variant array be passed to C# using com interop?

I try to transfer some data between Excel and C#. For this I wrote a simple C# class with a property to set and get the data.
[Guid("xxx")]
[ClassInterface(ClassInterfaceType.AutoDual)]
[ComVisible(true)]
public class Vector
{
private object[,] _values;
public object[,] ExcelValues
{
get { ... }
set { ... }
}
}
Getting the ExcelValues property in VBA works well, but it is not possible to set it in VBA. The VBA code does not compile if I try to set the property:
Dim values As Variant
With myRange
' typo: Set values = Range(.Offset(0, 0), .Offset(100, 0))
values = Range(.Offset(0, 0), .Offset(100, 0))
End With
Dim data As New Vector
' this doesn't compile
data.ExcelValues = values
' this works
values = data.ExcelValues
Any suggestions how I can acomplish this, without setting each value from the variant Array one at a time?
I found a solution based on code that was posted here. A variant array has to be passed from VBA to C# as an object (not object[,]). Then it can be converted to something that is more handy by using reflection:
[Guid("xxx")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class Vector
{
[ComVisible(false)]
public IList<double> Values { get; set; }
public object[,] GetExcelValues()
{
// own extension method
return Values.ConvertToExcelColumn();
}
public void SetExcelValues(object comArray)
{
IEnumerable<object> values = ConvertExcelCloumnToEnumerable(comArray);
Values = new List<double>(values.Select(Convert.ToDouble));
}
private static IEnumerable<object> ConvertExcelCloumnToEnumerable(object comObject)
{
Type comObjectType = comObject.GetType();
if (comObjectType != typeof(object[,]))
return new object[0];
int count = (int)comObjectType.InvokeMember("Length", BindingFlags.GetProperty, null, comObject, null);
var result = new List<object>(count);
var indexArgs = new object[2];
for (int i = 1; i <= count; i++)
{
indexArgs[0] = i;
indexArgs[1] = 1;
object valueAtIndex = comObjectType.InvokeMember("GetValue", BindingFlags.InvokeMethod, null, comObject, indexArgs);
result.Add(valueAtIndex);
}
return result;
}
}
The other way - from C# to VBA - it can be passed more comfortable as object[,] or double[,].
Hope there are no syntax typos :).
By using Set you're telling VBA that "values" is an object (in this case a Range), but then you're not using set when assigning that to ExcelValues. Try dropping the Set when you're reading the values.
With myRange
values = Range(.Offset(0, 0), .Offset(100, 0)).Value
End With

Categories