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;
}
}
}
}
Related
I have a WinForms ListView table, which I load from a text file, which is comma delimited. This is then split, line by line, by comma. The resulting string is then added into a ListViewItem. Example of what my ListView looks like after insert ListView
I have a couple of issues, the main one being that I have set up a listener on column header click, which allows deletion of the column. This does result in the removal of the column data. The code for this is (I'm still learning C# so I appreciate this may not be the correct way of doing things):
private void DataTable_ColClick(object sender, ColumnClickEventArgs e)
{
Int32 colIndex = Convert.ToInt32(e.Column.ToString());
PublicVars.ColClicked = colIndex;
contextMenuStrip1.Show(Cursor.Position);
}
private void DeleteMenu_Click(object sender, EventArgs e)
{
if (PublicVars.ColClicked >= 0)
{
RawCSVData.BeginUpdate();
RawCSVData.Columns.RemoveAt(PublicVars.ColClicked);
RawCSVData.EndUpdate();
PublicVars.ColClicked = -1;
}
}
My problem starts when I want to add the contents of the visual ListView into a ListView which I pass to another class, when that happens it also includes all the column data that was "deleted".
This is the code used to create a new List<string>, using the visual ListView sub-items, created as a comma delimited string (this is the only way I could find of doing it - again, I'm still learning so this may not be the best way of achieving it).
public void UpdatedDataOkBtn_Click(object sender, EventArgs e)
{
try
{
List<string> StringLV = new List<string>();
for (int i = 0; i < RawCSVData.Items.Count; i++)
{
string EachLine = "";
for (int j = 0; j < RawCSVData.Items[i].SubItems.Count; j++)
{
string element = RawCSVData.Items[i].SubItems[j].Text;
if (element.Length > 0)
{
EachLine += element + ",";
}
}
StringLV.Add(EachLine.Remove(EachLine.Length - 1));
}
string[] StrArr = StringLV.ToArray();
string FirstLineInArray = StrArr[0];
List<string> Lines = new List<string>();
Lines.AddRange(Regex.Split(FirstLineInArray, "[,]{1}(?=(?:[^\"]*\"[^\"]*\")*(?![^\"]*\"))"));
ActiveForm.Close();
ConvertForm convertForm = new ConvertForm();
int[] DataPosition = CheckCSVData.ScanArray(Lines);
var (CSVListData, TotalAmount, NumRows) = PopulateLV.Populate(StringLV, DataPosition);
convertForm.Show();
convertForm.BuildDataTable(CSVListData, TotalAmount, NumRows);
}
catch (Exception error)
{
ConvertForm.ErrorState(error.ToString());
}
}
How do I just create a new ListView with only the visible items?
As a side question - I want to start at the first Col0 (I create the number of columns based on the number of comma delimited values), but as you can see, I end up with an initial Col0 (which is set in the ListView ColumnHeader Collection Editor), but then my data imported from the file begins in the next col (I assign the Col value using a counter).
And finally, I did attempt to allow column reordering, which worked OK, but again, when inserting into a new ListView, it didn't retain the visual ordering.
I have attempted several different possible solutions, but none seem to take only the remaining column data into the new ListView.
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();
}
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.
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 currently in need of an idea, I'm developing a software solution and I have to create Words documents from BDD's information.
Everything is fine about informations's use but one point and it's the most important :
On my Word document, I use signets to repeat a table where information is written. One of the column refer to a cost, and since I'm using the signets's system, I don't know where my table end, it can take 1 page as it can take 2500's but I need to calculate at the end of EACH page a total of every row that was written on the current page, and then rewrite this total at the beginning of the next page. So it would be like :
Page 1
nameOfTheItem1 Cost1
nameOfTheItem2 Cost2
nameOfTheItem3 Cost3
nameOfTheItem4 Cost4
TOTAL PAGE 1 : TotalPage1
Page 2
TotalPage1
nameOfTheItem5 Cost5 nameOfTheItem6 Cost6 nameOfTheItem7
Cost7 nameOfTheItem8 Cost8 TOTAL PAGE 2 :
TotalPage2(+TotalPage1)
and this for each page existing for this document.
I'm still looking for a solution by myself, and every help would be great.
It can be done with the mail merge feature of Aspose.Words. The solution is particularly related to the nested mail merge.
You have to arrange the data in a DataSet, in order to make the report appear according to your requirements. For this scenario, arrange in 2 tables. One is "Page" and other is "Item"
Your template document (DOCX) should define the merge fields according the the image below. Note that there is a page break after the table.
The following code will help you to get started. It uses dummy data of course. You can populate with your own data to make it work for you.
MS Word template document that works with this code: Download Template
private void yourMethod()
{
string srcDoc = dataDir + "ItemsTemplate.docx";
string dstDoc = dataDir + "ItemsTemplate_Result.docx";
int totalRecords = 10;
int recordsPerPage = 4;
// Prepare some data
DataSet ds = getData(totalRecords, recordsPerPage);
// Prepare the document in Aspose
Aspose.Words.Document doc = new Aspose.Words.Document(srcDoc);
doc.MailMerge.ExecuteWithRegions(ds);
doc.MailMerge.CleanupOptions = Aspose.Words.Reporting.MailMergeCleanupOptions.RemoveEmptyParagraphs;
doc.Save(dstDoc);
Process.Start(dstDoc);
}
private DataSet getData(int totalRecords, int recordsPerPage)
{
DataSet ds = new DataSet("Dataset");
// Add the page table
System.Data.DataTable pageTable = new System.Data.DataTable("Page");
pageTable.Columns.Add("PageNumber");
pageTable.Columns.Add("PageTotal");
pageTable.Columns.Add("PreviousPageTotal");
// Add the item table
System.Data.DataTable itemTable = new System.Data.DataTable("Item");
itemTable.Columns.Add("ID");
itemTable.Columns.Add("Name");
itemTable.Columns.Add("Cost");
itemTable.Columns.Add("PageNumber");
// Add pages
int iRow = 1, iPage = 1;
while (iRow <= totalRecords )
{
DataRow pageRow = pageTable.NewRow();
pageRow["PageNumber"] = iPage;
pageRow["PageTotal"] = 0;
// Add the items in this page
int iRecordsPerPage = 1;
while (iRow <= totalRecords && iRecordsPerPage <= recordsPerPage)
{
DataRow itemRow = itemTable.NewRow();
itemRow["ID"] = iRow;
itemRow["Name"] = "Item " + iRow;
itemRow["Cost"] = iRow;
itemRow["PageNumber"] = iPage;
pageRow["PageTotal"] = int.Parse(pageRow["PageTotal"].ToString()) + int.Parse(itemRow["Cost"].ToString());
itemTable.Rows.Add(itemRow);
iRow++;
iRecordsPerPage++;
}
pageTable.Rows.Add(pageRow);
// Previous page total
if (iPage == 1)
pageRow["PreviousPageTotal"] = 0; // Always 0 for first page
else
pageRow["PreviousPageTotal"] = pageTable.Rows[iPage - 2]["PageTotal"]; // Get total of previous page
iPage++;
}
ds.Tables.Add(pageTable);
ds.Tables.Add(itemTable);
// We must have relationship for Aspose mail merge to work correctly
ds.Relations.Add(pageTable.Columns["PageNumber"], itemTable.Columns["PageNumber"]);
return ds;
}
Try changing the values of totalRecords and recordsPerPage variables and you will see the data arranged in pages accordingly. Just be sure to keep the recordsPerPage value low, so that it does not exceed single page.
I am a developer evangelist at Aspose.
I am exporting data to existing word template which is .dotx
But the final report does not show the table of contents even though i have added it in code.
My code is as follows
public void ExportToWordUsingTemplate()
{
Aspose.Words.Document doc1 = new Aspose.Words.Document(#"E:/excel/HOVEDMAL Prognoserapporter 2.dotx");
DocumentBuilder docBuilder1 = new DocumentBuilder(doc1);
SkinAPI.ReportAPISoapClient svc = new SkinAPI.ReportAPISoapClient();
SkinAPI.GetReportContextResult myReportContext = svc.GetReportContext(1);
docBuilder1.InsertHtml("<h1 align='left'>" + myReportContext[0].MainReportName + "</h1>");
docBuilder1.InsertTableOfContents("\\o \"1-3\" \\h \\z \\u");
//for (int i = 0; i < myReportContext.Count - 2; i++)
for (int i = 0; i < 5; i++)
{
SkinAPI.GetReportElementGraphDataResult myElementGraphData = svc.GetReportElementGraphData(myReportContext[i].ReportId, myReportContext[i].ElementId);
SkinAPI.GetReportElementDataResult myElementData = svc.GetReportElementData(myReportContext[i].ReportId, myReportContext[i].ElementId, 0, 0, 0); // Three last parameters set to 0, used when fetching drilldown data for tables that support it
docBuilder1.ParagraphFormat.StyleIdentifier = StyleIdentifier.Heading1;
docBuilder1.Writeln(myReportContext[i].ElementHeader);
docBuilder1.ParagraphFormat.StyleIdentifier = StyleIdentifier.BodyText;
// Is there a graph for this element, and has it a datasource other than the main data source as fetched above?
if (myReportContext[i].HasGraph && myReportContext[i].SeparateGraphDataSource)
{
// Is there a text part for this element
if (myReportContext[i].HasText)
{
// The returned string will contain a HTML text.
// Note that the text is connected to a TileId, not an ElementId, meening the text might have been fetched before.
string myElementHTMLDescription = svc.GetReportText(myReportContext[i].TileId);
docBuilder1.InsertHtml(myElementHTMLDescription);
}
}
docBuilder1.InsertBreak(BreakType.PageBreak);
}
doc1.Save(#"E:/excel/HOVEDMAL Prognoserapporter 2_Report.doc");
}