update data on a chart and then refresh underlying 'edit data' view - c#

Using the NetOffice PowerPoint API to update charts in PowerPoint using (eg)
using (NetOffice.PowerPointApi.Series ser =
(NetOffice.PowerPointApi.Series)xob2.Chart.SeriesCollection(xs))
{
ser.Values = someNewValues;
}
while this accomplishes what I need for display (values are right, and the tooltip shows them as expected), if I select the graph and 'Edit Data' it opens a sheet with the original, unmodified data.
Is there a way to update the underlying sheet data, ideally without using code like
using (NetOffice.PowerPointApi.Chart cc = ob2.Chart)
{
using (ChartData cd = cc.ChartData)
{
cd.Activate();
using (Workbook wb = (Workbook)cd.Workbook)
{
using (Worksheet ws = (Worksheet)wb.Worksheets[1])
...
}
}
}
as I want to avoid the issues instantiating Excel seems to cause (RPC timeouts, clashes if two scripts are running concurrently, and the time factor - using direct manipulation seems to be about 2x faster than updating the worksheet)
I was hoping there would be a simple solution, but stumped so far (even looked at unzipping the pptx and editing the chart XMLs but that got messy fast!)

Related

Excel interop CopyFromRecordset/ADODB (custom data/no database) in .net core

We have this poor man's weekly task, which essentially loads a bunch of CSVs, massages them a bit, dumps them into preloaded Excel template and sends them as an attachment to mgmt. Then they play with it using excel filters and charts, etc (not important here).
What is important is that this used to be a .NET Framework app which we have to migrate to .net core, and the initial author did this by constructing an ADODB recordset and using CopyFromRecordset method on a Range to dump the entire table. For example, this constructs the recordset:
internal static ADODB.Recordset ConvertToADODBRecordSet(this List<MyData> data)
{
var result = new ADODB.Recordset { CursorLocation = ADODB.CursorLocationEnum.adUseClient };
var resultFields = result.Fields;
resultFields.Append("Date", ADODB.DataTypeEnum.adDate);
resultFields.Append("Hour", ADODB.DataTypeEnum.adInteger);
resultFields.Append("Code", ADODB.DataTypeEnum.adVarWChar, 16);
result.Open(CursorType: ADODB.CursorTypeEnum.adOpenStatic, LockType: ADODB.LockTypeEnum.adLockOptimistic);
foreach (var dr in data)
{
result.AddNew();
resultFields[0].Value = dr.Date;
resultFields[1].Value = dr.Hour;
resultFields[2].Value = dr.Code;
}
return result;
}
...
var table = sheet.ListObjects["CodesTable"];
table.InsertRowRange.CopyFromRecordset(data.ConvertToADODBRecordSet());
As far as I see, there is not ADODB support in .net framework, I couldn't even try it with supported database objects because, well, there is no database here, recordset is manual and judging by other peoples' attempts it wouldn't work anyway. Entering values in cells one by one is not feasible, there are a dozen sets, each with over 100K rows, it takes forever.
So, how does one go about injecting a manual range of values in Excel using a single call C# interop on .net core?
PS: I cannot use any library which directly just modifies xlsx file. Upon injecting data, a bunch of charts and pivot tables are refreshed by Excel, so whatever alternative solution might come up, it needs to act as a full Excel and update all the sheets before saving.
Ok, I found a solution, in case anyone needs it. I have abandoned the data transfer route and leaned on clipboard to do the job. "Copying" tab delimited data and then pasting it in the table inject region did the trick. You will however have to convert the pure .net console project into *-windows targeted project and add OutputType of WinExe and UseWindowsForms to true (or WPF) so that Clipboard class can be available (its not in pure console project). You do not have to create any forms or Run the winForms/WPF though, just let it run as console app in Main.
using var writer = new StringWriter();
var cfg = new CsvConfiguration(CultureInfo.GetCultureInfo("en-us"))
{
HasHeaderRecord = false,
Delimiter = "\t"
};
using var csv = new CsvWriter(writer, cfg);
csv.WriteRecords(data);
var dataObject = new DataObject();
dataObject.SetText(writer.ToString());
System.Windows.Forms.Clipboard.SetDataObject(dataObject, true);
// paste to excel
var table = sheet.ListObjects["CodesTable"];
table.InsertRowRange.PasteSpecial(XlPasteType.xlPasteAll, XlPasteSpecialOperation.xlPasteSpecialOperationAdd);

OpenXML refresh connectors on a PowerPoint Slide automatically

I have a simple scenario where I programmatically create a presentation with a slide using OpenXML SDK 2.5 and c#. The slide has 2 shapes on it as well as a connector that connects these 2 shapes.
When I open the presentation in PowerPoint both shapes and the connector shown, but the connector is not positioned properly between the shapes. When I drag one of the shapes on the slide, PowerPoint immediately refreshes the connector and puts it into the correct position.
My question: is it possible to create an openxml PowerPoint slide that automatically refreshes the connector positions when the file is opened?
Thank you
The solution I came up with for this problem may seem rather hack-ish, but as far as I can tell there isn't a better way. The problem is that PowerPoint controls connector placement internally and doesn't expose any methods to refresh them. In testing, I was amazed to discover that PowerPoint will dynamically change the connector type during the refresh if necessary.
In order to get the refresh to happen, I had to write a VBA macro in a .pptm file and call it from my C# code. I added a module and put the function there so it wouldn't be associated with a particular slide.
This code moves each shape on a slide in order to cause the connector refresh to fire. It looks for shapes inside of groups too. It is filtering on triangle and diamond shapes.
I avoided using ActivePresentation in the code because I want to hide PowerPoint while the macro runs.
Public Sub FixConnectors()
Dim mySlide As Slide
Dim shps As Shapes
Dim shp As Shape
Dim subshp As Shape
Set mySlide = Application.Presentations(1).Slides(1)
Set shps = mySlide.Shapes
For Each shp In shps
If shp.AutoShapeType = msoShapeIsoscelesTriangle Or shp.AutoshapeType = msoShapeDiamond Then
shp.Left = shp.Left + 0.01 - 0.01
End If
If shp.Type = mso.Group Then
For Each subshp In shp.GroupItems
If subshp.AutoShapeType = msoShapeIsoscelesTriangle Or subshp.AutoshapeType = msoShapeDiamond Then
subshp.Left = subshp.Left + 0.01 - 0.01
End If
Next subshp
End If
Next shp
Application.Presentations(1).Save
End Sub
Next comes the C# code to run the macro using PowerPoint Interop. Closing and reopening the file as a Windows process allows the garbage collector to clean up any handles that Interop has. In testing, the finalizers could take several seconds to run so the GC calls happen after reopening the file so the application doesn't appear to hang.
using System;
using System.Diagnostics;
using OC = Microsoft.Office.Core;
using PP = Microsoft.Office.Interop.PowerPoint;
string filePath = "C:/Temp/";
string fileName = "Template.pptm";
// Open PowerPoint in hidden mode, run the macro, and shut PowerPoint down
var pptApp = new PP.Application();
PP.Presentation presentation = pptApp.Presentations.Open(filePath + fileName, OC.MsoTriState.msoFalse, OC.MsoTriState.msoFalse, OC.MsoTriState.msoFalse);
pptApp.Run(filename + "!.FixConnectors");
presentation.Close();
presentation = null;
pptApp.Quit();
pptApp = null;
// Reopen the file through Windows
Process.Start(filePath + fileName);
// Clear all references to PowerPoint
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
GC.WaitForPendingFinalizers();

System.AccessViolationException when using Excel.Worksheet.Copy

We have a VSTO addin for Excel. The main functionality creates reports that are used to generate workbooks. When I run a batch of reports, I get a System.AccessViolationException when using Excel.Worksheet.Copy, which also crashes Excel. Here's how I recreate it:
1) Open and run report #1 with a single parameter which creates one workbook. We close the workbook.
2) Open and run the same report with several parameters. This create 5 workbooks but crashes when creating the second, but ONLY if we have run the first single output report (see step 1). If we remove the report from step 1 from the batch, this creates all 5 workbooks without error.
I've checked to make sure that the sheet we are copying is from the workbook is open, and is not referencing the first report. In fact, we close the first one so I know that it's not. Again, this ONLY happens if we have the report in step one, which it does not access at all, so how could that be affecting a sheet from a completely different workbook?
This doesn't even finish out my try/catch so that I can get more info. It simply blows up Excel and I have to restart.
UPDATE:
Here's the basic code:
function void ReplaceSheets(Dictionary<Excel.Worksheet, IReportSheet> sheetReports)
{
List<string> oldNames = new List<string>(sheetReports.Count);
foreach (Excel.Worksheet oldSheet in sheetReports.Keys)
{
Excel.Worksheet veryHiddenSheet = null;
Excel.Worksheet newSheet = null;
try
{
string sheetName = oldSheet.Name;
veryHiddenSheet = WorkbookHelper.FindSheet(this.DocumentView, MakeHiddenSheetName(sheetName, "--VH--"));
veryHiddenSheet.Visible = Excel.XlSheetVisibility.xlSheetVisible; //Sheet has to be visible to get the copy to work correctly.
veryHiddenSheet.Copy(this.DocumentView.Sheets[1], Type.Missing);//This is where it crashes
newSheet = (Excel.Worksheet)this.DocumentView.Sheets[1]; //Get Copied sheet
/* do other stuff here*/
}
finally
{
veryHiddenSheet = null;
newSheet = null;
}
}
}
I never found a way in VSTO to "fix" this. I switched code to NetOffice, and I was able to get some better error message. Excel/Com was not releasing the memory attached to the spreadsheets. I rebuilt the reports from blank 2010 spreadsheets and it took care of it. I think it was a corrupted 2007 spreadsheet that may have occured on converting to 2010 or something like that. I recommend NetOffice over VSTO because the exception handling is far superior, and you have access to the source code, but it does have it's quirks. (You'll need to pay attention to loading order for taskpanes.)

Delete rows from Excel

Following are the approaches I tried:
A) I tried to delete rows from an excel sheet using Microsoft.Office.Interop.Excel.
I'm doing this in a script task within a SSIS package.
I added the library to the GAC, since it was raising an error : Could not load Library.
Now it's raises this error saying : Retrieving the COM class factory for component with CLSID {00024500-0000-0000-C000-000000000046} failed due to the following error: 80040154.
Googling this tells me I need MS Office installed for it to work, which I don't want coz the server I deploy this solution on is definitely not going to have MS Office installed on it. I'm no expert, but I would like to know why such operations are not possible, by simply adding reference to a dll? Why is it mandatory to install MS Office.
B) I also tried Oledb jet provider, but this one doesn't allow deleting of rows.
The only operations it supports is Insert, Update and Select.
Things I have come across on the web:
A) A SO Questions' answer suggests to use Npoi, but I can't totally rely on that, because what's free library today can become paid in future.
B) Also I have come across EPP Plus library. I have used it and understand that it's based on a GNU public license, but I'm apprehensive on using it because it may become a paid tool in future.
C) I have also come across people using Open XML SDK by Microsoft. Before I get my hands dirty in this, I would love if someone up front tells me whether I should be using this. Not that I'm lazy to try it out myself but what what would be helpful to me before I start is, does this SDK need any external programs installed on the machine. Coz it requires me to install an msi to be able to us it.
Is there a work around to do this using Microsoft COM components? I'm not asking a subjective question here. I want to know technical obstacles, if any when I use the above three researched tools.
Thanks in advance
The point is with Interop that you indeed must have office installed. So bluntly said, you cannot use Interop. If you only need to support xlsx files, you can do it in xml.
See this and this link for more details about unpacking xlsx files, editing and repacking. The only thing you need than is something to unzip it and your own xml handling code.
If the requirement is to also support xls files you have a bit of a problem. I tried this in the past without any additional installations but did not succeed, so I decided to only support xlsx. I either needed some .msi files or office installed on the server.
You're saying that you are using a script task in SSIS; then why not import the excel file you want to delete the values from it (preferably into a database or keep it cached into a datatable) and then generate a new xls file with just the data you want to keep.
OR don't use the script task at all and use, inside a data flow, a configured excel source combined with a script component (which is basically the same thing as a script task just that you can use this one only in a data flow) and do all your work there. If you have a dynamic connection to the excel file, you can always use variables (parameters if you're on DataTools) to configure such a connection.
Good luck!
If you want to use Microsoft.Office.Interop.Excel then, yes, you do need Excel on the server. Therefore, so long as you only want to deal with xlsx based workbooks / 2007+ then I would suggest that OpenXML is the way to go. It's a bit of a learning curve and you get to realise how much work Excel does for you in the background but is not too bad once you get used to it.
A very quick sample knocked up in LINQPad:
void Main()
{
string fileName = #"c:\temp\delete-row-openxml.xlsx";
using (SpreadsheetDocument doc = SpreadsheetDocument.Open(fileName, true))
{
// Get the necessary bits of the doc
WorkbookPart workbookPart = doc.WorkbookPart;
SharedStringTablePart sstpart = workbookPart.GetPartsOfType<SharedStringTablePart>().First();
SharedStringTable sst = sstpart.SharedStringTable;
// Get the first worksheet
WorksheetPart worksheetPart = workbookPart.WorksheetParts.First();
Worksheet sheet = worksheetPart.Worksheet;
var rows = sheet.Descendants<Row>();
foreach (Row row in rows.Where(r => ShouldDeleteRow(r, sst)))
{
row.Remove();
}
}
}
private bool ShouldDeleteRow(Row row, SharedStringTable sst)
{
// Whatever logic to apply to decide whether to remove a row or not
string txt = GetCellText(row.Elements<Cell>().FirstOrDefault(), sst);
return (txt == "Row 3");
}
// Basic way to get the text of a cell - need to use the SharedStringTable
private string GetCellText(Cell cell, SharedStringTable sst)
{
if (cell == null)
return "";
if ((cell.DataType != null) && (cell.DataType == CellValues.SharedString))
{
int ssid = int.Parse(cell.CellValue.Text);
string str = sst.ChildElements[ssid].InnerText;
return str;
}
else if (cell.CellValue != null)
{
return cell.CellValue.Text;
}
return "";
}
Note that this will clear the row not shuffle up all the other rows. To do that you'd need to provide some logic to adjust row indexes of the remaining rows.
To answer a little more of the OP question - the OpenXML msi is all that is needed apart from the standard .Net framework. The sample needs a reference to WindowsBase.dll for the packaging API and using statements for DocumentFormat.OpenXml.Packaging and DocumentFormat.OpenXml.Spreadsheet. The OpenXML API package can be referenced in VS via Nuget too so you don't even need to install the msi if you don't want. But it makes sense to do so IMHO.
One other item that you will find VERY useful is the OpenXML tools msi. This lets you open a Word or Excel doc and see the XML layout inside - most helpful.
This is how I managed to remove rows in excel and move up the data
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using (SpreadsheetDocument document = SpreadsheetDocument.Open(pathToFile, true))
{
WorkbookPart wbPart = document.WorkbookPart;
var worksheet = wbPart.WorksheetParts.First().Worksheet;
var rows = worksheet.GetFirstChild<SheetData>().Elements<Row>();
// Skip headers
foreach (var row in rows.Skip(1))
{
if (/* some condition on which rows to delete*/)
{
row.Remove();
}
}
// Fix all row indexes
string cr;
for (int i = 2; i < rows.Count(); i++)
{
var newCurrentRowIndex = rows.ElementAt(i - 1).RowIndex.Value + 1;
var currentRow = rows.ElementAt(i);
currentRow.RowIndex.Value = updatedRowIndex;
IEnumerable<Cell> cells = currentRow.Elements<Cell>().ToList();
if (cells != null)
{
foreach (Cell cell in cells)
{
cr = cell.CellReference.Value;
cr = Regex.Replace(cell.CellReference.Value, #"[\d-]", "");
cell.CellReference.Value = $"{cr}{updatedRowIndex}";
}
}
}
worksheet.Save();
}

how to create and download excel document using asp.net

How to create and download excel document using asp.net ?
The purpose is to use xml, linq or whatever to send an excel document to a customer via a browser.
Edit : Use case
The customer load a gridview ( made with ajax framework ) in a browser, the gridview is directly linked to an sql database.
I put a button 'export to excel' to let customer save this gridview data on his computer ansd i would like to launch a clean download of an excel.
The solutions proposed here are not clean, like send an html document and change the header to excel document etc, i'm searching a simple solution on codeplex right now, i will let you know.
Starter kit
First i have downloaded the Open XML Format SDK 2.0.
It comes with 3 useful tools in :
C:\Program Files\Open XML Format SDK\V2.0\tools
DocumentReflector.exe wich auto
generate the c# to build a
spreadsheet from the code.
OpenXmlClassesExplorer.exe display
Ecma specification and the class
documentation (using an MSDN style
format).
OpenXmlDiff.exe graphically compare
two Open XML files and search for
errors.
I suggest anyone who begin to rename .xlsx to .zip, so you can see the XML files who drive our spreadsheet ( for the example our sheets are in "xl\worksheets" ).
The code
Disclaimer : I have stolen all the code from an MSDN technical article ;D
The following code use an *.xlsx template i made manually to be able to modify it.
Namespaces references
using System.IO;
using System.Xml;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Spreadsheet;
using DocumentFormat.OpenXml;
// Database object
DataClassesDataContext db = new DataClassesDataContext();
// Make a copy of the template file.
File.Copy(#"C:\inetpub\wwwroot\project.Web\Clients\Handlers\oxml-tpl\livreurs.xlsx", #"C:\inetpub\wwwroot\project.Web\Clients\Handlers\oxml-tpl\generated.xlsx", true);
// Open the copied template workbook.
using (SpreadsheetDocument myWorkbook = SpreadsheetDocument.Open(#"C:\inetpub\wwwroot\project.Web\Clients\Handlers\oxml-tpl\generated.xlsx", true))
{
// Access the main Workbook part, which contains all references.
WorkbookPart workbookPart = myWorkbook.WorkbookPart;
// Get the first worksheet.
WorksheetPart worksheetPart = workbookPart.WorksheetParts.ElementAt(2);
// The SheetData object will contain all the data.
SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();
// Begining Row pointer
int index = 2;
// Database results
var query = from t in db.Clients select t;
// For each item in the database, add a Row to SheetData.
foreach (var item in query)
{
// Cell related variable
string Nom = item.Nom;
// New Row
Row row = new Row();
row.RowIndex = (UInt32)index;
// New Cell
Cell cell = new Cell();
cell.DataType = CellValues.InlineString;
// Column A1, 2, 3 ... and so on
cell.CellReference = "A"+index;
// Create Text object
Text t = new Text();
t.Text = Nom;
// Append Text to InlineString object
InlineString inlineString = new InlineString();
inlineString.AppendChild(t);
// Append InlineString to Cell
cell.AppendChild(inlineString);
// Append Cell to Row
row.AppendChild(cell);
// Append Row to SheetData
sheetData.AppendChild(row);
// increase row pointer
index++;
}
// save
worksheetPart.Worksheet.Save();
}
I havent finished yet, my second job is to auto download the spreadsheet after modification.
Finally, i redirect the user to my generated spredsheet (from my aspx)
context.Response.Redirect("Oxml-tpl/generated.xlsx");
just set Response.ContentType = "application/vnd.ms-excel" and your page will rendered as an excel sheet on the clients browser
Sample code here
There are quite a few ways of handling this, depending on how extensive the Excel functionality is. Binoj's answer works if the Excel is just a spreadsheet and has no direct Excel functionality built in. The client can add functionality, concats, etc. These are "dumb" excel docs until the client does soemthing.
To create a more full featured Excel doc, you havve two basic choices that I can think of offhand.
Use either the office components (re: bad) to create an excel document, or a third party component, like SoftArtisan's ExcelWriter. Great component, but there is a cost.
Use a control on the page that allows export to Excel. Most vendors of ASSP.NET controls have this functionality on their grids.
Option #1 allows you pretty much all functionality of Excel. Option #2 is a bit more limited, at least in the controls I have tried.
Good article on how top export to excel from Erika Ehrli
http://blogs.msdn.com/erikaehrli/archive/2009/01/30/how-to-export-data-to-excel-from-an-asp-net-application-avoid-the-file-format-differ-prompt.aspx

Categories