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;
}
Related
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];
}
I'm having a strange issue while using the MLApp.GetWorkspaceData function. I noticed that this functions works properly when I do the following:
matlab = new MLApp.MLAppClass();
object myObject;
matlab.GetWorkspaceData("myVariable", "base", out myObject);
But if I then try to use the same object as an output I get an "Invalid Callee" exception. In addition this also gives the same error:
matlab = new MLApp.MLAppClass();
object myObject = new object();
matlab.GetWorkspaceData("myVariable", "base", out myObject);
This is very troublesome because I need to get a large amount of data from Matlab to Visual Studio, and I cannot practically create 52K uninitialized variables and keep them around. Is there some way to "uninitialize" a variable? Is there some concept I'm missing here?
As #wonko79 explained in the comments, if you want to reuse the out variable, you should set it to null first.
Here is a tested example calling MATLAB from C#:
using System;
namespace CSharp_matlab_com
{
class Program
{
static void Main(string[] args)
{
MLApp.MLAppClass matlab = new MLApp.MLAppClass();
// create variables: a_0, a_1, ..., a_4
for (int k = 0; k < 5; k++) {
matlab.Execute(string.Format("a_{0} = rand(2);", k));
}
// retrieve variables from MATLAB and print their contents
object a;
for (int k = 0; k < 5; k++) {
// current variable name
string varname = string.Format("a_{0}", k);
// get data array
a = null; // without this line, an exception is thrown!
matlab.GetWorkspaceData(varname, "base", out a);
// print contents
var arr = (double[,]) a;
Console.WriteLine("\nndims(a) = {0}, numel(a) = {1}", arr.Rank, arr.Length);
for (int i = 0; i < arr.GetLength(0); i++) {
for (int j = 0; j < arr.GetLength(1); j++) {
Console.WriteLine("{0}[{1},{2}] = {3}", varname, i, j, arr[i,j]);
}
}
}
}
}
}
The output:
ndims(a) = 2, numel(a) = 4
a_0[0,0] = 0.251806122472313
a_0[0,1] = 0.617090884393223
a_0[1,0] = 0.290440664276979
a_0[1,1] = 0.265280909810029
...
ndims(a) = 2, numel(a) = 4
a_4[0,0] = 0.425259320214135
a_4[0,1] = 0.16148474431175
a_4[1,0] = 0.312718886820616
a_4[1,1] = 0.178766186752368
You can create a wrapper for GetWorkspaceData method, like in the next example:
public object GetData(string name)
{
object data;
mlApp.GetWorkspaceData(name, "base", out data);
return data;
}
Or, even more useful, a generic wrapper:
public T GetData<T>(string name)
{
object data;
mlApp.GetWorkspaceData(name, "base", out data);
if (data == null)
return default(T);
if (data is T)
return (T)data;
else
throw new InvalidCastException($"The variable '{name}', of type '{data.GetType().Name}' cannot be casted to type '{typeof(T).Name}'.");
}
The solution is to set the output object to null.
I found it here.
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());
Hope you can help.
I'm looking for a way to build an excel sheet range by just passing in some parameters. My method is as follows:
private void TableCellformatting(int rowStart, int rowEnd, char colStart, char colEnd)
{
char temp;
StringBuilder s = new StringBuilder();
excelSheetRange = null;
for (int i = rowStart; i <= rowEnd; i++)
{
temp = colStart;
while (temp <= colEnd)
{
if (s.Length > 0)
s.Append(", ");
s.Append(string.Format("{0}{1}", temp.ToString(), i));
temp++;
}
}
excelSheetRange = excelSheet.get_Range(s.ToString());
excelSheetRange.HorizontalAlignment = Microsoft.Office.Interop.Excel.XlHAlign.xlHAlignCenter;
}
This runs ok until the excelSheet.get_Range part is called. When I pass in the cell numbers manually, I have to do it like this:
get_Range("A1", "A2" etc...)
Any ideas if this is even possible, or if there is another way?
Thanks!
Use worksheet.range property.
excelSheetRange = excelSheet.Range[excelsheet.Cells[rowstart, colstart], excelsheet.Cells[rowend, colend]];
In a program I'm writing, I've decided to use my own data types instead of any other database ( for educational purposes).
The data is saved as csv files, and i have a method to convert from .csv to a simple string[,].
Each class X as it's own string[,] to List converter.
After the first 3 classes i started to play with generics and reflection.
I did succeed converting a general List to string[,], but doing the opposite is started to look hard.
the way i implement List to string[,] is:
public string[,] ToArray(object[] sender)
{
if (sender.Length==0)
{
return null;
}
string[,] ret;
PropertyInfo[] props;
if (sender.Length > 0)
{
props = sender[0].GetType().GetProperties();
ret = new string[props.Length, sender.Length];
}
else
{
return null;
}
for (int i=0;i<sender.Length;i++)
{
for (int p = 0; p < props.Length; p++)
{
object value=props[p].GetValue(sender[i], null);
if (value!=null) ret[p, i] = value.ToString();
}
}
return ret;
}
and for lets say class Windows( string Name, double Size, bool Blinds)
i convert array[,] to Windows ( very generally ) like this :
public static List<Windows> ToList(string[,] arra)
{
List<Windows> ret = new List<Windows>(); // change Windows to anything
int col = array.GetLength(1);
int row = array.GetLength(0);
PropertyInfo[] props=PropArray(new Windows());
int propslen=props.Length;
for (int c = 0; c < col; c++)
{
Windows entry=new Windows();
for (int r = 0; r < propslen; r++)
{
Type pt = props[r].PropertyType;
if (pt==typeof(string))
props[r].SetValue(entry,array[r,c],null);
else
if (pt==typeof(int))
{
int i=0;int.TryParse(array[r,c],out i);
props[r].SetValue(entry,i,null);
}
else
if (pt==typeof(bool))
{
bool i = false; bool.TryParse(array[r, c], out i);
props[r].SetValue(entry,i,null);
}
else
if (pt == typeof(double))
{
double i = 0; double.TryParse(array[r, c], out i);
props[r].SetValue(entry, i, null);
}
else
if (pt==typeof(DateTime))
{
DateTime i = DateTime.MinValue; DateTime.TryParse(array[r, c], out i);
props[r].SetValue(entry,i,null);
}
}
ret.Add(entry);
}
return ret;
}
All i need to do is FindReplace the word "Windows" to any other datatype and it works.
The real question is how can i generalize it to receive a type and make a list of instances it all by itself?
Is it even possible?
Thanks in advance,
Gabriel
I will not be foolproof, but you can use generics. Something like:
public static List<T> ToList<T>(string[,] arra) // T can be anything
where T : new() // as long as it has a default constructor
{
List<T> ret = new List<T>();
int col = array.GetLength(1);
int row = array.GetLength(0);
PropertyInfo[] props=PropArray(new T());
int propslen=props.Length;
for (int c = 0; c < col; c++)
{
T entry=new T();
// ...
What you are doing is essentially a serialization format. Serialization formats are relatively simple if objects are values without complex references. Simple xml or json are common formats. Perhaps you should look into using json?
If you go for "roll your own", why not try to use property names as keys? Prefix the key/value list with the class name and instantiate the objects when you deserialize. Example format
MyApplication.Person, MyAssembly
Name=Steve
Age=30
MyApplication.Person, MyAssembly
Name=Bob
Age=54
To deserialize, read the class name, instatiate using FormatterServices.GetUninitializedObject() to get an empty instance of the class. Then using the property infos of that class, set the values from the key/value pairs that follow in the file. This is essentially a simplified JSON notation (does not support lists,maps etc.)