2

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.

Dan
  • 45,079
  • 17
  • 88
  • 157
  • Can you write some random value to that range object? – MatthewD Nov 18 '15 at 14:42
  • @MatthewD Yes. I just tested replacing the erroring line with `range.Value = "TESTING";` and it works fine. – Dan Nov 18 '15 at 14:45
  • Hey it looks like there is a chart.copy method. https://msdn.microsoft.com/en-us/library/microsoft.office.interop.excel.charts.copy.aspx – MatthewD Nov 18 '15 at 14:47
  • Here is another option with the chart area. http://stackoverflow.com/questions/25120862/copy-excel-chart-sheet-to-clipboard-using-c-sharp – MatthewD Nov 18 '15 at 14:49
  • @MatthewD I will try that as a last resort but if I can keep the clipboard out of this, I would prefer that. It looks to me like the [`.Location()` method](https://msdn.microsoft.com/en-us/library/microsoft.office.tools.excel.chart.location.aspx) is made for moving charts across sheets, but I can't find any examples. – Dan Nov 18 '15 at 14:49
  • I've also tried `chart.Chart.ChartArea.Copy(); range.PasteSpecial(Excel.XlPasteType.xlPasteAll);` but it seems to paste the charts as pictures rather than as charts... – Dan Nov 18 '15 at 15:06

3 Answers3

3

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.

enter image description here

To see the recorded Macro press Alt + F11 to bring up the VB Editor:

enter image description here

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#:

enter image description here

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

halfer
  • 19,824
  • 17
  • 99
  • 186
Jeremy Thompson
  • 61,933
  • 36
  • 195
  • 321
  • @Dan if you're doing this on a server may I suggest using [ClosedXML](https://closedxml.codeplex.com/) instead. – Jeremy Thompson Dec 03 '15 at 08:39
  • Silly question, but what constitutes a server? – Dan Dec 03 '15 at 08:40
  • Is your Dev box running a server OS? or Excel file stored on a server? The 0x80010105 RPC_E_SERVERFAULT means the **server threw an exception** – Jeremy Thompson Dec 03 '15 at 08:45
  • Hang on.. do you have Excel installed on your Dev PC? – Jeremy Thompson Dec 03 '15 at 08:48
  • Hmm, does it work when the excel file is not embedded in the assembly? – Jeremy Thompson Dec 03 '15 at 08:54
  • I can try that, but by the time it hits the error, it's working with a file on disk and the embedded file should have nothing to do with it anymore. But I will try an absolute reference to the template just to see... – Dan Dec 03 '15 at 09:02
  • If that fails, best to upload a small bare bones repro for us and we'll take a look first hand – Jeremy Thompson Dec 03 '15 at 09:03
  • Tried changing it to `string pathTemplate = @"C:\Working\In Development\Dan\Vol Report\name.xlsx";` and still get the same error :( I can try make a minimal project that replicates the error, where should I upload it to? – Dan Dec 03 '15 at 09:05
  • The example is almost ready, I see no that the server error is getting thrown one line earlier actually at `Excel.Shape shape = shapes.Item(chartCopy.Name);` and not at `shape.Top = range.Top;` – Dan Dec 03 '15 at 09:42
  • OK so the minimal example is ready: http://tempsend.com/714AC6441E. I have managed to fix it. The error is thrown by `chartCopy.Name`! Not sure why `chartArea.Top = range.Top;` didn't work. I've also commented out another for loop based solution in there. It all seems a bit convoluted though :( Now to try get it working on the actual project... – Dan Dec 03 '15 at 10:03
  • I'm interested to hear if you get the same server error on your side? – Dan Dec 04 '15 at 06:19
  • Unfortunately I cannot reproduce the problem, it works: http://i.imgur.com/wlHbTNs.png and http://i.imgur.com/8ocfQJ5.png. My advice is to quickly run ProcessMonitor whilst executing the LOC and check what is in the logs - I'm still not convinced something server-side is causing the problem. I wish I could help more mate, let me know if there's anyway I can do that? – Jeremy Thompson Dec 04 '15 at 08:29
  • 1
    Apologies for the confusion, the code as it stands does work. To get the error, you need to change the line `Excel.Shape shape = shapes.Item("Chart Copy");` to `Excel.Shape shape = shapes.Item(chartCopy.Name);` – Dan Dec 04 '15 at 08:33
  • It doesn't work that way, you cant create an instance of the abstract class or interface Microsoft.Office.Interop.Excel.ChartObject. Its a bit of misnomer but you cant change the name - each object in an Excel Spreadsheet has a unique name and in this scenario it wont work to change the name `chartCopy.Name = "Chart Copy";` and then refer to the object by Chart Copy. Sorry about that. **Solution** Record the name of the chartCopy (Chart1) and use that when you refer to it in the `shapes.Item("...");` code. – Jeremy Thompson Dec 04 '15 at 08:43
  • Sorry not sure I've followed, `chartCopy.Name = "Chart Copy";` does change the name of the chart in the output spread sheet. I'm a bit confused as to why I can write to `.Name` here but then later on not read from it. – Dan Dec 04 '15 at 09:11
2

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.

HBomb
  • 1,117
  • 6
  • 9
  • Thanks, I will try this tomorrow. It seems crazy to have to pass the name of the sheet as a string though, but stranger things have happened. – Dan Dec 02 '15 at 20:24
  • @Dan - I agree. Especially since the parameter is of Type System.Object. Its not apparent to me why the method does not simply take a string parameter instead. Of course, since string inherits from object (obviously), string is valid as a parameter. It almost feels like its an error in the framework on the design of the method signature... a rarity, but not an impossibility. – HBomb Dec 02 '15 at 20:51
  • So the chart does copy to the destination sheet now. However trying to reposition it using `chartArea.Top = range.Top;` throws an error: `HRESULT: 0x80010105 (RPC_E_SERVERFAULT)` – Dan Dec 03 '15 at 08:27
  • Did you check out my answer and record a macro doing the same thing in VBA? Why is it a server fault? You're not running Automation code on a server are you? See this KB: https://support.microsoft.com/en-us/kb/257757 – Jeremy Thompson Dec 03 '15 at 08:33
  • @JeremyThompson Yes, it uses shapes. It throws the identical error: `Excel.Shapes shapes = sheetReport.Shapes; Excel.Shape shape = shapes.Item(chartCopy.Name); shape.Top = range.Top;` Although when you move things using the macro recorder it uses a function called `IncrementTop ` or something rather than setting the `Top` property directly, but I tried my C# code in VBA first and it works there. – Dan Dec 03 '15 at 08:35
  • @JeremyThompson I don't know. At the moment it's just running on my dev PC. What would make it a server? – Dan Dec 03 '15 at 08:39
  • @JeremyThompson I'm generating the file using `Path.GetTempFileName` so it should be local right? `string pathTemplate = Path.GetTempFileName(); Assembly assembly = Assembly.GetExecutingAssembly(); using (Stream input = assembly.GetManifestResourceStream("name.name.xlsx")) using (Stream output = File.Create(pathTemplate)) { input.CopyTo(output); } ` . OS is Windows 7 Professional so I don't think it's a server OS either... – Dan Dec 03 '15 at 08:46
  • @Dan - for clarity sake, and for the sake of anyone else who may come across this question in the future, your exception HRESULT: 0X80010105( RPC_E_SERVERFAULT ) is because you're using the chartArea.Top Property wrong. It takes a double value and it is defined as the distance, in points, from the top edge of the object to the top of row 1 (on a worksheet) or the top of the chart area (on a chart). – HBomb Dec 07 '15 at 17:43
  • @Dan - see this link for proper usage: https://msdn.microsoft.com/en-us/library/microsoft.office.interop.excel.chartarea.top.aspx – HBomb Dec 07 '15 at 17:44
  • @Dan the range.Top property actually appears to be of type system.Object. So you probably just needed to do an explicit conversion to avoid the error (so long as your range variable was populated and valid). something like chartArea.Top = (double)range.Top; – HBomb Dec 07 '15 at 17:47
1

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.

Schalton
  • 2,867
  • 2
  • 32
  • 44