I'm trying to use EPPlus to create a table on a worksheet. I can create the table, but all my # variables become #Ref! when opening up the file. If I paste the exact same formula into Excel it takes it and has no problem. What am I missing here? Do I need to apply the table somehow after creating it?
Thanks,
Lee
private void ProcessVehicleData(BorrowingBase bbData, ExcelWorksheet ew, int colStart, int rowStart) {
int origFirstRow = rowStart;
foreach (DailyCAPS data in bbData.DailyCAPS) {
FillRow(ew, data, colStart, rowStart);
++rowStart;
}
try {
ExcelAddressBase eab = new ExcelAddressBase(origFirstRow - 1, ExcelColumnNameToNumber("A"), rowStart - 1, ExcelColumnNameToNumber("Y"));
ExcelTable et = ew.Tables.Add(eab, "VehicleData");
if (origFirstRow != rowStart) {
ew.Cells[origFirstRow, ExcelColumnNameToNumber("Y")].Formula = "=IF([#Inventory Days]>210,\"H\",IF([#TitleApp]+[#UtahTitleReceived]=0,\"B\",\"\"))";
}
}
catch { }
}
See comments for answer...github.com/JanKallman/EPPlus/issues/521
No,epplus can't do it.
Because epplus Tables.Add is only pure data fill not workbook query,so =[#XXX] is not work.
Related
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.
I get the following error when calling the package.Save():
Table Table1 Column Type does not have a unique name
I gave the table a name, ensured that any null cells have a default empty type, but still cant find where its going wrong or how I can set the Column Type name that is not unique.
Here's the code I use:
public static bool ConvertToXlsx(string csvFilePath)
{
bool success = false;
//we need an xlsx file path for the export and need to ensure the passed in file path is a CSV one
var xlsxFilePath = Path.ChangeExtension(csvFilePath, "xlsx");
csvFilePath = Path.ChangeExtension(csvFilePath, "csv");
//convert the csv
if (!string.IsNullOrWhiteSpace(xlsxFilePath) && !string.IsNullOrWhiteSpace(csvFilePath))
{
try
{
using (ExcelPackage package = new ExcelPackage(new FileInfo(xlsxFilePath)))
{
//add a/another worksheet with datetime value so it doesn't clash with existing worksheets to the document
ExcelWorksheet worksheet = package.Workbook.Worksheets.Add("Export_" + DateTime.Now.ToString("yyyy-dd-M--HH-mm-ss"));
//starting from cell A1, load in the CSV file data with first row as the header
worksheet.Cells["A1"].LoadFromText(
new FileInfo(csvFilePath), new ExcelTextFormat
{
Delimiter = ','
}, OfficeOpenXml.Table.TableStyles.Light1, true);
worksheet.Cells[worksheet.Dimension.Address].AutoFitColumns();
foreach (var cell in worksheet.Cells)
{
if (cell.Value == null)
{
cell.Value = "";
}
}
//save as xlsx
package.Save();
success = true;
}
//attempt to delete the previously generated CSV file
File.Delete(csvFilePath);
}
catch (Exception ex)
{
//if we cant delete the origionaly generated CSV file (used for the conversion), then return true
if (ex.Message.Contains($"Access to the path '{csvFilePath}' is denied."))
{
return true;
}
Console.WriteLine("Error: " + ex.Message);
return false;
}
}
return success;
}
The error message is a little bit unclear: it's actually saying "The Table 'Table1', Column 'Type' does not have a unique name". See here what happens.
LoadFromText will provoke the creation of a Table inside your worksheet, it is called "Table1". You say "I gave the table a name", well I'd say you didn't. EPPlus named the table "Table1". It happens here. You named the worksheet, the table was named automatically.
Check the first line of your source csv, it probably contains the word "Type" more than once. You might want to modify its contents slightly before passing to epplus (check for duplicates), or check out some of the other overloads for not using the first line as a header when importing text.
I faced the same problem and solved is as follows
public class ProductModel
{
[DisplayName("Name")]
public string ProductName {get; set;}
[DisplayName("Price")]
public decimal Price {get; set;}
[DisplayName("Price")]
public string Category {get ;set;}
}
[DisplayName("Price")]-->duplicate
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;
}
}
}
}
For example, I have a sheet called EmployeeSheet, which is just a single column of every employee's name first and last in a company. And let's assume this list is perfectly formatted and has no duplicates so every cell is unique in this sheet.
Now I have a sheet for each department in the company, such as FinanceSheet, ITSheet, and SalesSheet. Each sheet has in it somewhere (as in each sheet doesn't have the same layout) a list of employees in each department. However any 1 employee name should only appear once between all of the department sheets (this excludes the EmployeeSheet).
Here's the solution I can think of but not figure out how to implement, would be to make a multidimensional array (Learned a small bit about them in school, vaguely remember how to use though).
Pseudocode something like:
arrEmployees = {"Tom Hanks", "Burt Reynolds", "Your Mom"}
arrFinance = {"Tom Hanks"}
arrIT = {"Burt Reynolds"}
arrSales = {"Your Mom"}
arrSheets = {arrEmployees, arrFinance, arrIT, arrSales}
While I've been able to get single cell values and ranges as strings by using
Sheets shts = app.Worksheets;
Worksheet ws = (Worksheet)sheets.get_Item("EmployeeSheet");
Excel.Range empRange = (Excel.Range)worksheet.get_range("B2");
string empVal = empRange.Value2.ToString();
But with that process to get a single cell value to a string, I don't know how I would put that into an element of my array, let alone a range of values.
I'm sure my method is not the most efficient, and it might not even be possible, but that's why I'm here for help, so any tips are appreciated.
EDIT: This is the solution that ended up working for me. Thanks to Ian Edwards solution.
Dictionary<string, List<Point>> fields = new Dictionary<string, List<Point>>();
fields["Finance"] = new List<Point>() { new Point(2,20)};
fields["Sales"] = new List<Point>();
for (int row = 5; row <= 185; row += 20) {fields["Sales"].Add(new Point(2,row));}
List<string> names = new List<string>();
List<string> duplicates = new List<string>();
foreach (KeyValuePair<string, List<Point>> kp in fields)
{
Excel.Worksheet xlSheet = (Excel.Worksheet)workbook.Worksheets[kp.Key];
foreach (Point p in kp.Value)
{
if ((xlSheet.Cells[p.Y, p.X] as Excel.Range.Value != null)
{
string cellVal = ((xlSheet.Cells[p.Y,p.X] as Excel.Range).Value).ToString();
if (!names.Contains(cellVal))
{ names.Add(cellVal)) }
else { duplicates.Add(cellVal); } } } }
Here's a little example I knocked together - the comments should explain what's going on line by line.
You can declare the name of the worksheets you want to check for names, as well as where to start looking for names in the 'worksheets' dictionary.
I assume you don't know how many names are in each list - it will keep going down each list until it encounters a blank cell.
// Load the Excel app
Microsoft.Office.Interop.Excel.Application xlApp = new Microsoft.Office.Interop.Excel.Application();
// Open the workbook
var xlWorkbook = xlApp.Workbooks.Open("XLTEST.xlsx");
// Delcare the sheets and locations to look for names
Dictionary<string, Tuple<int, int>> worksheets = new Dictionary<string, Tuple<int, int>>()
{
// Declare the name of the sheets to look in and the 1 base X,Y index of where to start looking for names on each sheet (i.e. 1,1, = A1)
{ "Sheet1", new Tuple<int, int>(1, 1) },
{ "Sheet2", new Tuple<int, int>(2, 3) },
{ "Sheet3", new Tuple<int, int>(4, 5) },
{ "Sheet4", new Tuple<int, int>(2, 3) },
};
// List to keep track of all names in all sheets
List<string> names = new List<string>();
// Iterate over every sheet we need to look at
foreach(var worksheet in worksheets)
{
string workSheetName = worksheet.Key;
// Get this excel worksheet object
var xlWorksheet = (Microsoft.Office.Interop.Excel.Worksheet)xlWorkbook.Worksheets[workSheetName];
// Get the 1 based X,Y cell index
int row = worksheet.Value.Item1;
int column = worksheet.Value.Item2;
// Get the string contained in this cell
string name = (string)(xlWorksheet.Cells[row, column] as Microsoft.Office.Interop.Excel.Range).Value;
// name is null when the cell is empty - stop looking in this sheet and move on to the next one
while(name != null)
{
// Add the current name to the list
names.Add(name);
// Get the next name in the cell below this one
name = (string)(xlWorksheet.Cells[++row, column] as Microsoft.Office.Interop.Excel.Range).Value;
}
}
// Compare the number of names to the number of unique names
if (names.Count() != names.Distinct().Count())
{
// You have duplicate names!
}
You can use .Range to define multiple cells (ie, .Range["A1", "F500"])
https://msdn.microsoft.com/en-us/library/microsoft.office.tools.excel.worksheet.range.aspx
You can then use .get_Value to get the contents/values of all cells in that Range. According to dotnetperls.com get_Value() is much faster than get_Range() (see 'Performance' section). Using the combo of multiple ranges + get_value will definitely perform better of lots of single range calls using get_range.
https://msdn.microsoft.com/en-us/library/microsoft.office.tools.excel.namedrange.get_value(v=vs.120).aspx
I store them in the an Object Array.
(object[,])yourexcelRange.get_Value(Excel.XlRangeValueDataType.xlRangeValueDefault);
From there you can write your own comparison method to compare multiple arrays. One quirk is that doing this returns a 1-indexed array, instead of a standard 0-based index.
I am facing an issue in parsing excel file. My file has more than 5000 rows. When I parse it, its taking ages I wanted to ask if there's any better way to do so.
public static List<List<List<string>>> ExtractData(string filePath)
{
List<List<List<string>>> Allwork = new List<List<List<string>>>();
Microsoft.Office.Interop.Excel.Application excelApp = new Microsoft.Office.Interop.Excel.Application();
Microsoft.Office.Interop.Excel.Workbook workBook = excelApp.Workbooks.Open(filePath);
foreach (Microsoft.Office.Interop.Excel.Worksheet sheet in workBook.Worksheets)
{
List<List<string>> Sheet = new List<List<string>>();
Microsoft.Office.Interop.Excel.Range usedRange = sheet.UsedRange;
//Iterate the rows in the used range
foreach (Microsoft.Office.Interop.Excel.Range row in usedRange.Rows)
{
List<string> Rows = new List<string>();
String[] Data = new String[row.Columns.Count];
for (int i = 0; i < row.Columns.Count; i++)
{
try
{
Data[i] = row.Cells[1, i + 1].Value2.ToString();
Rows.Add(row.Cells[1, i + 1].Value2.ToString());
}
catch
{
Rows.Add(" ");
}
}
Sheet.Add(Rows);
}
Allwork.Add(Sheet);
}
excelApp.Quit();
return Allwork;
}
This is my code.
Your issue is that you are reading one cell at a time, this is very costly and inefficient try reading a range of cells.
Simple example below
Excel.Range range = worksheet.get_Range("A"+i.ToString(), "J" + i.ToString());
System.Array myvalues = (System.Array)range.Cells.Value;
string[] strArray = ConvertToStringArray(myvalues);
A link to basic example
Read all the cell values from a given range in excel
I suggest not use interop, but odbc connection for getting excel data. This will allow you to treat excel file as database and use sql statements to read needed data.
If that's an option, and if your tables have a simple structure, I would suggest to try exporting the file to .csv and applying simple string processing logic.
You might also want to try out the Igos's sugestion.
One approach is to use something like the ClosedXML library to directly read the .xlsx file, not going through the Excel interop.