So, I have a class that I wrote to handle opening excel files, however excel doesn't close even though I call the close function. This worked before I started using multiple workbooks so I presume that its something to do with that but I have no idea how to fix this. Any help appreciated!
This is the code:
public class ExcelReaderClass
{
public Excel.Application? excelApp = null; // create a null excell object that will later be used to store the reference to the open excel app
public Excel.Workbooks? excelBooks = null; // create a null object collection that will contain all workbooks
public Excel.Workbook? excelBook = null; // create a null object that will contain a workbook (excel file)
public Excel.Worksheet? curWorksheet = null; // create a null worksheet object that will contain the reference to the current worksheet that is open
public Excel.Range? curWorksheetRange = null; // create a range object that will contain the data from the currently open worksheet
// function that opens excel file and opens excel if it isnt already open
public void openExcelFile(string filePath) // filePath = filepath for the worksheet that is to be opened
{
if(excelApp == null) // if excel isnt open
{
excelApp = new Excel.Application(); // create a new instance of excel
excelApp.DisplayAlerts = false; // prevents "do you want to save" popups, which we want because we are only looking at files not changing them
excelBooks = excelApp.Workbooks; // create a new instance of workbook object collection
}
excelBook = excelBooks.Add(filePath); // add the workbook to the collection
}
// function that closes all instances of excel opened by this program
public void closeExcelFile(bool save, bool closeAll) // bool that controls whether only the worksheet is closed or the whole instance of excel
{
//excelBook.Save(); // save function not needed in this instance but might be useful later
GC.Collect();
GC.WaitForPendingFinalizers();
foreach (Excel.Workbook workbook in excelBooks)
{
workbook.Close(save, System.Reflection.Missing.Value, System.Reflection.Missing.Value); // close the workbook
NAR(workbook);
}
NAR(excelBooks); ; // release the workbook collection from Com
NAR(excelBook);
NAR(curWorksheet);
NAR(curWorksheetRange);
if (closeAll) // if close all is true, we want to close the excel instance too
{
excelApp.Quit(); // quit out of excel
NAR(excelApp); // release excel from Com
}
}
public List<object[,]> readExcelFile(string[] filePaths) //
{
List<object[,]> rangeList = new List<object[,]>(); // list of objects that will contain the data from the excel sheets
foreach (var filePath in filePaths) // for each file
{
openExcelFile(filePath); // open the file in excel
}
foreach (var workbook in excelBooks) // iterate through workbooks
{
var book = workbook as Excel.Workbook; // set book as a reference to the workbook
curWorksheet = book.Worksheets[1]; // set this variable to the first sheet in the workbook
curWorksheetRange = curWorksheet.UsedRange; // set this variable to the workbook range
object[,]? range = (object[,])curWorksheetRange.Value2;
rangeList.Add(range);
}
closeExcelFile(false,true);
return (rangeList);
}
//function to kill object and release it from COM
private void NAR(object o)
{
try
{
Marshal.FinalReleaseComObject(o);
}
catch { }
finally
{
o = null;
}
}
}
I've tried using GC.Collect() and GC.WaitForPendingFinalizers() but that doesn't seem to work either.
Update, this is what I did to fix the code. Feel free to use it. (Doesn't work in Debug mode though)
public class ExcelReaderClass
{
public Excel.Application? excelApp = null; // create a null excell object that will later be used to store the reference to the open excel app
public Excel.Workbooks? excelBooks = null; // create a null object collection that will contain all workbooks
public Excel.Workbook? excelBook = null; // create a null object that will contain a workbook (excel file)
public Excel.Worksheet? curWorksheet = null; // create a null worksheet object that will contain the reference to the current worksheet that is open
public Excel.Range? curWorksheetRange = null; // create a range object that will contain the data from the currently open worksheet
// function that opens excel file and opens excel if it isnt already open
public void openExcelFile(string filePath) // filePath = filepath for the worksheet that is to be opened
{
if(excelApp == null) // if excel isnt open
{
excelApp = new Excel.Application(); // create a new instance of excel
excelApp.DisplayAlerts = false; // prevents "do you want to save" popups, which we want because we are only looking at files not changing them
excelBooks = excelApp.Workbooks; // create a new instance of workbook object collection
}
excelBook = excelBooks.Add(filePath); // add the workbook to the collection
}
// function that closes all instances of excel opened by this program
public void closeExcelFile(bool save, bool closeAll) // bool that controls whether only the worksheet is closed or the whole instance of excel
{
foreach (Excel.Workbook workbook in excelBooks)
{
workbook.Close(save, System.Reflection.Missing.Value, System.Reflection.Missing.Value); // close the workbook
}
excelBooks = null; // clear the reference to the workbook collection
excelBook = null; // clear the reference to the active workbook
curWorksheet = null; // clear the reference to the current worksheet
curWorksheetRange = null; // clear the reference to the range of the current worksheet
if (closeAll) // if close all is true, we want to close the excel instance too
{
excelApp.Quit(); // quit out of excel
excelApp = null;
}
}
public List<object[,]> readExcelFile(string[] filePaths)
{
List<object[,]> rangeList = new List<object[,]>();
foreach (var filePath in filePaths)
{
openExcelFile(filePath);
}
Console.WriteLine();
foreach (var workbook in excelBooks) // done to select the first workbook because other methods have failed but this works
{
var book = workbook as Excel.Workbook; // set book as a reference to the workbook
curWorksheet = book.Worksheets[1]; // set this variable to the first sheet in the workbook
curWorksheetRange = curWorksheet.UsedRange; // set this variable to the workbook range
object[,]? range = (object[,])curWorksheetRange.Value2;
rangeList.Add(range);
}
closeExcelFile(false,true);
GC.Collect();
GC.WaitForPendingFinalizers();
return (rangeList);
}
}
Quick Rundown:
I deleted the NAR function and any places it was called, added GC.Collect() and GC.WaitForPendingFinalisers after all my excel code is executed, and made sure all variables are set to null before GC.* functions are called.
Related
I'm writing a VSTO for excel. I have the following problem:
All this is going in the excel application
I need to programmatically add a new worksheet that must look the same as a template. I've got a template from which I can copy cells and formatting to the new added cell.
How can I do this?
I have tried the following way, to open an excel application making it invisible to user and opening the necessary template in that application. I'm going through the used range rows and trying to copy row by row. However, I encounter problems in opening the template. Once it gets opened, the other time it throws COM Exception(don't know what kind of that is).
var activeExcel = (Workbook)Globals.ThisAddIn.Application.ActiveWorkbook;
Sheet = (Worksheet) activeExcel.Worksheets.Add();
Sheet.Name = "Счёт-фактура";
var sourcePath = LocationHelperTool.GetTemplatePathByName("SystemInvoice.xlsx");
try
{
var excelApp = new Application() { Visible = false };
var workbook = excelApp.Workbooks.Open(sourcePath);
var workSheets = workbook.Worksheets;
const string sourceSheetName = "Счёт-фактура";
var sourceSheet = (Worksheet)workSheets.Item[sourceSheetName];
var sourceRange = sourceSheet.UsedRange;
for (var i = 1; i <= sourceRange.Rows.Count; i++)
{
var soRange = sourceRange.Rows[i];
var deRange = Sheet.Rows[i];
soRange.Copy(Type.Missing);
deRange.pasteSpecial(XlPasteType.xlPasteFormats);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
finally
{
Clipboard.Clear();
excelApp.Quit();
}
I want to open a new sheet in the excel instance that the user is interacting and that sheet should be an exact clone of the template
The reason the code runs only once appears to be because the COM objects it instantiates are not being released - so they can't be re-used:
var excelApp = new Application() { Visible = false };
var workbook = excelApp.Workbooks.Open(sourcePath);
var workSheets = workbook.Worksheets;
const string sourceSheetName = "Счёт-фактура";
var sourceSheet = (Worksheet)workSheets.Item[sourceSheetName];
In the clean-up for this section of code you need something along these lines, where the objects are released in the reverse order they were instantiated (when the later depends on the earlier). Then the released objects need to be garbage-collected in order to ensure the code can no longer "see" them.
sourceRange = null;
soRange = null;
deRange = null;
workbook.Close(false); //do not save changes
sourceSheet = null;
workSheets = null;
workbook = null;
excelApp.Quit();
excelApp = null;
GC.Collect();
GC.AwaitPendingFinalizers();
GC.Collect();
GC.AwaitPendingFinalizers();
So I have this code for a window:
public partial class List : Window
{
DataTable table = null;
ExcelWorksheet ws = null;
string user = System.Environment.UserName;
public void Initialize()
{
string path = "Log.xlsx";
FileInfo file = new FileInfo(path);
try
{
if (File.Exists(path))
{
using (ExcelPackage pack = new ExcelPackage(file))
{
bool sheetfound = false;
//runs through each sheet to find a specific one
foreach (ExcelWorksheet sheet in pack.Workbook.Worksheets)
{
if (sheet.Name.Equals(user))
{
sheetfound = true;
ws = pack.Workbook.Worksheets[user];
break;
}
}
//Creates new sheet if it hasn't found the specific one
if (!(sheetfound))
{
ws = MainWindow.Create_Worksheet(pack);
pack.Save();
}
}
}
else
{
using (ExcelPackage pack = new ExcelPackage(file))
{
ExcelWorksheet ws = MainWindow.Create_Worksheet(pack);
pack.Save();
}
}
}
catch (Exception ex)
{
MessageBox.Show("Exception caught:\n\n" + ex as string, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
}
fUpdate(new Object, new RoutedEventArgs);
}
public void fUpdate(object sender, RoutedEventArgs e)
{
table.Rows.Clear();
MessageBox.Show(ws.Dimension.End.Row.ToString());
}
}
and this one from the main window:
public partial class MainWindow : Window
{
public static ExcelWorksheet Create_Worksheet(ExcelPackage pack)
{
ExcelWorksheet ws = pack.Workbook.Worksheets.Add(System.Environment.UserName);
ws.Cells[1, 1].Value = "Date";
ws.Cells[1, 2].Value = "Time";
ws.View.FreezePanes(2, 1);
return ws;
}
}
What this is supposed to do right now is, when the second window launches, it sets the Excel file and worksheet. I used Quickwatch to see if it works and it does work, ws gets set to the specific sheet that I wanted and ws.Dimension.End.Row returns 1. However, after it gets out of the try-catch part (once it reaches fUpdate), ws.Dimension.End.Row suddenly throws a NullReferenceException. I checked and ws is still the same ExcelWorksheet object and it didn't go through anything (that I know of) that would change its value. What causes this error? Thanks!
(ws returns the ExcelWorksheet object but ws.Dimensions return the exception)
The Dimension object of the ExcelWorksheet will be null if the worksheet was just initialized and is empty.
For example:
ExcelWorksheet worksheet = new ExcelPackage().Workbook.Worksheets.Add("Sheet1");
Console.WriteLine(worksheet.Dimension.End.Row);
This code will throw a NullReferenceException since the Dimension object is null.
On the other hand:
ExcelWorksheet worksheet = new ExcelPackage().Workbook.Worksheets.Add("Sheet1");
worksheet.Cells[1, 1].Value = "Some text value";
Console.WriteLine(worksheet.Dimension.End.Row);
This code will not throw an exception since the Dimension object was initialized by adding content to the worksheet.
If the loaded ExcelWorksheet already contains data, you will not face this issue.
You may get NullReferenceException if your file doesn't exists. In this case you're getting into ELSE block and assigning created WorkSheet to a local method variable instead of class variable.
using (ExcelPackage pack = new ExcelPackage(file))
{
// ExcelWorksheet ws = MainWindow.Create_Worksheet(pack); // wrong
ws = MainWindow.Create_Worksheet(pack); // right
pack.Save();
}
For anyone coming along years later like me and facing this issue. This also happens if the ExcelPackage is disposed.
Specifically I had a method where I had
using ExcelPackage excelPackage = new ExcelPackage(fi);
return excelPackage.Workbook.Worksheets.First();
This returns a worksheet, but because the ExcelPackage is disposed when the method is exited, the Dimensions property is null (as are many other properties).
I am creating a WPF app. When it first loads up it reads in data from 3 different excel files using interop if it is left to its own devices everything is fine and all the services are ended properly however if you close the app before it finishes reading and gets to release everything the services will remain open until they are manually shut down.
Heres a few example of the code I have in place so you can get an idea of how I am doing everything.
This is how I call methods in my Excel class *note my Excel class is static
Parallel.Invoke(
() => _someList1 =
Excel.ReadFile1(path + "ExcelFile1.xlsx"),
() => _someList2 =
Excel.ReadFile2(path + "ExcelFile2.xlsx"),
() => _someList3 =
Excel.ReadFile3(path + "ExcelFile3.xlsx"));
Here is example of methods in my Excel Class
public static IList<SomeObject> ReadFile1(string absolutePath)
{
object[,] values = StartRead(absolutePath);
for (int i = 2; i < values.GetLength(0) + 1; i++)
{
//doing stuff in here
}
return mylist<SomeObject>;
}
private static object[,] StartRead(string absolutePath)
{
Application excel = new Application();
Workbook workBook = excel.Workbooks.Open(absolutePath);
Worksheet sheet = (Worksheet)workBook.Worksheets[1];
Range range = sheet.Range["A1"];
range = range.End[XlDirection.xlToRight];
range = range.End[XlDirection.xlDown];
string downAddress = range.Address[false, false];
range = sheet.Range["A1", downAddress];
object[,] values = (object[,])range.Value2;
workBook.Close(true);
excel.Quit();
ReleaseExcel(range, sheet, workBook, excel);
return values;
}
private static void ReleaseExcel(Range range, Worksheet worksheet, Workbook workbook, Application application)
{
if (range != null)
{
Marshal.ReleaseComObject(range);
}
if (worksheet != null)
{
Marshal.ReleaseComObject(worksheet);
}
if (workbook != null)
{
Marshal.ReleaseComObject(workbook);
}
if (application != null)
{
Marshal.ReleaseComObject(application);
}
}
So there problem is clearly happening if the app is closed before it gets a chance to call ReleaseExcel() is there some way I can get the wpf app to wait until all its excel services are released before closing.
edit** and by excel services I meant processes
I have an Excel file.
I need to open it, select specific sheets from it, and convert those sheets to a PDF format. I am able to convert the whole excel file, I just don't know how to convert only the specific sheets.
My idea is to copy specific sheets from an existing file to a new temporary file, and convert that whole new temporary file to PDF.
Maybe there's an easier way?
My code so far is =>
using Word = Microsoft.Office.Interop.Word;
using Excel = Microsoft.Office.Interop.Excel;
public static void ExportExcel(string infile, string outfile, int[] worksheets)
{
Excel.Application excelApp = null;
Excel.Application newExcelApp = null;
try
{
excelApp = new Excel.Application();
excelApp.Workbooks.Open(infile);
//((Microsoft.Office.Interop.Excel._Worksheet)excelApp.ActiveSheet).PageSetup.Orientation = Microsoft.Office.Interop.Excel.XlPageOrientation.xlLandscape;
excelApp.ActiveWorkbook.ExportAsFixedFormat(Excel.XlFixedFormatType.xlTypePDF, outfile);
}
finally
{
if (excelApp != null)
{
excelApp.DisplayAlerts = false;
excelApp.SaveWorkspace();
excelApp.Quit();
}
}
}
Maybe the ExportAsFixedFormat method can be set to consider only specific pages (sheets) while converting?
If not, how do I copy the sheets from one file to another?
Thanks!
You might be able to just print the sheets you want from the original file. I fired up the Macro Recorder, selected a couple of sheets, and Saved As to PDF. Here's the code:
Sheets(Array("Sheet1", "Sheet2")).Select
ActiveSheet.ExportAsFixedFormat Type:=xlTypePDF, Filename:= _
"C:\Users\doug\Documents\Book1.pdf", Quality:=xlQualityStandard, _
IncludeDocProperties:=True, IgnorePrintAreas:=False, OpenAfterPublish:=True
When I changed the selected sheets and ran again it worked as expected.
What's strange is that in the actual Save As dialog, you can go to Options and check "Selected Sheets." That's not available as a parameter to ExportAsFixedFormat, but it was automatically selected in the dialog, and maybe is the default also when called.
You could simply copy the file to the new destination, open the destination, remove the unwanted sheets and export. This is an example (tested) of my idea.
// infile is the excel file, outfile is the pdf to build, sheetToExport is the name of the sheet
public static void ExportExcel(string infile, string outfile, string sheetToExport)
{
Microsoft.Office.Interop.Excel.Application excelApp = new
Microsoft.Office.Interop.Excel.Application();
try
{
string tempFile = Path.ChangeExtension(outfile, "XLS");
File.Copy(infile, tempFile, true);
Microsoft.Office.Interop.Excel._Workbook excelWorkbook =
excelApp.Workbooks.Open(tempFile);
for(int x = excelApp.Sheets.Count; x > 0; x--)
{
_Worksheet sheet = (_Worksheet)excelApp.Sheets[x];
if(sheet != null && sheet.Name != sheetToExport)
sheet.Delete();
}
excelApp.ActiveWorkbook.ExportAsFixedFormat(XlFixedFormatType.xlTypePDF, outfile);
}
finally
{
if (excelApp != null)
{
excelApp.DisplayAlerts = false;
excelApp.SaveWorkspace();
excelApp.Quit();
}
}
}
I have an excel workbook opened via double-clicking it in windows explorer but cannot access it in code
Excel.Application xlApp = (Application)Marshal.GetActiveObject("Excel.Application");
Excel.Workbooks xlBooks = xlApp.Workbooks;
xlBooks.Count equals 0, why isn't it referencing my opened workbook?
EDIT
Here are the various scenarios and what is happening:
Scenario 1: If the file is not already open
Code opens workbook, I am happy.
Scenario 2: If the file is initially opened from code and I close and reopen the app
Code references file just fine xlBooks.Count equals 1, I am happy.
Scenario 3: If the file is initially opened not from code, and via double-clicking it in explorer
Code opens another instance of the file xlBooks.Count equals 0, I am in a rage!
Here is the entire code as it stands right now
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;
public class ExcelService : IExcelService
{
const string _filePath = #"C:\Somewhere";
const string _fileName = #"TestFile.xlsb";
string _fileNameAndPath = Path.Combine(_filePath, _fileName);
Application xlApp;
Workbooks xlBooks;
Workbook xlBook;
Worksheet xlSheet;
public ExcelService()
{
try
{
xlApp = (Application)Marshal.GetActiveObject("Excel.Application");
xlBooks = xlApp.Workbooks;
var numBooks = xlBooks.Count;
Log.Info("Number of workbooks: {0}".FormatWith(numBooks));
if (numBooks > 0)
{
xlBook = xlBooks[1];
Log.Info("Using already opened workbook");
}
else
{
xlBook = xlBooks.Open(_fileNameAndPath);
Log.Info("Opening workbook: {0}".FormatWith(_fileNameAndPath));
}
xlSheet = (Worksheet)xlBook.Worksheets[1];
// test reading a named range
string value = xlSheet.Range["TEST"].Value.ToString();
Log.Info(#"TEST: {0}".FormatWith(value));
xlApp.Visible = true;
}
catch (Exception e)
{
Log.Error(e.Message);
}
}
~ExcelService()
{
GC.Collect();
GC.WaitForPendingFinalizers();
try
{
Marshal.FinalReleaseComObject(xlSheet);
}
catch { }
try
{
Marshal.FinalReleaseComObject(xlBook);
}
catch { }
try
{
Marshal.FinalReleaseComObject(xlBooks);
}
catch { }
try
{
Marshal.FinalReleaseComObject(xlApp);
}
catch { }
}
}
I know this thread is a little old, but I found another way of doing this. When you create a new Excel.Applicationobject you have to create the WorkBooks object. When you access an already opened Excel file, the WorkBooks object is already created, so you just need to add a new WorkBook to the existing one. #Tipx 's solution works great if you have access to the WorkBook name, but in my case the current WorkBook name is always random. Here's the solution I came up with to get around this:
Excel.Application excelApp = null;
Excel.Workbooks wkbks = null;
Excel.Workbook wkbk = null;
bool wasFoundRunning = false;
Excel.Application tApp = null;
//Checks to see if excel is opened
try
{
tApp = (Excel.Application)System.Runtime.InteropServices.Marshal.GetActiveObject("Excel.Application");
wasFoundRunning = true;
}
catch (Exception)//Excel not open
{
wasFoundRunning = false;
}
finally
{
if (true == wasFoundRunning)
{
excelApp = tApp;
wkbk = excelApp.Workbooks.Add(Type.Missing);
}
else
{
excelApp = new Excel.Application();
wkbks = excelApp.Workbooks;
wkbk = wkbks.Add(Type.Missing);
}
//Release the temp if in use
if (null != tApp) { Marshal.FinalReleaseComObject(tApp); }
tApp = null;
}
//Initialize the sheets in the new workbook
Might not be the best solution but it worked for my needs. Hope this helps someone. :)
If all your workbooks are opened in the same Excel instance (you can check this by checking if you can switch from one to the other using Alt-tab). You can simply refer to the other using Workbooks("[FileName]"). So, for example :
Dim wb as Workbook //for C#, type Excel.Workbook wb = null;
Set wb = Workbooks("MyDuperWorkbook.xlsx") //for C#, type wb = Excel.Workbooks["MyDuperWorkbook.xlsx"];
wb.Sheets(1).Cells(1,1).Value = "Wahou!"