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.
Related
I need to create a "Document level customization" with C# code (not and Excel Add-in!)
I created a Visual Studio Office Excel 2010 Workbook project type. This creates a workbook with 3 sheets in my project. I added some "configuration" information to one of those sheets.
I need to access this configuration information programmatically (Sheet1 contains a button - pressing on that button should
load configuration data
open a WinForm
present configuration data on that form,
but somehow I can't find how to do that...
If I try to initialize Sheet1 class, compiler expects two parameters - Microsoft.Office.Tools.Excel.Factory and IServiceProvider, but I am calling this from a button that is placed on Sheet2 - so it's after Excel Workbook is already opened... shouldn't Sheet1 be initialized automatically?
So, how can I access Sheet1 from my VSTO project's c# code?
EDIT
Please see project sample screencast here
I have a button on Sheet2, that should
load some data from Sheet1
initialize WinForm
add it as a DataSource for a ComboBox on that WinForm
I can not find a way how to read data from that Sheet1...
It seems, that there are not a lot developers (at least at stackoverflow) that work with Excel workbooks in Visual Studio / VSTO), but still this is is how I got this basic stuff working - in case if this is helpful to someone else
Since my code was in the Worksheet's *.cs file it turned out I can access project's xlsx file this way:
var excel = (Excel.Application)this.Application;
var xlbook = (Excel.Workbook)excel.ActiveWorkbook;
var worksheets = xlbook.Worksheets;
var sheet = (Excel.Worksheet)worksheets["Sheet3"];
int row = 2;//1st row for column titles
while (!string.IsNullOrEmpty(((Excel.Range)sheet.Cells[row, 2]).Value))
{
var weight = ((Excel.Range)sheet.Cells[row, 3]).Value;
row++;
}
Some additional things about processing data from Excel sheet in c# code, I found out (maybe that's helpful for someone):
the .NET type for Excel Cell is Excel.Range (at least I didn't
find any other option)
when reading cell that is empty in Excel file, it's value on .NET side is null, not ""
values that seem to be strings on Excel side - can turn to be different types when loaded on c# side. I don't know if it's the best way, but I solved it like this:
var weight = (((Excel.Range)sheet.Cells[row, 3]).Value);
if (weight is double)
{
product.Weight = ((double)((Excel.Range)sheet.Cells[row, 3]).Value).ToString();
}
else if (weight is string)
{
product.Weight = ((Excel.Range)sheet.Cells[row, 3]).Value;
}
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.)
Im just starting to use EPPLus Lib to create "complex" workbooks via C#, and i just ran into some trouble while trying to create two pivot tables.
The first one creates fine, but when i try to create the second one it doesnt throw any exceptions but when i try to open the worknook using excel it says
"Excel found unreadable content in 'myworkbook.xlsx'. Do you want to
recover the contents of this workbook? If you trust the source of this
workbook, clickYes"
And when i press 'yes':
Repair log ->
Removed Feature: PivotTable report from /xl/pivotTables/pivotTable2.xml part (PivotTable > view) Removed
Records: Workbook properties from /xl/workbook.xml part (Workbook)
Repaired Records: Workbook properties from /xl/workbook.xml part
(Workbook)
Here's the code that i build:
CreatePivotTable("Pivot1", "Pivot1", rng1);
CreatePivotTable("Pivot2", "Pivot2", rng2);
public void CreatePivotTable(string pivotSheet, string pivotName, ExcelRangeBase srcRange)
{
if (m_wb.Worksheets[pivotSheet] != null)
m_wb.Worksheets.Delete(pivotSheet);
var ws = m_wb.Worksheets.Add(pivotSheet);
var pivot = ws.PivotTables.Add(ws.Cells["A1"], srcRange, pivotName);
}
Any ideas?
Thanks!
What was wrong and i didnt put it in my question was that i was reopening the workbook on step before, like this:
CreatePivotTable("Pivot1", "Pivot1", rng1);
Save();
CreatePivotTable("Pivot2", "Pivot2", rng2);
private void Save()
{
m_writer.Save();
m_writer.OpenWorkbook ();
}
And since the save method of epplus closes the workbook, the program lost some sort of reference or just got lost with some info.
In short, to use epplus correctly, you should write everything u need before saving and closing the workbook, its not good to reopen.
Thank you.
UPDATE1:
I am using Excel 2010 and I've searched the web and found thousands upon thousands of ways to do this via win form, console, etc. But I can't find a way to do this via DLL. and none of the sample on-line is complete all in bit and pieces.
UPDATE END
I have looked and goggled but did not get the specific what i am looking for, as show below the excel sample sheet.
i'm looking a way to read and store the each cell data in a variable
i have started something like this:
Workbook workbook = open(#"C:\tmp\MyWorkbook.xls");
IWorksheet worksheet = workbook.Worksheets[0];
IRange a1 = worksheet.Cells["A1"];
object rawValue = a1.Value;
string formattedText = a1.Text;
Console.WriteLine("rawValue={0} formattedText={1}", rawValue, formattedText);
Your code can work with a couple changes.
One thing to remember is that Excel worksheets are 1-based, not 0-based (and use Worksheet instead of IWorksheet):
Worksheet worksheet = workbook.Worksheets[1];
And to get a range, it is easiest to call get_Range() on the worksheet object (and use Range instead of IRange):
Range a1 = worksheet.get_Range("A1");
With those two lines of code changed, your example will work fine.
UPDATE
Here is a "complete" example:
Right-click your project in the solution explorer and click "Add
Reference".
Click on the COM tab and sort the list by Component Name. Find "Microsoft Excel 14.0 Object Library" in the list and select it. Click OK.
In the code file where you want this to run, add a using Microsoft.Office.Interop.Excel;
Use this code, which I've modified as little as possible from your example:
var excel = new Microsoft.Office.Interop.Excel.Application();
Workbook workbook = excel.Workbooks.Open(#"C:\tmp\MyWorkbook.xls");
Worksheet worksheet = workbook.Worksheets[1];
Range a1 = worksheet.get_Range("A1");
object rawValue = a1.Value;
string formattedText = a1.Text;
Console.WriteLine("rawValue={0} formattedText={1}", rawValue, formattedText);
Excel.Sheets sheets = workbook.Worksheets;
Excel.Worksheet worksheet = (Excel.Worksheet)sheets.get_Item(1);
System.Array myvalues;
Excel.Range range = worksheet.get_Range("A1", "E1".ToString());
myvalues = (System.Array)range.Cells.Value;
If you don't want to be in a war with com components and registering dlls,
the best way to read excel is Excel Reader for .NET
I have been using it for so long time , and I can say it just works.
and excelReader.IsFirstRowAsColumnNames property makes everything easy.
You can play your data within a dataset.
This was working..and I moved the disposal code to the finally block, and now it fails every time.
I have a test spreadsheet with 4 records, 6 columns long. Here is the code I'm using to bring it in. This is ASP .Net 3.5 on IIS 5 (my pc) and on IIS 6 (web server).
It blows up on the line right before the catch: "values = (object[,])range.Value2;" with the following error:
11/2/2009 8:47:43 AM :: Not enough storage is available to complete this operation. (Exception from HRESULT: 0x8007000E (E_OUTOFMEMORY))
Any ideas? Suggestions? I got most of this code off codeproject, so I have no idea if this is the correct way to work with Excel. Thanks for any help you can provide.
Here is my code:
Excel.ApplicationClass app = null;
Excel.Workbook book = null;
Excel.Worksheet sheet = null;
Excel.Range range = null;
object[,] values = null;
try
{
// Configure Excel
app = new Excel.ApplicationClass();
app.Visible = false;
app.ScreenUpdating = false;
app.DisplayAlerts = false;
// Open a new instance of excel with the uploaded file
book = app.Workbooks.Open(path);
// Get first worksheet in book
sheet = (Excel.Worksheet)book.Worksheets[1];
// Start with first cell on second row
range = sheet.get_Range("A2", Missing.Value);
// Get all cells to the right
range = range.get_End(Excel.XlDirection.xlToRight);
// Get all cells downwards
range = range.get_End(Excel.XlDirection.xlDown);
// Get address of bottom rightmost cell
string downAddress = range.get_Address(false, false, Excel.XlReferenceStyle.xlA1, Type.Missing, Type.Missing);
// Get complete range of data
range = sheet.get_Range("A2", downAddress);
// get 2d array of all data
values = (object[,])range.Value2;
}
catch (Exception e)
{
LoggingService.log(e.Message);
}
finally
{
// Clean up
range = null;
sheet = null;
if (book != null)
book.Close(false, Missing.Value, Missing.Value);
book = null;
if (app != null)
app.Quit();
app = null;
}
return values;
I'm not sure if this is your issue or not, but it very well may be. You are not cleaning up your excel objects properly. They are unmanaged code and can be tricky to clean up. Finally should look something like this: And as the comments have noted working with excel from asp.net is not a good idea. This cleanup code is from a winform app:
GC.Collect();
GC.WaitForPendingFinalizers();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(range);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(sheet);
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(book);
WB.Close(false, Type.Missing, Type.Missing);
Excel.Quit();
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(Excel);
EDIT
An alternative would be to use ado.net to open the workbook.
DataTable dt = new DataTable();
string connectionString;
System.Data.OleDb.OleDbConnection excelConnection;
System.Data.OleDb.OleDbDataAdapter da;
DataTable dbSchema;
string firstSheetName;
string strSQL;
connectionString = #"provider=Microsoft.ACE.OLEDB.12.0;Data Source=" + filename + #";Extended Properties=""Excel 12.0;HDR=YES;IMEX=1""";
excelConnection = new System.Data.OleDb.OleDbConnection(connectionString);
excelConnection.Open();
dbSchema = excelConnection.GetOleDbSchemaTable(System.Data.OleDb.OleDbSchemaGuid.Tables, null);
firstSheetName = dbSchema.Rows[0]["TABLE_NAME"].ToString();
strSQL = "SELECT * FROM [" + firstSheetName + "]";
da = new OleDbDataAdapter(strSQL, excelConnection);
da.Fill(dt);
da.Dispose();
excelConnection.Close();
excelConnection.Dispose();
Creating/destroying excel on every request will have absolutely terrible performance to matter what you do. In general running any Office app using automation is a nasty business for a lot of reasons (see here). The only way I got it to work is to have a single instance of the app (in my case Word) which is initialized once, and then requests are queued to this instance for processing
If you can stay away from the apps and parse the file yourself (using MS libraries, of just XML)
You're going to run into a lot of trouble using interop from ASP.NET. Unless this is meant for some tiny in house application it would be advisable not to go forward with it.
Office Interop is not a programming API in the traditional sense - it's the Office macro system taken to its maximum, with the ability to work interprocess - for example an Excel macro could interact with Outlook.
Some consequences of using interop are:
You are actually opening a full copy of the office application.
Your actions are being executed by the application as if a user initiated them - that means instead of error messages being returned in code, they are displayed in the GUI.
Your copy of the application only closes if you explicitly command it - and even then errors can prevent that from actually happening (the application may present a "do you want to save" dialog if you did not programmably tell Excel that the changes do not need to be saved). This usually results in many hidden copies of Excel being left running on the system - open task manager and see how many excel.exe processes are running.
All of this makes interop something to avoid for regular desktop application, and something that should only be used as a last resort for server applications, since a GUI popup requiring action or script that leaks process is murder on a server environment.
Some alternatives include:
Using Microsoft Office 2007 XML based formats, so that you can write the XML files yourself.
Using SpreadsheetGear.Net, which is a .NET binary Excel file reader/writer (you don't need Excel installed, as it is completely stand alone). SpreadsheetGear models itself after the Intertop interfaces to make conversion of older code easier.
The error is probably exactly what it says, you are getting an out of memory error. Try to split the loading of the values array into several smaller chunks instead of getting the entire Range at one time. I tried your code out in C# and had no issues, but my spreadsheet I was loading was mostly empty.
I noticed that the Range was the entire spreadsheet though (from A2 to IV65536 or something). I'm not sure if that is intended.
One thing you could try using is sheet.UsedRange, that will cut down on the number of cells you are loading.
A couple additional small things that I have learned which you may find useful:
Use Application instead of ApplicationClass
Use Marshal.FinalReleaseComObject(range) (and also for sheet, book, app) otherwise you will have your EXCEL.EXE process sticking around.