0

This question had been asked a plethora of times, and I just want to get to know what is the best way to do this.

After reading articles and posts I found out that to correctly release COM object (I'll be using Excel), you need to release all references to COM objects. This means that whenever you use some object's property and the methods (or property) of this property, you need to release them all. I will explain it in the "way 1".

There's another way I've come up with, and it's rude - kill Excel process. However, there's a catch which I'll explain it in the "way 2".

Let's start with the initial attempt.

Say, you have the following code which merely outputs some numbers into cells and saves a workbook. Your attempt is the following:

using Excel = Microsoft.Office.Interop.Excel;

static void UseExcelNotReleased()
{
  var app = new Excel.Application { Visible = false };
  var book = app.Workbooks.Add();
  var sheet = book.Worksheets[1] as Excel.Worksheet;
  new List<int> { 1, 2, 3, 4, 5 }
    .ForEach(row =>
    {
      sheet.Cells[row, "A"].Resize[1, 2] = GetValues();
    });
  book.SaveAs("C:\\excel1.xlsx");
  book.Close();
  app.Quit();

  GC.Collect();
  GC.WaitForFullGCComplete();
  Marshal.FinalReleaseComObject(app);
  Marshal.FinalReleaseComObject(book);
  Marshal.FinalReleaseComObject(sheet);
}

This code does its job, but if we take a look at Task Manager, we'll see EXCEL.EXE process is lurking. You seem to have released all the variables which used COM objects. How come EXCEL.EXE is still in processes?

The problem starts on this line:

var book = app.Workbooks.Add();

The thing is that you used Excel.Workbooks object! When you said app.Workbooks, the Excel.Workbooks was returned to you, but you didn't store it in a variable. Thus, the reference flew away. In the end of code, where you release COM objects, this object is not mentioned and the counter is still in the game.

This way I have come up with two ways to release:

Way 1: Store all references in variables

As I mentioned above, you need to store all references to all COM objects you have used. For instance, you have this code:

rng.Interior.Color = 255; //rng is "A1" cell
Marshal.FinalReleaseComObject(rng);

The problem is that rng.Interior returns Excel.Interior object, reference to which isn't stored anywhere. So, you must write the following code:

var interior = rng.Interior;
interior.Color = 255;
Marshal.FinalReleaseComObject(interior);
Marshal.FinalReleaseComObject(rng);

So, our example must look like this:

static void UseExcelReleaseCOM()
{
  var app = new Excel.Application { Visible = false };
  var books = app.Workbooks;    //Store reference
  var book = books.Add();
  var sheets = book.Worksheets; //Store reference
  var sheet = sheets[1] as Excel.Worksheet;
  Excel.Range rng = null;       //Store reference
  new List<int> { 1, 2, 3, 4, 5 }
    .ForEach(row =>
    {
      var values = new[] { 1, 2 };
      rng = sheet.Cells[row, "A"];
      rng.Value = values[0];
      rng = sheet.Cells[row, "B"];
      rng.Value = values[1];
    });
  book.SaveAs("C:\\excel1.xlsx");
  book.Close();
  app.Quit();

  GC.Collect();
  GC.WaitForFullGCComplete();
  Marshal.FinalReleaseComObject(app);
  Marshal.FinalReleaseComObject(books);
  Marshal.FinalReleaseComObject(book);
  Marshal.FinalReleaseComObject(sheets);
  Marshal.FinalReleaseComObject(sheet);
  Marshal.FinalReleaseComObject(rng);
}

In this case we store all references in variables and release them. Now EXCEL.EXE process is gone completely.

Way 2: Killing EXCEL.EXE process

The idea is simple: as soon as we start Excel, we store its handler in a variable and search for EXCEL.EXE process using it.

static void UseExcelKillProcess()
{
  var app = new Excel.Application { Visible = true };
  var xlHandle = new IntPtr(app.Hwnd);
  var xlProc = Process.GetProcesses().First(proc => proc.MainWindowHandle == xlHandle);
  var book = app.Workbooks.Add();
  var sheet = book.Worksheets[1] as Excel.Worksheet;
  new List<int> { 1, 2, 3, 4, 5 }
    .ForEach(row =>
    {
      sheet.Cells[row, "A"].Resize[1, 2] = new[] { 1, 2 };
    });
  book.SaveAs("C:\\excel1.xlsx");
  book.Close();
  app.Quit();
  xlProc.Kill();
}

It's much shorter (for instance, we use Resize property to assign values in one fell swoop), but it has a catch: the visibility of Excel must be set to true - otherwise, you won't get it's handler!


So, here's the question: should I use explicit objects references assignments to variables or I can just terminate EXCEL.EXE process? If I choose terminate, will its handlers stay in the system?

JohnyL
  • 6,894
  • 3
  • 22
  • 41
  • 1
    Don't manually terminate Excel. Put all your Excel stuff in a function without the GC stuff. Call that function from another function. When the Excel stuff finishes being called and returns, then call the GC stuff. If you are going to manually release those objects and are doing it correctly, you don't have to call any GC functions. If you do it manually, release the last object returned first and the first object returned last. You are missing Cells being temporarily created, and also within your ForEach, temporary Range objects are being created. – Joseph Willcoxson Aug 09 '21 at 13:57
  • @JosephWillcoxson Thanks for the tip! Releasing manually all variables is daunting, so I'd better stick with separate method with Excel work! – JohnyL Aug 10 '21 at 20:28

0 Answers0