Friday, March 23, 2012

Properly disposing COM objects in your .NET code

Here at work, we are using a third party component where they provide a COM interface.

We're using .NET code so we are using COM interop to use the COM object.

Remember back in the VB6 days when you would set your objects to null when you're done with it? Well, so did someone else here, because that's what I saw.

I'm going to show you an example using the Microsoft Excel Type Library.

Here's some sample code that I wrote that will open up an Excel file and get the text value from a cell in the first worksheet.


    using System;
 
    using Microsoft.Office.Interop.Excel;
 
    internal class Program
    {
        #region Methods
 
        private static void Main(string[] args)
        {
            var app = new Application();
            var books = app.Workbooks;
            var book = books.Open(@"C:\Temp\doc.xlsx");
            var sheets = book.Sheets;
            var sheet = sheets.Item[1] as Worksheet;
            string cellText = string.Empty;
 
            if (sheet != null)
            {
                var cells = sheet.Cells;
                var cell = cells.Item["2""B"as Range;
                if (cell != null)
                {
                    cellText = cell.Text.ToString();
                    cells = null;
                    cell = null;
                }
 
                sheet = null;
                sheets = null;
            }
 
            app.Quit();
 
            books = null;
            book = null;
            app = null;
            Console.WriteLine(cellText);
            Console.ReadLine();
        }
 
        #endregion
    }

See what I did there, I set all of the object references to null, which is how it was done back in the VB6 days, but we're using the .NET CLR. Things are different.


So what the program does is essentially fires up an instance of excel.exe, and then does what I asked it to do which in this case is to just get text from a cell.


The problem is that after my program finishes... excel.exe is still running even though I called "app.Quit()". That's because it can't quit while I still have references to the objects. Simply setting the references to null didn't do the trick. The managed runtime of the .NET CLR doesn't know how to garbage collect unmanaged COM Objects, so even though these references are null, they won't be properly disposed.


So how do you properly dispose of these references? By using Marshal.ReleaseComObject or Marshal.FinalReleaseComObject. Click on the links to read more about each method.


Here's the new version of the program:


    using System;
    using System.Runtime.InteropServices;
 
    using Microsoft.Office.Interop.Excel;
 
    internal class Program
    {
        #region Methods
 
        private static void Main(string[] args)
        {
            var app = new Application();
            var books = app.Workbooks;
            var book = books.Open(@"C:\Temp\doc.xlsx");
            var sheets = book.Sheets;
            var sheet = sheets.Item[1] as Worksheet;
            string cellText = string.Empty;
 
            if (sheet != null)
            {
                var cells = sheet.Cells;
                var cell = cells.Item["2""B"as Range;
                if (cell != null)
                {
                    cellText = cell.Text.ToString();
                    Marshal.FinalReleaseComObject(cells);
                    Marshal.FinalReleaseComObject(cell);
                }
 
                Marshal.FinalReleaseComObject(sheet);
                Marshal.FinalReleaseComObject(sheets);
            }
 
            app.Quit();
 
            Marshal.FinalReleaseComObject(books);
            Marshal.FinalReleaseComObject(book);
            Marshal.FinalReleaseComObject(app);
            Console.WriteLine(cellText);
            Console.ReadLine();
        }
 
        #endregion
    }

Give it a try yourself. Run the app and keep an eye on task manager. When the app gets to the last line, Console.ReadLine(), you'll notice that excel.exe is still running in the first version of the code. Now run the second version and wait for the program to get to the last line, you'll notice that excel.exe is not runnning. 

Useful links:
Mixing deterministic and non-deterministic cleanup


Tuesday, March 20, 2012

Debugging .NET Windows Service apps more easily

Here at work, I keep seeing a couple of my fellow developers struggle to debug their Windows Service apps that they're writing.  What they have to do is remove the service, install the new version of the service, start the service, then attach the debugger.

If there's an error when the service first starts up, the workaround is to introduce a Thread.Sleep before it actually does anything, giving you enough time to attach the debugger.

By default, you can't directly run the service from within Visual Studio. If you try, you get the following message:



If you look at the code that actually starts the service, you'll see that it's not that different from a console app.

By default, it looks something like this:

    internal static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        private static void Main()
        {
            ServiceBase[] ServicesToRun;
            ServicesToRun = new ServiceBase[] { new Service1() };
            ServiceBase.Run(ServicesToRun);
        }
    }
And the Service1 class looks like this:

    public partial class Service1 : ServiceBase
    {
        public Service1()
        {
            this.InitializeComponent();
        }
 
        protected override void OnStart(string[] args)
        {
            Console.WriteLine("Hello, world!");
        }
 
        protected override void OnStop()
        {
        }
    }
The code that runs when the service Starts happens in OnStart() .  There are other events that you can override, such as OnPowerEvent, OnSessionChange, OnShutdown, but for the most part, you'd primarily be concerned about OnStart.

So how can we get a Windows Service to run like a console app?  Pretty simply actually. I'm just going to post the solution here and walk you through it:

    internal static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        private static void Main(string[] args)
        {
            var service = new Service1();
            if (Environment.UserInteractive)
            {
                service.Start(args);
                Thread.Sleep(Timeout.Infinite);
            }
            else
            {
                var servicesToRun = new ServiceBase[] { service };
                ServiceBase.Run(servicesToRun);
            }
        }
    }


First, you should change the signature of Main() to include an array of arguments.
  private static void Main(string[] args)
Next, we'll want to create a new instance of Service1 and keep that in a variable.
var service = new Service1();
Now comes the magic part.  We want to be able to check whether the app is being ran as an application, or as a service.  How can we determine that?  Simple:
if (Environment.UserInteractive)
The above code checks if the application is being ran by the user, or as a service.  If it's being ran by the user, then we want to treat it like a console application, otherwise, run it as a service.  This gives us the flexibility of being able to run it in two different modes.

Next, you'll see me calling a method on Service1 called "Start" instead of "OnStart".  The problem is that OnStart is protected, which means we can only access that method from derived classes, and since Program is not deriving Service1, we can't access that method.  The simplest way to be able to call the "OnStart" method would be to just add a public method to Service1 that will do it for us:

        public void Start(string[] args)
        {
            this.OnStart(args);
        }

And now you can run and debug your service from Visual Studio without having to go through the trouble of installing it.  Once you feel that you have it working, you can then install it by creating a Installer Project, or manually installing it using the "installutil".