I need to conditionally colorize ranges in a PivotTable. I tried to do it this way:
private void ColorizeContractItemBlocks(List<string> contractItemDescs)
{
int FIRST_DESCRIPTION_ROW = 7;
int DESCRIPTION_COL = 1;
int ROWS_BETWEEN_DESCRIPTIONS = 4;
int rowsUsed = pivotTableSheet.Cells.Rows.Count;
int currentRowBeingExamined = FIRST_DESCRIPTION_ROW;
// Loop through PivotTable data, colorizing contract items
while (currentRowBeingExamined < rowsUsed)
{
Cell descriptionCell = pivotTableSheet.Cells[currentRowBeingExamined, DESCRIPTION_COL];
String desc = descriptionCell.Value.ToString();
if (contractItemDescs.Contains(desc))
{
// args are firstRow, firstColumn, totalRows, totalColumns
Range rangeToColorize = pivotTableSheet.Cells.CreateRange(
currentRowBeingExamined, 0,
ROWS_BETWEEN_DESCRIPTIONS, _grandTotalsColumnPivotTable + 1);
Style style = workBook.Styles[workBook.Styles.Add()];
style.BackgroundColor = CONTRACT_ITEM_COLOR;
StyleFlag styleFlag = new StyleFlag();
styleFlag.All = true;
rangeToColorize.ApplyStyle(style, styleFlag);
}
currentRowBeingExamined = currentRowBeingExamined + ROWS_BETWEEN_DESCRIPTIONS;
}
}
...but it doesn't work, because rowsUsed does not take into consideration the rows on the PivotTable on the pivotTableSheet, and so my while loop is never entered.
How can I determine how many rows the PivotTable takes up on the sheet, so that I can loop through the PivotTable?
Or, am I approaching this the wrong way? Is there a different standard way of manipulating the styles/formatting of a PivotTable after it has been generated?
#B. Clay Shannon, You may consider using any of the following APIs for your requirement. I have added comments to the code for your reference.
var book = new Workbook(dir + "sample.xlsx");
var sheet = book.Worksheets[0];
var pivot = sheet.PivotTables[0];
// DataBodyRange returns CellArea that represents range between the header row & insert row
var dataBodyRange = pivot.DataBodyRange;
Console.WriteLine(dataBodyRange);
// TableRange1 returns complete Pivot Table area except page fields
var tableRange1 = pivot.TableRange1;
Console.WriteLine(tableRange1);
// TableRange2 returns complete Pivot Table area including page fields
var tableRange2 = pivot.TableRange2;
Console.WriteLine(tableRange2);
// ColumnRange returns range that represents the column area of the Pivot Table
var columnRange = pivot.ColumnRange;
Console.WriteLine(columnRange);
// RowRange returns range that represents the row area of the Pivot Table
var rowRange = pivot.RowRange;
Console.WriteLine(rowRange);
In case you still face any difficulty, please share your sample spreadsheet along with desired results (that you may create manually in Excel application) in a thread at Aspose.Cells support forum for thorough investigation.
Note: I am working as Developer Evangelist at Aspose.
The RowRange property of the pivot table should take you row by row through every element in the table:
Excel.Worksheet ws = wb.Sheets["Sheet1"];
Excel.PivotTable pt = ws.PivotTables("PivotTable1");
Excel.Range cell;
foreach (Excel.Range row in pt.RowRange)
{
cell = ws.Cells[row.Row, 5]; // for example, whatever is in column E
// do your formatting here
}
There are other ranges available -- for example, I typically only care about:
pt.DataBodyRange
Which is every cell within the actual pivot table (whatever is being aggregated).
Related
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 need to pass the value of a new cell.
this is because I need to make the same logic as in ItextSharp, that logic being that I need to be able to create a table only by adding cells.
Instead of adding rows and then adding the cells to the rows.
var cellValue = new Cell();
((Row)Rows.LastObject).Cells[i] = cellValue;
public void Add(PdfPCell cellValue)
{
MigraRow row = null;
if (currentRowCellIndex == Columns.Count || Rows.Count == 0)
{
row = this.AddRow();
currentRowCellIndex = 0;
}
currentRowCellIndex += cellValue.Colspan;
int cellCount = ((MigraRow)Rows.LastObject).Cells.Count;
int celIndex = cellCount == 0 ? 0 : cellCount - 1;
var cell = ((MigraRow)Rows.LastObject).Cells[celIndex];
cell = cellValue.Clone();
cell.Elements = cellValue.Elements;
//((MigraRow)Rows.LastObject).Cells[celIndex]. = cellValue.Borders.Clone();
//((MigraRow)Rows.LastObject).Cells[celIndex].Borders = cellValue.Borders.Clone();
//((MigraRow)Rows.LastObject).Cells[celIndex].Elements = cellValue.Elements.Clone();
//((MigraRow)Rows.LastObject).Cells[celIndex].Shading.Color = cellValue.BackgroundColor;
//((MigraRow)Rows.LastObject).Cells[celIndex].MergeRight = cellValue.Colspan;
//para = ((Paragraph)cell.Elements.LastObject).Clone();
}
this solution doesnt work because ...Cells[i]... is read only.
but I need to do something akin to this, is it possible?
As you can see I know that I can pass the values in each property to the Cells[i], but I don't like that solution.
Ps. in the presented code the PdfPCell is just a MigraDoc cell with some added functions by me, it can be used as a regular MigraDoc cell.
Answer to the current question:
MigraDoc is not iTextSharp. You can either use MigraDoc as intended - or add a setter to overcome the "read only" limitation since MigraDoc is open source. Instead of creating a PdfPCell and changing that you could pass a reference to the cell created by MigraDoc to your code to change the contents. This should be a simple change.
Answer to the original question:
You don't have to call new Cell() - the cell will be created automatically when you call AddRow() for the table.
The code looks much nicer then without casts and such:
Row row = table.AddRow();
Cell cell = row.Cells[0];
Sample code:
http://pdfsharp.net/wiki/HelloMigraDoc-sample.ashx
I have a specific template I have use for every page. It is essentially two columns of tables with 1 column and 1 row. Why the whole thing is not one table I do not know, but I am stuck with it. It looks like the following. Again each cell is actually its own table. So there is 40 tables on the page. These will be used as labels in a binder. Note that a table on the left is duplicated on the right. This is because they fold over and will be used as one.
What I need is to duplicate this to a new page and start over every time a page fills up and the count exceeds 20.
My code is as follows:
Word.Application app = new Word.Application();
var doc = app.Documents.Add(#"C:\.....LabelTemplate.dotx");
object missing = System.Type.Missing;
object pageBreak = Word.WdBreakType.wdPageBreak;
Word.Selection selection = app.Selection;
string path = Path.Combine(destination, "labels.docx");
int page = 1;
foreach (var batch in batches)
{
for (int i = 1; i <= batch.Count(); i++)
{
Word.Table table = doc.Tables[i];
table.Range.Font.Size = 7;
table.Range.ParagraphFormat.Alignment = Word.WdParagraphAlignment.wdAlignParagraphCenter;
table.Range.Font.Bold = 1;
int x = i - 1;
string text = batch[x].WellName + "\v" + batch[x].Field + "\v" + batch[x].Reservior;
//int cellNum = i + 1;
table.Cell(1, 1).Range.Text = text;
Word.Table table2 = doc.Tables[i + 20];
table2.Range.Font.Size = 7;
table2.Range.ParagraphFormat.Alignment = Word.WdParagraphAlignment.wdAlignParagraphCenter;
table2.Range.Font.Bold = 1;
table2.Cell(1, 1).Range.Text = text;
if (i % 20 == 0)
{
selection.EndKey(Word.WdUnits.wdStory, Word.WdMovementType.wdMove);
selection.InsertBreak(ref pageBreak);
}
}
page++;
}
doc.SaveAs(path);
doc.Close();
app.Quit();
Building Blocks provide a convenient way to store and re-use "boiler-plate" in Word documents. The content can be "plain text" or include formatting and all things supported by Word. This is often easier and faster than trying to recreate content using code or importing content from an outside file.
Building Blocks are stored in templates (dotx or dotm). A template can be attached to a document (usually when a document is created from that template) or it can be "global", such as Normal.dotm.
In this case, the Building Block is stored in the attached template and will be available to any document created from that template (but not for other documents).
Building Blocks can be organized into "Types" and "Categories", meaning there can be multiple Building Blocks in a template "container" with the same name. If that's the case, it's necessary to specify the Building Block type, category and name when addressing it. If the name is unique in the template, then only the name is required.
The following code snippet shows both possibilities. The two Building Blocks are inserted one after the other at the end of the Document object.
A Building Block is inserted using the Insert method.
Word.Range rng = doc.Content;
rng.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
Word.Template objTmpl = (Word.Template) doc.get_AttachedTemplate(); // NormalTemplate
Word.BuildingBlock objBB = objTmpl.BuildingBlockEntries.Item("TestCCwithActiveX");
objBB.Insert(rng, true);
rng = doc.Content;
rng.Collapse(Word.WdCollapseDirection.wdCollapseEnd);
Word.BuildingBlock objBB2 = objTmpl.BuildingBlockTypes.Item(Word.WdBuildingBlockTypes.wdTypeAutoText).Categories.Item("Test").BuildingBlocks.Item("test heading");
objBB2.Insert(rng, true);
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");
}