Simple Example of VSTO Excel using a worksheet as a datasource - c#

I think I'm running into a case of "the easiest answers are the hardest ones to find" and I haven't come across any searches that give this to me in a straightforward way. This is for Excel 2010 and VS 2010 within an existing VSTO (C#) project.
I have an Excel worksheet that contains 4 columns of data that I would like to use as a source for a DataGridView. Can someone please provide C# code snippets for (1) getting the data from a particular worksheet and populating a custom object with it? (2) binding the object (like an IEnumerable list) to a Datagridview and (3) some snippets for the update and delete functionality that would be inherent to the grid and feed back to the source worksheet.
I know I'm asking for a lot here, but so much of the VSTO information seems to be dis-jointed and not always easy to find. Thanks!

Edit: Great, I just noticed that I missed a big part of your question, getting updates and deletes back to the worksheet. I have absolutely no idea if that is possible but I think that makes my solution worthless. I'll leave it here anyway, maybe it can help in any way.
Why do you need VSTO? As far as I know VSTO is used for Office Add-Ins. But since you want to show the data in a DataGridView I assume that you have a WinForms application that should just access a workbook. In this case you can simply open the workbook by using Office Interop. Just add a reference to Microsoft.Office.Interop.Excel to your project and add a using Microsoft.Office.Interop.Excel; statement.
MSDN reference documentation for Excel Interop can be found here: http://msdn.microsoft.com/en-us/library/ms262200%28v=office.14%29.aspx
I'll give you the Excel part, maybe someone else can do the rest.
First, open Excel and the workbook:
Application app = new Application();
// Optional, but recommended if the user shouldn't see Excel.
app.Visible = false;
app.ScreenUpdating = false;
// AddToMru parameter is optional, but recommended in automation scenarios.
Workbook workbook = app.Workbooks.Open(filepath, AddToMru: false);
Then somehow get the correct worksheet. You have a few possiblities:
// Active sheet (should be the one which was active the last time the workbook was saved).
Worksheet sheet = workbook.ActiveSheet;
// First sheet (notice that the first is actually 1 and not 0).
Worksheet sheet = workbook.Worksheets[1];
// Specific sheet.
// Caution: Default sheet names differ for different localized versions of Excel.
Worksheet sheet = workbook.Worksheets["Sheet1"];
Then get the correct range. You didn't specify how you know where the needed data is, so I'll assume it is in fixed columns.
// If you also know the row count.
Range range = sheet.Range["A1", "D20"];
// If you want to get all rows until the last one that has some data.
Range lastUsedCell = sheet.Cells.SpecialCells(XlCellType.xlCellTypeLastCell);
string columnName = "D" + lastUsedCell.Row;
Range range = sheet.Range["A1", columnName];
Get the values:
// Possible types of the return value:
// If a single cell is in the range: Different types depending on the cell content
// (string, DateTime, double, ...)
// If multiple cells are in the range: Two dimensional array that exactly represents
// the range from Excel and also has different types in its elements depending on the
// value of the Excel cell (should always be that one in your case)
object[,] values = range.Value;
That two dimensional object array can then be used as a data source for your DataGridView. I haven't used WinForms for years so I don't know if you can bind it directly or first need to get the data into some specific format.
Finally close Excel again:
workbook.Close(SaveChanges: false);
workbook = null;
app.Quit();
app = null;
// Yes, we really want to call those two methods twice to make sure all
// COM objects AND all RCWs are collected.
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();
Correctly closing Excel after using Interop is a task itself because you have to make sure that all references to COM objects have been released. The easiest way I have found to do this is to do all the work except opening and closing Excel and the workbook (so my first and last code block) in a seperate method. This ensures that all COM objects used in that method are out of scope when Quit is called.

UPDATE:
I replaced my previous method with newer code for faster approach. System.Array is quite efficient and faster way to read and bind data to the excel. You can download the demo from this link.
I have developed VSTO application in Excel 2003 Workbook. There is no big differences in terms of syntax,so you can use it in 2007 / 2010 with no efforts.
I didn't know which event you will be using to open the window showing data so i am assuming that you will be using.
SheetFollowHyperlink
I am going to use Static workbook object declared in Showdata.cs. Here's the code for your Thisworkbook.cs
private void ThisWorkbook_Startup(object sender, System.EventArgs e)
{
ShowData._WORKBOOK = this;
}
private void ThisWorkbook_SheetFollowHyperlink(object Sh, Microsoft.Office.Interop.Excel.Hyperlink Target)
{
System.Data.DataTable dTable = GenerateDatatable();
showData sh = new showData(dTable);
sh.Show(); // You can also use ShowDialog()
}
I have added a Link on the current sheet and it will pop up the window with a datagridview.
private System.Data.DataTable GenerateDatatable()
{
Range oRng = null;
// It takes the current activesheet from the workbook. You can always pass any sheet as an argument
Worksheet ws = this.ActiveSheet as Worksheet;
// set this value using your own function to read last used column, There are simple function to find last used column
int col = 4;
// set this value using your own function to read last used row, There are simple function to find last used rows
int row = 5;
//lets assume its 4 and 5 returned by method
string strRange = "A1";
string andRange = "D5";
System.Array arr = (System.Array)ws.get_Range(strRange, andRange).get_Value(Type.Missing);
System.Data.DataTable dt = new System.Data.DataTable();
for (int cnt = 1;
cnt <= col; cnt++)
dt.Columns.Add(cnt.Chr(), typeof(string));
for (int i = 1; i <= row; i++)
{
DataRow dr = dt.NewRow();
for (int j = 1; j <= col; j++)
{
dr[j - 1] = arr.GetValue(i, j).ToString();
}
dt.Rows.Add(dr);
}
return dt;
}
Here's the form which will allow user to display and edit values. I have added extension methods and Chr() to convert numerical into respective alphabets which will come handy.
public partial class ShowData : Form
{
//use static workbook object to access Worksheets
public static ThisWorkbook _WORKBOOK;
public ShowData(System.Data.DataTable dt)
{
InitializeComponent();
// binding value to datagrid
this.dataGridView1.DataSource = dt;
}
private void RefreshExcel_Click(object sender, EventArgs e)
{
Worksheet ws = ShowData._WORKBOOK.ActiveSheet as Worksheet;
System.Data.DataTable dTable = dataGridView1.DataSource as System.Data.DataTable;
// Write values back to Excel sheet
// you can pass any worksheet of your choice in ws
WriteToExcel(dTable,ws);
}
private void WriteToExcel(System.Data.DataTable dTable,Worksheet ws)
{
int col = dTable.Columns.Count; ;
int row = dTable.Rows.Count;
string strRange = "A1";
string andRange = "D5";
System.Array arr = Array.CreateInstance(typeof(object),5,4);
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
try
{
arr.SetValue(dTable.Rows[i][j].ToString(), i, j);
}
catch { }
}
}
ws.get_Range(strRange, andRange).Value2 = arr;
this.Close();
}
public static class ExtensionMethods
{
static string alphabets = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
public static string Chr(this int p_intByte)
{
if (p_intByte > 0 && p_intByte <= 26)
{
return alphabets[p_intByte - 1].ToString();
}
else if (p_intByte > 26 && p_intByte <= 700)
{
int firstChrIndx = Convert.ToInt32(Math.Floor((p_intByte - 1) / 26.0));
int scndIndx = p_intByte % 26;
if (scndIndx == 0) scndIndx = 26;
return alphabets[firstChrIndx - 1].ToString() + alphabets[scndIndx - 1].ToString();
}
return "NA";
}
}

this is one of the most ugly codes I've written but it will work as a proof of concept :)
I've created an example workbook like that
Column1 Column2 Column3 Column4
------------------------------------------------------
Data-1-1 Data-2-1 Data-3-1 Data-4-1
Data-1-2 Data-2-2 Data-3-2 Data-4-2
....
Excel file contains exactly 50 lines, this explains the hard-coded range selectors.
After writing that part of code rest is easy, just create a form, add a dataviewgrid, create a data source for MyExcelData, create an instance of MyExcelData like var data = new MyExcelData(pathToExcelFile); and bind it to grid.
Code is ugly, and has many assumptions but it implements your requirements. If you open excel and program you can see updates on grid are reflected on excel after cell edited. deleted row is also removed from excel. since I did not know whether you have primary keys of your excel or not, I used row index as ID.
BTW, I'm really bad when it comes to VSTO. so if you know a better way open/edit/save please notify me.
public class MyExcelDataObject
{
private readonly MyExcelData owner;
private readonly object[,] realData;
private int RealId;
public MyExcelDataObject(MyExcelData owner, int index, object[,] realData)
{
this.owner = owner;
this.realData = realData;
ID = index;
RealId = index;
}
public int ID { get; set; }
public void DecrementRealId()
{
RealId--;
}
public string Column1
{
get { return (string)realData[RealId, 1]; }
set
{
realData[ID, 1] = value;
owner.Update(ID);
}
}
public string Column2
{
get { return (string)realData[RealId, 2]; }
set
{
realData[ID, 2] = value;
owner.Update(ID);
}
}
public string Column3
{
get { return (string)realData[RealId, 3]; }
set
{
realData[ID, 3] = value;
owner.Update(ID);
}
}
public string Column4
{
get { return (string)realData[RealId, 4]; }
set
{
realData[ID, 4] = value;
owner.Update(ID);
}
}
}
public class MyExcelData : BindingList<MyExcelDataObject>
{
private Application excel;
private Workbook wb;
private Worksheet ws;
private object[,] values;
public MyExcelData(string excelFile)
{
excel = new ApplicationClass();
excel.Visible = true;
wb = excel.Workbooks.Open(excelFile);
ws = (Worksheet)wb.Sheets[1];
var range = ws.Range["A2", "D51"];
values = (object[,])range.Value;
AllowEdit = true;
AllowRemove = true;
AllowEdit = true;
for (var index = 0; index < 50; index++)
{
Add(new MyExcelDataObject(this, index + 1, values));
}
}
public void Update(int index)
{
var item = this[index - 1];
var range = ws.Range["A" + (2 + index - 1), "D" + (2 + index - 1)];
range.Value = new object[,]
{
{item.Column1, item.Column2, item.Column3, item.Column4}
};
}
protected override void RemoveItem(int index)
{
var range = ws.Range[string.Format("A{0}:D{0}", (2 + index)), Type.Missing];
range.Select();
range.Delete();
base.RemoveItem(index);
for (int n = index; n < Count; n++)
{
this[n].DecrementRealId();
}
}
}
PS: I'd like to use lightweight objects but it adds unnecessary complications.

So in the Sheet1_Startup event
Excel.Range range1 = this.Range["A1", missing];
var obj = range1.Value2.ToString();
You would need to move to the next cell then
range1 = this.Range["A2", missing];
obj = New list(range1.Value2.ToString());

Related

C# - Exporting DataTable to Excel as Left-Justified (Left Aligned) columns

I am trying to export a datatable filled with numbers and letters with spacing before and after. I am using the below code to export - it works great. However, all columns are centered instead of being aligned to the left.
How do I adjust my below code to left-justify the excel sheet?
internal static void Export2Excel(DataTable dataTable, string RevisedFileName)
{
object misValue = System.Reflection.Missing.Value;
Microsoft.Office.Interop.Excel.Application _appExcel = null;
Microsoft.Office.Interop.Excel.Workbook _excelWorkbook = null;
Microsoft.Office.Interop.Excel.Worksheet _excelWorksheet = null;
try
{
// excel app object
_appExcel = new Microsoft.Office.Interop.Excel.Application();
// excel workbook object added to app
_excelWorkbook = _appExcel.Workbooks.Add(misValue);
_excelWorksheet = _appExcel.ActiveWorkbook.ActiveSheet as Microsoft.Office.Interop.Excel.Worksheet;
// Left align all cells - THIS DOES NOT WORK.
_excelWorksheet.get_Range("A1", "A1").Style.HorizontalAlignment = Microsoft.Office.Interop.Excel.XlHAlign.xlHAlignCenter;
// column names row (range obj)
Microsoft.Office.Interop.Excel.Range _columnsNameRange;
_columnsNameRange = _excelWorksheet.get_Range("A1", misValue).get_Resize(1, dataTable.Columns.Count);
// column names array to be assigned to _columnNameRange
string[] _arrColumnNames = new string[dataTable.Columns.Count];
for (int i = 0; i < dataTable.Columns.Count; i++)
{
// array of column names
_arrColumnNames[i] = dataTable.Columns[i].ColumnName;
}
// assign array to column headers range, make 'em bold
_columnsNameRange.set_Value(misValue, _arrColumnNames);
_columnsNameRange.Font.Bold = true;
// populate data content row by row
for (int Idx = 0; Idx < dataTable.Rows.Count; Idx++)
{
_excelWorksheet.Range["A2"].Offset[Idx].Resize[1, dataTable.Columns.Count].Value =
dataTable.Rows[Idx].ItemArray;
}
// Autofit all Columns in the range
_columnsNameRange.Columns.EntireColumn.AutoFit();
_excelWorkbook.SaveAs(#Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + "/" + RevisedFileName + " Revised.xlsx");
_excelWorkbook.Close();
System.Runtime.InteropServices.Marshal.ReleaseComObject(_appExcel);
}
catch { throw; }
MessageBox.Show("Success! Revised file has been saved to Desktop!");
}
I would guess the line
_excelWorksheet.get_Range("A1", "A1").Style.HorizontalAlignment = Microsoft.Office.Interop.Excel.XlHAlign.xlHAlignCenter;
ends with
....xlHAlignCenter
but should end something like
....xlHAlignLeft;

How to convert Object from UDF parameter to Excel Range in Excel-DNA?

I am developing a simple Addin using Excel-DNA. I have written a below function, but I am finding difficulties in converting it to a Range object. Tried googling and not able to figure out. Can someone please help me
[ExcelFunction(Description = "Excel Range")]
public static string Concat2([ExcelArgument(AllowReference = true)] object rng)
{
try
{
// Assuming i am calling this from Excel cell A5 as =Concat2(A1:A2)
var app = (Excel.Application)ExcelDnaUtil.Application;
var r = app.Range[rng, Type.Missing];
return r.Cells[1,1] + r.Cells[2,2]
}
catch (Exception e)
{
return "Error";
}
}
You should rather get the values directly from the input parameter, without getting the Range COM object. It's also much more efficient doing it that way.
Your simple function might then look like this:
public static object Concat2(object[,] values)
{
string result = "";
int rows = values.GetLength(0);
int cols = values.GetLength(1);
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
object value = values[i, j];
result += value.ToString();
}
}
return result;
}
Typically you'd want to check the type of the value object, and do something different based on that. The object[,] array passed from Excel-DNA could have items of the following types:
double
string
bool
ExcelDna.Integration.ExcelError
ExcelDna.Integration.ExcelEmpty
ExcelDna.Integration.ExcelMissing (if the function is called with no parameter, as =Concat2()).
If you change the signature to have a single parameter of type object (instead of object[,]), like this:
public static object Concat2(object value)
then, depending on how the function is called, you might get one of the above types as the value or you might get an object[,] array as the value, so your type checks would look a bit different before you do the iteration.
In my F# addin I have a function that does that (I use this function mainly to extract the displayed values of dates):
[<ExcelFunction(Description="Returns what is currently displayed as text.", IsMacroType=true)>]
let DISPLAYEDTEXT ([<ExcelArgument(Description="Cell", AllowReference=true)>] rng : obj) =
app().get_Range(XlCall.Excel(XlCall.xlfReftext, rng, true)).Text
where app is:
let app()= ExcelDnaUtil.Application :?> Excel.Application
How about this?
[ExcelFunction(IsMacroType = true)]
public static double GetBackColor([ExcelArgument(AllowReference=true)] object cell)
{
ExcelReference rng = (ExcelReference)cell;
Excel.Range refrng = ReferenceToRange(rng);
return refrng.Interior.Color;
}
and this is the helper function
private static Excel.Range ReferenceToRange(ExcelReference xlRef)
{
Excel.Application app = (Excel.Application)ExcelDnaUtil.Application;
string strAddress = XlCall.Excel(XlCall.xlfReftext, xlRef, true).ToString();
return app.Range[strAddress];
}

How to split excel rows into multiple equal lots and process per iteration

I'm processing a large excel (10k records) and it is a requirement that this process run on multiple threads to improve performance.
Right now i'm doing a check if row <= 2000 then that's fine run Utils.IxGenerateWithData with all records. But if row > 2000 (e.g. 10k) I want to split these into multiple threads that process Utils.IxGenerateWithData with 2000 records each.
Please help
using (Stream contentStream = await requestContent.ReadAsStreamAsync())
{
Workbook workbook = new Workbook(contentStream);
Worksheet worksheet = workbook.Worksheets[0];
int column = 0; // first column
Cell lastCell = worksheet.Cells.EndCellInColumn((short)column);
//Run on multiple threads if the file has more than 2000 records
if (lastCell.Row > 2000)
{
//Not sure what to do here
// Infiniti GenerateWithData Web Service
Thread thread = new Thread(() => Utils.IxGenerateWithData(payloadSettings.ProjectGUID, payloadSettings.DatasourceGUID, xmlContent, payloadSettings.InfinitiUsername, payloadSettings.InfinitiPassword, payloadSettings.ServiceWSDL));
thread.Start();
}
else
{
for (int row = 0; row <= lastCell.Row; row++)
{
Cell cell = worksheet.Cells.GetCell(row, column);
xmlContent += cell.StringValueWithoutFormat;
}
// Infiniti GenerateWithData Web Service
Utils.IxGenerateWithData(payloadSettings.ProjectGUID, payloadSettings.DatasourceGUID, xmlContent, payloadSettings.InfinitiUsername, payloadSettings.InfinitiPassword, payloadSettings.ServiceWSDL);
}
}
A good start would be to determine how many threads you want to start. If you are going on 2000 rows per thread, then threadCount would be calculated as follows:
var threadCount = (lastCell.Row / 2000) + 1;
The 1 is added to ensure that the thread will never have more than 2000 rows but it can have less.
Then calculate rowsPerThread as follows:
var rowsPerThread = lastCell.Row / threadCount;
Lastly have a for loop to start the threads passing it the array of rows that it should process. Here I would create a class that are created in the for loop, and the rows it need to process is passed through in the constructor. Then have a Start method that starts a thread to process the rows in the object.
An outline of such a class would look as follows:
public class ExcelRowProcessor()
{
private List<ExcelRow> _rows = new List<ExcelRow>();
public ExcelRowProcessor(IEnumerable<ExcelRow> rows)
{
_rows.AddRange(rows);
}
public void Start()
{
// Start the thread here.
}
}
I hope this helps.
Sorry to make this a new answer, but I don't have the reputation yet to be able to post on Jaco's yet.
Anyways, in general you don't want to determine the number of threads based on workload/bucketsize. It is better to determine the bucket size based on the number of CPU Cores. This is in order to prevent thread switching, also allowing one core for the OS/Virus scanner can help as well.
To get thread/core/process count ... see this post: How to find the Number of CPU Cores via .NET/C#?
var threadCount = cpuCoreCount - 1; //TODO: use code from above URL
if (0 == threadCount) {
threadCount = 1;
}
var rowsPerThread = lastCell.Row / threadCount; // As Jaco posted
So back to your question about how to thread:
using (Stream contentStream = await requestContent.ReadAsStreamAsync())
{
Workbook workbook = new Workbook(contentStream);
Worksheet worksheet = workbook.Worksheets[0];
int column = 0; // first column
Cell lastCell = worksheet.Cells.EndCellInColumn((short)column);
List<IAsyncResult> asyncResults = new List<IAsyncResult>();
string xmlContent = ""; // assuming this is local
for (int row = 0; row <= lastCell.Row; row++)
{
Cell cell = worksheet.Cells.GetCell(row, column);
xmlContent += cell.StringValueWithoutFormat;
if (((row > 0) && (row % rowsPerThread == 0)) || (rows == lastCell.Row))
{
var caller = new GenerateDelegate(Generate);
asyncResults.Add(caller.BeginInvoke(xmlContent, null, null));
xmlContent = "";
}
}
// Wait for the threads
asyncResults.ForEach(result => {
while(result.IsCompleted == false) {
Thread.Sleep(250);
}
});
}
Place this code outside the function
private delegate void GenerateDelegate(string xmlContent);
///<summary>
/// Call Infiniti GenerateWithData Web Service
///<summary>
private void Generate(string xmlContent)
{
Utils.IxGenerateWithData(payloadSettings.ProjectGUID, payloadSettings.DatasourceGUID, xmlContent, payloadSettings.InfinitiUsername, payloadSettings.InfinitiPassword, payloadSettings.ServiceWSDL);
}

NPOI - Get excel row count to check if it is empty

I'm reading an xlsx file using NPOI lib, with C#. I need to extract some of the excel columns and save the extracted values into some kind of data structure.
I can successfully read the file and get all the values from the 2nd (the first one contains only headers) to the last row with the following code:
...
workbook = new XSSFWorkbook(fs);
sheet = (XSSFSheet)workbook.GetSheetAt(0);
....
int rowIndex = 1; //--- SKIP FIRST ROW (index == 0) AS IT CONTAINS TEXT HEADERS
while (sheet.GetRow(rowIndex) != null) {
for (int i = 0; i < this.columns.Count; i++){
int colIndex = this.columns[i].colIndex;
ICell cell = sheet.GetRow(rowIndex).GetCell(colIndex);
cell.SetCellType(CellType.String);
String cellValue = cell.StringCellValue;
this.columns[i].values.Add(cellValue); //--- Here I'm adding the value to a custom data structure
}
rowIndex++;
}
What I'd like to do now is check if the excel file is empty or if it has only 1 row in order to properly handle the issue and display a message
If I run my code against an excel file with only 1 row (headers), it breaks on
cell.SetCellType(CellType.String); //--- here cell is null
with the following error:
Object reference not set to an instance of an object.
I also tried to get the row count with
sheet.LastRowNum
but it does not return the right number of rows. For example, I have created an excel with 5 rows (1xHEADER + 4xDATA), the code reads successfully the excel values. On the same excel I have removed the 4 data rows and then I have launched again the code on the excel file. sheet.LastRowNum keeps returning 4 as result instead of 1.... I think this is related to some property bound to the manually-cleaned sheet cells.
Do you have any hint to solve this issue?
I think it would be wise to use sheet.LastRowNum which should return the amount of rows on the current sheet
Am I oversimplifying?
bool hasContent = false;
while (sheet.GetRow(rowIndex) != null)
{
var row = rows.Current as XSSFRow;
//all cells are empty, so is a 'blank row'
if (row.Cells.All(d => d.CellType == CellType.Blank)) continue;
hasContent = true;
}
You can retrieve the number of rows using this code:
public int GetTotalRowCount(bool warrant = false)
{
IRow headerRow = activeSheet.GetRow(0);
if (headerRow != null)
{
int rowCount = activeSheet.LastRowNum + 1;
return rowCount;
}
return 0;
}
Here is a way to get both the actual last row index and the number of physically existing rows:
public static int LastRowIndex(this ISheet aExcelSheet)
{
IEnumerator rowIter = aExcelSheet.GetRowEnumerator();
return rowIter.MoveNext()
? aExcelSheet.LastRowNum
: -1;
}
public static int RowsSpanCount(this ISheet aExcelSheet)
{
return aExcelSheet.LastRowIndex() + 1;
}
public static int PhysicalRowsCount(this ISheet aExcelSheet )
{
if (aExcelSheet == null)
{
return 0;
}
int rowsCount = 0;
IEnumerator rowEnumerator = aExcelSheet.GetRowEnumerator();
while (rowEnumerator.MoveNext())
{
++rowsCount;
}
return rowsCount;
}

how do you get Excel formulas from a range using ExcelDNA?

I want to pass a range (1d at this point) into my function, and return an array of strings which contain the formulas of the range.
Here's my (not working) code so far:
public static object[,] ReadFormulas([ExcelArgument(AllowReference=true)]object arg)
{
ExcelReference theRef = (ExcelReference)arg;
object[,] o = (object[,])theRef.GetValue();
string[,] res = new string[o.GetLength(1),1];
for(int i=0;i<o.GetLength(1);i++)
{
ExcelReference cellRef = new ExcelReference(theRef.RowFirst+i, theRef.ColumnFirst);
res[i,0] = XlCall.Excel(XlCall.xlfGetFormula, cellRef) as string; //Errors here
}
return res;
}
The GET.FORMULA (xlfGetFormula) function is allowed on macro sheets only. To call it from a worksheet, your Excel-DNA function should be marked as IsMacroType=true, like this:
[ExcelFunction(IsMacroType=true)]
public static object[,] ReadFormulas(
[ExcelArgument(AllowReference=true)]object arg) {...}
Also, you need to be a bit careful when constructing the new ExcelReference in your loop. By default, the sheet referred to in the reference will be the current sheet, and not the sheet of the passed in reference. You should probably pass the SheetId into the new ExcelReference explicitly. There's also something funny with your indexing - perhaps the o.GetLength(1) is not what you intend.
The following version seemed to work:
[ExcelFunction(IsMacroType=true)]
public static object[,] ReadFormulasMacroType(
[ExcelArgument(AllowReference=true)]object arg)
{
ExcelReference theRef = (ExcelReference)arg;
int rows = theRef.RowLast - theRef.RowFirst + 1;
object[,] res = new object[rows, 1];
for(int i=0; i < rows; i++)
{
ExcelReference cellRef = new ExcelReference(
theRef.RowFirst+i, theRef.RowFirst+i,
theRef.ColumnFirst,theRef.ColumnFirst,
theRef.SheetId );
res[i,0] = XlCall.Excel(XlCall.xlfGetFormula, cellRef);
}
return res;
}

Categories