One of +60 columns I pass to Excel is a hyperlink
private object GetApplicationUrl(int id)
{
var environmentUri = _configurationManager.Default.AppSettings["EnvironmentUri"];
return $"=HYPERLINK(\"{environmentUri}://TradingObject={id}/\", \"{id}\");";
}
this resolves external protocol and opens particular deal in another application. This works, but initially, the hyperlink formula is unevaluated, forcing the users to evaluate it first. I use object array:
protected void SetRange(object[,] range, int rows, int columns, ExcelCell start = null)
{
start = start ?? GetCurrentCell();
ExcelAsyncUtil.QueueAsMacro(
() =>
{
var data = new ExcelReference(start.Row, start.Row + (rows - 1), start.Column, start.Column + (columns - 1));
data.SetValue(range);
});
}
How can I force the excel to evaluate it?
I tried several things, but they did not work. For example:
return XlCall.Excel(
XlCall.xlcCalculateNow,
$"HYPERLINK(\"{environmentUri}://TradingObject={id}/\", \"{id}\")");
maybe I am using an incorrect flag or missing something.
I found out several ways to fix it, but not all of them worked as I expected. In short, I came up with calling the cell formula value instead.
private void SetFormulaColumn(List<int> ids, ExcelCell cell)
{
var wSheet = ((Application)ExcelDnaUtil.Application).ActiveWorkbook.ActiveSheet;
for (var i = 0; i < ids.Count; i++)
{
wSheet.Cells(cell.Row + i, cell.Column).Formula = GetApplicationUrl(ids[i]);
}
}
It is probably slightly less performant but works well enough to be considered as a valid solution.
Related
I have seen many posts about finding the last row of a given column for Google Sheets API v4 in C#, but I can't seem to find anything about finding the last column of a given row. I didn't find any questions about this specifically - but if I'm mistaken please direct me to the right place.
In my sheet, I have headers for each column. Over time, I anticipate I will need to add or remove columns as needed - it would be great to not have to update my code every time this happens.
I'm at the beginning stages of writing my code that gathers my data from Google Sheets - but here is what I have so far. I know that I will need to change the way my variable "range" is written, just don't know what.
static void ReadEntries()
{
var range = $"{sheet}!A1:ET";
var request = service.Spreadsheets.Values.Get(SpreadsheetId, range);
var response = request.Execute();
var values = response.Values;
if(values != null && values.Count>0)
{
foreach (var row in values)
{
System.Diagnostics.Debug.WriteLine("{0} | {1} | {2}", row[0], row[1], row[2]);
}
}
else
{
System.Diagnostics.Debug.WriteLine("No data found.");
}
}
EDIT: SOLVED
I used the pseudo code provided by Nazi A for this. I was having issues with the if(row[col]) piece with casting and other system exceptions. It turns out foreach allows for us to not have to check if that row[col] is in range. Below is my final code in case anyone needs it in the future. I plan to let column "ET" declared in var range = $"{sheet}!A1:ET; be big enough to accommodate any future columns being added to my spreadsheet. Thanks for your help!
static void ReadEntries()
{
var range = $"{sheet}!A1:ET";
var request = service.Spreadsheets.Values.Get(SpreadsheetId, range);
var response = request.Execute();
var values = response.Values;
int max = 0;
int currMax;
if (values != null && values.Count>0)
{
foreach(var row in values)
{
currMax = 0;
foreach(var col in row)
{
currMax++;
}
if (max < currMax)
{
max = currMax;
}
}
}
else
{
System.Diagnostics.Debug.WriteLine("No data found.");
}
System.Diagnostics.Debug.WriteLine(max);
}
So basically, you need to have a nested loop to traverse all rows and columns in values.
I have tested this psuedo code and worked but since I have no any means to run a C# code, this is all I can give to you. This is a pseudo code that should be readable to you.
var max = 0;
foreach(var row in values){
var currMax = 0;
foreach(var col in row){
if(row[col]){ // as long as data exists, currMax will increment
currMax++;
continue;
}
break; // stop loop if last cell being checked is empty
}
if(max < currMax){ // assign the largest currMax to max
max = currMax;
}
}
So in this psuedo code, max will contain the value of the largest column of all rows in the range. this code above should replace your foreach call
If you have any questions, feel free to clarify below.
I wrote a small method that will give me the headers of a table in excel:
private List<string> GetCurrentHeaders(int headerRow, Excel.Worksheet ws)
{
//List where specific values get saved if they exist
List<string> headers = new List<string>();
//List of all the values that need to exist as headers
var headerlist = columnName.GetAllValues();
for (int i = 0; i < headerlist.Count; i++)
{
//GetData() is a Method that outputs the Data from a cell.
//headerRow is defining one row under the row I actually need, therefore -1 )
string header = GetData(i + 1, headerRow - 1, ws);
if (headerlist.Contains(header) && !headers.Contains(header))
{
headers.Add(header);
}
}
return headers;
}
Now I got an Excel-table, where the first value I need is in cell A11 (or Row 11, Column 1).
When I set a breakpoint after string header = GetData(i + 1, headerRow - 1, ws);, where i+1 = 1 and headerRow - 1 = 11, I can see that the value he read is empty, which is not the case.
The GetData-Method just does one simple thing:
public string GetData(int row, int col, Excel.Worksheet ws)
{
string val = "";
val = Convert.ToString(ws.Cells[row, col].Value) != null
? ws.Cells[row, col].Value.ToString() : "";
val = val.Replace("\n", " ");
return val;
}
I don't get why this can't get me the value I need, while it works on every other excel table too. The excel itself is no different from the others. It's file extension is .xls, the data is in the same layout as in the other tables, etc
There are a few steps to getting this right. You need to know the dimensions of your table to know where the headers are. Your method hast two ways of knowing this: 1) passing the table Range to the method, or 2) giving the coordinates of a cell within the table (usually the top-left cell) and trusting the CurrentRegion property to do the job for you. The most reliable way would be the first as you will be explicitly telling the method where to look, but it'll require the consumer to figure out the address which isn't always straightforward. The CurrentRegion approach works fine too but note that if you have an empty column within your table range, it will only address until that empty column. Having said all that, you could have the following:
List<string> GetHeaders(Worksheet worksheet, int row, int column)
{
Range currentRegion = worksheet.Cells[row, column].CurrentRegion;
Range headersRow = currentRegion.Rows[1];
var headers = headersRow
.Cast<Range>() // We cast so we can use LINQ
.Select(c => c.Text is DBNull ? null : c.Text as string) //The null value of c.Text is not null but DBNull
.ToList();
return headers;
}
Then you can simply test if you're missing headers. The following code assumes the ActiveCell is a cell within the table Range, but you can change that easily to address a specific cell.
List<string> GetMissingHeaders(List<string> expectedHeaders)
{
var worksheet = App.ActiveSheet; //App is your Excel application
Range activeCell = worksheet.ActiveCell;
var headers = GetHeaders(worksheet, activeCell.Row, activeCell.Column);
return expectedHeaders.Where(h => headers.Any(i => i == h) == false).ToList();
}
I am writing a program that accesses an excel template containing columns of data (with an unique ID number in the first column). Based on the first two numbers of the ID number, the row will either be kept or deleted. In the template, this unique ID number column feeds an ActiveX Combobox's (located on the Worksheet) ListFill attribute. When the non-matching rows are removed, the ListFill attribute is reset, but the text is not reset.
Example, if I select rows based on '02' being the first two numbers of the unique ID in Column A, I have no problem removing everything that does not start with '02' but the Combobox text still reads "010001" since that is the first Unique ID in the template, even though it doesn't exist in the new list.
I tell you all this to ask if anyone knows a better way to access the combobox? I can access it as an OLEObject, but that does not allow me to change the index or text properties of the combobox as they are 'read only' as per the following intellisense error in VS 2013:
Property or Indexer 'Microsoft.Office.Interop.Excel_OLEObject.Index' cannot be assinged to -- it is read only.
The error appears on the line:
oleobj.Index = 1;
The code snippet is below. The current Excel application is passed as xlApp and the array comboboxes is passed. Each member of the comboboxes array contains the sheet name the combobox is on, the name of the control and the ListFillRange it has on the template. Example array member would be:
Sheet1!:cbTest:$A$1:$A$10
private void ResetComboBoxes2(string[] comboboxes, Excel.Application xlApp)
{
Excel.Worksheet wksht = new Excel.Worksheet();
Excel.Range rng;
int listEndCellNum;
string listEndCellApha;
string listEndCell;
for (int i = 0; i < comboboxes.Length; i++)
{
string[] comboBoxesSplit = comboboxes[i].Split(':');
string sheetName = comboBoxesSplit[0].ToString();
string oleObjName = comboBoxesSplit[1].ToString();
string[] rangeArray = comboBoxesSplit[2].Split(':');
string rangeStart = rangeArray[0];
listEndCellNum = wksht.Range[rangeStart].End[Excel.XlDirection.xlDown].Offset[1, 0].Row - 1;
string[] cellBreakdown = rangeStart.Split('$');
listEndCellApha = cellBreakdown[1];
listEndCell = "$" + listEndCellApha + "$" + listEndCellNum;
string listFull = rangeStart + ":" + listEndCell;
wksht = xlApp.ActiveWorkbook.Worksheets[sheetName];
foreach (Excel.OLEObject oleobj in wksht.OLEObjects())
{
if (oleobj.Name.ToString() == oleObjName)
{
oleobj.ListFillRange = listFull;
oleobj.Index = 1;
}
}
}
}
I'm not even sure there IS a way to do this properly. I could always make a chunk of VBA code to reset it before saving and access that through C# but I am hoping to do it here.
So I was able to figure out that I was doing too much thinking. I went back to VBA then transposed that back to C#. The result was the following code, which yu will notice is considerably shorter and succinct. I had to test the oleObject's programID which for ALL activeX comboboxes is "Forms.ComboBox.1" then grab that object's name, then call it by name, with an extra "Object" in there for good measure.
private void ResetComboBoxes2(string[] comboboxes, Excel.Application xlApp)
{
Excel.Worksheet wksht = new Excel.Worksheet();
for (int i = 0; i < comboboxes.Length; i++)
{
string[] comboBoxesSplit = comboboxes[i].Split(':');
string sheetName = comboBoxesSplit[0].ToString();
wksht = xlApp.ActiveWorkbook.Worksheets[sheetName];
foreach (Excel.OLEObject oleobj in wksht.OLEObjects())
{
if (oleobj.progID == "Forms.ComboBox.1")//oleobj.Name.ToString() == oleObjName)
{
string cbName = oleobj.Name.ToString();
wksht.OLEObjects(cbName).Object.ListIndex = 0;
}
}
}
}
can anyone help me?
I have a structure
public struct Data
{
public string aaaAAA;
public string bbbBBB;
public string cccCCC;
...
...
}
then some code to bring in a data into a List, creaitng new list etc.
I want to then transport this to excel which I have done like this,
for (int r = 0; r < newlist.Count; r++)
{
ws.Cells[row,1] = newlist[r].aaaAAA;
ws.Cells[row,2] = newlist[r].bbbBBB;
ws.Cells[row,3] = newlist[r].cccBBB;
}
This works, but it is painfully slow. I am inputting over 12,000 rows and my structure has 85 elements (so each row has 85 columns of data).
Can anyone help make this quicker??
Thanks,
Timujin
If as #juharr mentioned you are able to use OpenXML, look at the ClosedXML library for creating Excel documents, found here.
Using your example above you could then use the following code:
var wb = new XLWorkbook();
var ws = wb.Worksheets.Add("Data_Test_Worksheet");
ws.Cell(1, 1).InsertData(newList);
wb.SaveAs(#"c:\temp\Data_Test.xlsx");
If you require a header row, then you would just have to add those manually, using something like the below(Then you would start inserting your rows above from Row 2):
PropertyInfo[] properties = newList.First().GetType().GetProperties();
List<string> headerNames = properties.Select(prop => prop.Name).ToList();
for (int i = 0; i < headerNames.Count; i++)
{
ws.Cell(1, i + 1).Value = headerNames[i];
}
On the performance requirement, this seems to be more performant than iterating through the array. I have done some basic testing on my side and to insert 20 000 rows for sample object containing 2 properties, it took a total of 1 second.
I am currently using one button for inserting/updating content within a table. It then takes the uploaded CSV and inserts or updates it into a data table depending on whether the row exists or not.
Here is the code fired after the button's OnClick:
if (ExcelDDL.SelectedValue == "Time Points" && fileName == "TimePoints.csv")
{
var GetTPoints = (SEPTA_DS.TimePointsTBLDataTable)tpta.GetDataByCategory(CategoryDDL.SelectedItem.ToString());
//Loop through each row and insert into database
int i = 0;
foreach (DataRow row in TempRouteDataTable.Rows)
{
//Gather column headers
var category = Convert.ToString(CategoryDDL.SelectedItem);
var agency = Convert.ToString(row["Agency"]);
if (agency == null || agency == "")
{
//If row is empty skip it entirely
goto skipped;
}
var route = Convert.ToString(row["Route"]);
var GetRShortName = (SEPTA_DS.RoutesTBLDataTable)rta.GetDataByRouteID(route);
var newRoute = "";
if (GetRShortName.Rows.Count > 0)
{
newRoute = Convert.ToString(GetRShortName.Rows[0]["route_short_name"]);
}
var direction = Convert.ToString(row["Direction"]);
var serviceKey = Convert.ToString(row["Service Key"]);
var language = Convert.ToString(row["Language"]);
var stopID = Convert.ToString(row["Stop ID"]);
var stopName = Convert.ToString(row["Stop Name"]);
if (stopName.Contains("accessible"))
{
string[] noHTML = stopName.Split('>');
int insertH = Convert.ToInt32(hta.InsertHandicapRow(newRoute,noHTML[2]));
}
var sequence = Convert.ToString(row["Sequence"]);
var origID = -1;
if (GetTPoints.Rows.Count > 0)
{
origID = Convert.ToInt32(GetTPoints.Rows[i]["TPointsID"]);
var GetID = (SEPTA_DS.TimePointsTBLDataTable)tpta.GetDataByID(origID);
if (GetID.Rows.Count < 1)
{
origID = -1;
}
}
if (origID == -1)
{
int insertData = Convert.ToInt32(tpta.InsertTimePoints(category, agency, newRoute, direction, serviceKey, language, stopID, stopName, sequence));
}
else
{
int updateData = Convert.ToInt32(tpta.UpdateTimePoints(category, agency, newRoute, direction, serviceKey, language, stopID, stopName, sequence, origID));
}
skipped:
i++;
}
}
You can see how I check whether to insert or update around the bottom. I am using this method across other sections of this program and it works just fine. But in this case it is distorting my datatable immensely and I can't figure out why.
This is the bottom part of my table after inserting [no items currently within the database]:
This is the table after reuploading the CSV with data already existing within the table:
I am also getting this error when updating There is no row at position 2230.
What is going wrong in the code to cause this huge shift? I am just checking to see if the ID exists and if it does update rather than insert.
Also the reason i am using goto is because there are blank rows in the document that need to be skipped.
Is your TPointsID column, a auto-generated number? If so, since you are skipping the empty row, some referential integrity problem might be occuring,because of empty data in the skipped rows in the database.
From the error : There is no row at position 2230 , it is also understood that, because of the skipping you might be trying to access some non existent row in the datatable.
Also, if possible consider using the ADO.NET DataAdapter which has got the CRUD operation capability. You can find more about it at : http://support.microsoft.com/kb/308507