OpenXML refresh connectors on a PowerPoint Slide automatically - c#

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();

Related

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

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!)

Microsoft Word Interop performance issue

I have a winform program which loads Microsoft Word and performs some basic editing (find & replace), as well as some autosaving.
It loads an existing word template, which again is basic text.
The code for the interop is;
try
{
// Is Word running?
WordApp = System.Runtime.InteropServices.Marshal.GetActiveObject("Word.Application") as Microsoft.Office.Interop.Word.Application;
WordApp.Visible = true;
return WordApp;
}
catch (COMException ce)
{
WordApp = null;
if (ce.ErrorCode == unchecked((int)0x800401E3))
WordApp = new Microsoft.Office.Interop.Word.Application();
WordApp.Visible = true;
return WordApp;
}
Once the document is open, the user types what they need to but it has been noted that after a page or so of text, the performance really slows. There is an increasing lag on the users typing.
I thought initially it was due to some issues with the find/replace code so I have commented out everything apart from the code for loading the template;
WordApp = WordEdit.GetWord(); //Class & Method calling interop code
WordApp.Documents.Add(AppDomain.CurrentDomain.BaseDirectory + "\\Templates\\" + DocType + ".dot");
//set Active Document
WordDoc = WordApp.ActiveDocument;
The performance is still as poor.
I then thought to release the COM and set things to null, but again this didn't have any effect.
System.Runtime.InteropServices.Marshal.ReleaseComObject(WordApp);
WordApp = null;
WordDoc = null;
I then thought maybe my app is causing the system in general to slow down. If I quit the application but continue to use the Word application it load, the performance is still slow. If I start a fresh Word application (manually) this works perfectly. So it has something to do with the way my application is loading word. My application does not have any impact on system resources, and is currently set to do nothing other than load the template.
Is there either a different way get hold of Word (a different way to use interop) or a way to improve performance?
Although I haven't been able to pin point the cause of the slow performance, I have found a workaround.
Instead of having the app create a new Word session via the marshal, I instead use this code;
Process.Start(AppDomain.CurrentDomain.BaseDirectory + "\\Templates\\" + DocType + ".dot");
System.Threading.Thread.Sleep(2000);
This opens the required template (DocType+ extension) so the system creates a Word session.
I then use
WordApp = System.Runtime.InteropServices.Marshal.GetActiveObject("Word.Application") as Microsoft.Office.Interop.Word.Application;
WordApp.Visible = true;
return WordApp;
To take control of the now active session.
The ` System.Threading.Thread.Sleep(2000); is there because if it wasn't, things run too quickly and then my app can't get hold of the Active Document in the WordApp object. Giving it a few seconds wait gives it chance to catch up.
It's not ideal - but it has removed all performance issues,
`

C# How to update Powerpoint chart without opening Excel [duplicate]

Slide.Shapes.AddChart() automatically opens Excel. Even if I quickly do Chart.ChartData.Workbook.Application.Visible = false, it still shows a little while. This makes automating chart creation error-prone as the user has to try not to touch the Excel applications that keeps popping up.
Opening a presentation with WithWindow = false will still open Excel when creating new charts.
This behavior is "by design" and Microsoft is not interested in changing. This is the way the UI functions.
What you could do would be to create the chart in Excel (using either the interop or OpenXML), then import (insert) that file into PowerPoint.
Check this link from MSDN
Here's a possible work around.
Sub ChartExample()
Dim s As Shape
Set s = Application.Presentations(1).Slides(1).Shapes.AddOLEObject(ClassName:="Excel.Chart")
End Sub
You would then manipulate the chart you added via the s.OLEFormat.Object. I only experimented slightly, but it does not open an external Excel application and I did not see any extreme flickering unless I activated the object. A trade off is that at least in Powerpoint 2010, you need to convert it to use all of the features. If this doesn't work you could always try web components.
Edit:
I don't understand why this method causes a problem, but to try to assist further here is a little more code that shows actually manipulating the object. This was written with objects instead of workbooks etc, so that no references need to be made. It only demands the user have Excel on their machine.
Option Explicit
Const xlcolumns = 2
Sub ChartExample()
Dim s As Shape
Dim wb As Object, chart As Object, data As Object
Set s = Application.Presentations(1).Slides(1).Shapes.AddOLEObject(ClassName:="Excel.Chart")
Set wb = s.OLEFormat.Object
Set chart = wb.Sheets(1)
Set data = wb.Sheets(2)
'Set the range for the chart data
chart.setsourcedata Source:=data.Range("A1:C7"), PlotBy:= _
xlcolumns
'Update data values for the chart
data.Range("B1").Value = "Column Label 1"
data.Range("C1").Value = "Column Label 2"
data.Range("A2:C7").clearcontents
data.Range("A2").Value = "Row Label"
data.Range("B2").Value = 7
data.Range("C2").Value = 11
End Sub
I would suggest another methdology to over come the same.
In the powerpoint VBA add refrences to "Microsoft Excel 12.0 Object Library"
Ensure the user that for this operation none of the excel must be open via yuser form popup before the operation.
In the VBA create an excel and set its parameters in the following code
Add the powerpoint chart, the user wouldnt be able to see the opening of the underlying excel sheet upon adding chart excet the excel tabs which can be controled via code.
Sample Code:
Option Explicit
Sub AddExcelChartSample()
Dim xlApp As Excel.Application, xlWkbk As Excel.Workbook
Dim pres As PowerPoint.Presentation, sld As PowerPoint.Slide, iCount As Integer, chtShape As PowerPoint.Shape
'Open up the excel instance and set parameters
Set xlApp = New Excel.Application
With xlApp
.WindowState = xlNormal
.Top = -1000
.Left = -1000
.Height = 0
.Width = 0
End With
Set sld = PowerPoint.ActiveWindow.View.Slide
For iCount = 1 To 10
Set chtShape = sld.Shapes.AddChart(xlLine)
Set xlWkbk = chtShape.Chart.ChartData.Workbook
With xlWkbk
.Sheets(1).Range("A2").Value = "Test 1"
.Sheets(1).Range("A3").Value = "Test 2"
.Sheets(1).Range("A4").Value = "Test 3"
End With
chtShape.Chart.Refresh
xlWkbk.Close False
Next iCount
xlApp.Quit
End Sub

Duplicate an Excel chart and move it to another sheet

I am using the C# Excel interop and I want to create a copy of a chart from one sheet but I want this copy on another sheet. I have tried the following:
Excel.ChartObject chartTemplate = (Excel.ChartObject)sheetSource.ChartObjects("chart 1");
object o = chartTemplate.Duplicate();
Excel.ChartObject chart = (Excel.ChartObject)sheetSource.ChartObjects("chart 2");
chart.Name = "Skew" + expiry.ToString("MMMyy");
range = sheetDestination.Range["T" + chartRowCoutner.ToString()];
chart.Chart.Location(Excel.XlChartLocation.xlLocationAsObject, range);
But when I try this, the last line throws an error:
An unhandled exception of type 'System.Exception' occurred in projectname.exe
Additional information: Error reading Excel file C:\ ...the file path...\template.xlsx: Value does not fall within the
expected range.
I have also tried passing a sheet in instead of a range:
chart.Chart.Location(Excel.XlChartLocation.xlLocationAsObject, sheetDestination);
but this gives the same error. I can't understand the reason for the error or how to fix it / bypass it.
I am trying to avoid bringing the clipboard into this, but even if I try copying and pasting, I can still only paste it as an image, which is really not ideal:
Excel.ChartArea chartArea = chart.ChartArea;
chartArea.Copy();
range = sheetDestination.Range["T" + chartRowCoutner.ToString()]; // Note that chart is not on the sheet sheetDestination
range.PasteSpecial(Excel.XlPasteType.xlPasteAll);
The only other solution I can think of now is to do this in VBA and then execute the macro via the interop. But surely it can be done in a clean way just using the interop without the clipboard.
You've already got the solution but instead of giving you a fish for a day I'll give you a proper answer that will help you with any C# Excel coding task.
The C# Interop Model for Excel is almost identical to the VBA Excel Model.
This means it's trivial to convert VBA recorded macros to C#. Let's try this with an exercise like moving a chart to a different sheet.
In the Developer Tab in Excel click Record Macro > right click Chart > select Move Chart > choose Object in: Sheet2 > click OK > click Stop Macro Recording.
To see the recorded Macro press Alt + F11 to bring up the VB Editor:
See in the above screenshot how VBA shows you the second parameter for Location() is Name and it's actually a string argument...
Let's convert this VBA Macro to C#:
EDIT by #Ama
The advice below is outdated, there's actually no need to worry about releasing COM objects, this is done automatically at RELEASE mode (DEBUG mode does not). See Hans Passant's answer to "Clean up Excel Interop Objects with IDisposable".
The trick here is: never use 2 dots with com objects.
Notice how I could have written:
var sheetSource = workbookWrapper.ComObject.Sheets["Sheet1"];
but that has two dots, so instead I write this:
var workbookComObject = workbookWrapper.ComObject;
var sheetSource = workbookComObject.Sheets["Sheet1"];
Ref: How do I properly clean up Excel interop objects?
You will see the AutoReleaseComObject code in the above QA that projects like VSTOContrib use.
Here is the complete code:
using Microsoft.Office.Interop.Excel;
...
var missing = Type.Missing;
using (AutoReleaseComObject<Microsoft.Office.Interop.Excel.Application> excelApplicationWrapper = new AutoReleaseComObject<Microsoft.Office.Interop.Excel.Application>(new Microsoft.Office.Interop.Excel.Application()))
{
var excelApplicationWrapperComObject = excelApplicationWrapper.ComObject;
excelApplicationWrapperComObject.Visible = true;
var excelApplicationWrapperComObjectWkBooks = excelApplicationWrapperComObject.Workbooks;
try
{
using (AutoReleaseComObject<Workbook> workbookWrapper = new AutoReleaseComObject<Workbook>(excelApplicationWrapperComObjectWkBooks.Open(#"C:\Temp\ExcelMoveChart.xlsx", false, false, missing, missing, missing, true, missing, missing, true, missing, missing, missing, missing, missing)))
{
var workbookComObject = workbookWrapper.ComObject;
Worksheet sheetSource = workbookComObject.Sheets["Sheet1"];
ChartObject chartObj = (ChartObject)sheetSource.ChartObjects("Chart 3");
Chart chart = chartObj.Chart;
chart.Location(XlChartLocation.xlLocationAsObject, "Sheet2");
ReleaseObject(chart);
ReleaseObject(chartObj);
ReleaseObject(sheetSource);
workbookComObject.Close(false);
}
}
finally
{
excelApplicationWrapperComObjectWkBooks.Close();
ReleaseObject(excelApplicationWrapperComObjectWkBooks);
excelApplicationWrapper.ComObject.Application.Quit();
excelApplicationWrapper.ComObject.Quit();
ReleaseObject(excelApplicationWrapper.ComObject.Application);
ReleaseObject(excelApplicationWrapper.ComObject);
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
private static void ReleaseObject(object obj)
{
try
{
while (System.Runtime.InteropServices.Marshal.ReleaseComObject(obj) > 0);
obj = null;
}
catch (Exception ex)
{
obj = null;
Console.WriteLine("Unable to release the Object " + ex.ToString());
}
}
I know Releasing all the Objects, using GC.Collect and not using two dots when assigning seems over the top but at least when I quit the instance of Excel the process is freed, I don't have to programmatically kill the Excel process!
Ref: Microsoft KB: Office application does not quit after automation from .NET client
From the MSDN documentation here:
https://msdn.microsoft.com/en-us/library/microsoft.office.tools.excel.chart.location.aspx
it states that for the Name parameter of type object:
Name
Type: System.Object
The name of the sheet where the chart is embedded if Where is xlLocationAsObject or the name of the new sheet if Where is xlLocationAsNewSheet.
This is somewhat misleading from the example at the bottom of the same linked page. It would appear from the example given, that you should actually pass a string of the sheet name. The pertinent line from the example is copied below (the example is for copying to a new sheet):
chart1.Location(Excel.XlChartLocation.xlLocationAsNewSheet,
"Sales");
So, for moving to an existing sheet, I would do:
chart1.Location(Excel.XlChartLocation.xlLocationAsObject,
"ExistingSheetName");
Do NOT pass a range, workbook or worksheet object. Try a string of the sheet name.
Now, from the same MSDN document page linked above, if you want to reposition the chart within the page once you have moved it to another sheet, there are additional instructions, repeated here for convenience:
If you want to move a chart to another position on a sheet, use the P:Microsoft.Office.Interop.Excel.ChartArea.Top property and P:Microsoft.Office.Interop.Excel.ChartArea.Left property of the ChartArea. You can get the ChartArea object of the Chart by using the ChartArea property.
If you're moving a chart to an existing sheet, be careful not to overlap your chart over existing data. If so, you will have to code around that separately.
This isn't the answer to the question you asked, but might be fruitful
if you're making a copy and editing it for different variations THIS IS NOT A SOLUTION
if you're truly just copying a chart then I recommend using Excel's "Camera" function instead. It basically creates a window into another sheet - you can do this programmatically and it's well documented, but a little known feature of excel I thought I'd be remiss if I didn't point out.
-E
If you are looking to make edits & the question is still open let me know that in a comment - I've done this before I just need to look back in my workbook and see exactly how I did it.
'Camera option is nice because it doesn't 'recalculate' the data - so I imagine it operates faster; a concern in large workbooks.

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.)

Categories