Click here to Skip to main content
15,867,756 members
Articles / Programming Languages / C#

Yes, I Know, But I Still Want a GUI for my Windows Service!

Rate me:
Please Sign up or sign in to vote.
4.75/5 (18 votes)
2 Jan 2017CPOL5 min read 49.6K   1.5K   48   18
There are lots of reasons why you might want an interface for your Windows Service. Here's how!

Download ServiceGui1.zip

Introduction

There are lots of reasons why you might want an interface for your service: monitoring service activities, running a test harness, providing ad hoc mock data to service consumers, exercising upstream interfaces, etc.  A user interface can provide useful visibility into the operation of an otherwise obscure process.

Of course, everyone knows Windows Services can't have a user interface, or can they?  In some situations you can actually coerce the service into an interactive mode, at least prior to Vista.  See this article for a complicated technique that might work for you.  However, as the author says, it's "usually not worth the effort."  WTSSendMessage can also be used if all you need is a simple message box and response.

For a more elaborate interface, you could create a separate GUI controller app that communicates with the service over some IPC channel using a custom protocol.  This will work, provided your service successfully reaches a state where it can communicate with your controller app.  Attaching a debugger to the running service is possible, but it has limitations.  And who wants to maintain two apps when (as we shall see) one is sufficient?

So let's take a step back and consider if we need the service to have a GUI, or if it would be sufficient for the service to appear to have a GUI.  If so, this article will show you how to achieve this goal and meet the following requirements:

1. You will only have to create a single app.
2. The GUI and the service will not interfere with each other in any way, unless you want them to.
3. Service consumers will be unaware that the GUI is running (unless you tell them).
4. Even if the GUI is running, the exact same code will be used to respond to service requests.

Background

Windows creates a "session" for each logged in user.  Starting with Vista, Session 0 is reserved for services and non-interactive user apps; users run in Session 1 and higher.  The switch was mainly for security reasons, to prevent user apps from accessing services that could be running with privileges.  To complete the isolation, processes running in session 0 are denied access to the graphics hardware.   More details can be found here.

The Solution in a Nutshell

We'll start by creating a standard Windows Service app; Visual Studio makes this very easy.  When the app runs, it will call ServiceBase.Run(new Service()), just like any other service.  Then we're going to add a Form to the project.  Check System.Environment.UserInteractive (or, use a command line argument if you want more options) at startup, and if the app is running in interactive mode, call Application.Run(new Form1()) instead of ServiceBase.Run(), then call OnStart().  To satisfy requirement #2 above, first use a ServiceController to stop the service if it's running.  When the GUI exits, use a ServiceController to restart the service if it was running before the GUI started.  You'll be able to switch from service mode to GUI and back again, and the service consumers will probably never know.

The Solution, One Step at a Time

Step 1: Create the Service Project

There are lots of articles and tutorials on how to create a service.  If you're new to this, here's a link to get you started.

Our goal for today is to create a simple Windows Service that can be augmented with additional code in the following steps.  Here is the VS2015 Solution Explorer and Program.cs for my service. 

Screenshot 1

Step 2: Make the Service Do Something

For this article, my service will simply write a line to a file periodically.  In a real service you would probably do something more interesting, such as listening for requests on a socket or message queue.  I've placed the OnStart and OnStop methods in Program.cs for convenience.  They will be called both from Program.Main and from ServiceGui1.OnStart/OnStop.

C#
static public class Program
{
	static Timer theTimer;
	static int lineNumber = 0;

	/// (summary)
	/// The main entry point for the application.
	/// (/summary)
	static void Main()
	{
		ServiceBase.Run(new ServiceGui1());
	}

	/// (summary)
	/// This method receives control when either the service or the GUI starts.
	/// For this demo, it will simply start a timer, which will periodically
	/// write a line to a file.
	/// (/summary)
	/// (param name="args")(/param)
	static internal void OnStart(string[] args)
	{
		theTimer = new Timer(tickHandler, null, 1000, 2000);
	}

	/// (summary)
	/// Shut down the timer.
	/// (/summary)
	static internal void OnStop()
	{
		theTimer.Change(0, Timeout.Infinite);
		theTimer.Dispose();
	}

	/// (summary)
	/// Write a line to the file.  For simplicity, assume no race conditions.
	/// (/summary)
	/// (param name="state")(/param)
	static private void tickHandler(object state)
	{
	    string line = string.Format("Line {0}{1}", ++lineNumber, Environment.NewLine);
		File.AppendAllText(@"C:\ServiceGuiLog.txt", line);
	}
}

Step 3: Add a Form to the Project

The form can be as simple or as complex as you like.  My GUIs typically have a menu structure that allows me to set operational or test modes, run unit or integration tests, view requests and responses as they occur, send mock responses to service consumers, send mock requests upstream, and so on.  For this article, a simple public ListBox to view the file will suffice.

Screenshot_3

Step 4: Add Service Controller Methods

Usually you don't want the service and GUI running at the same time, for example when they listen on a port for socket connections.  If the service is running, a ServiceController will stop the service while the GUI is running, then re-start the service when the GUI exits.  (Of course, if you start the service after the GUI is running, there will be problems, so don't do that, but it you must, then add an appropriate interlock mechanism.)

C#
/// (summary)
/// If the service is running, stop it.
/// (/summary)
private static void DisableServiceIfRunning()
{
    try
    {
        // ServiceController will throw System.InvalidOperationException if service not found.
        ServiceController sc = new ServiceController("ServiceGui1");
        sc.Stop();
        serviceWasRunning = true;
        Thread.Sleep(1000);  // wait for service to stop
    }
    catch (Exception)
    {
    }
}

/// (summary)
/// If the service was running, start it up again.
/// (/summary)
private static void EnableServiceIfWasRunning()
{
    if (serviceWasRunning)
    {
        try
        {
            // ServiceController will throw System.InvalidOperationException if service not found.
            ServiceController sc = new ServiceController("ServiceGui1");
            sc.Start();
        }
        catch (Exception)
        {
        }
    }
}

Step 5: Integrate the Service and GUI Modes

Make the following changes in Program.cs to select the correct mode.  If there's a trick involved, this is it.

using System.Windows.Forms;

    static Form1 theForm;

    /// (summary)
    /// The main entry point for the application.
    /// STAThread for the benefit of the GUI; service will ignore it.
    /// (/summary)
    [STAThread]
    static void Main(string[] args)
    {
        if (Environment.UserInteractive)
        {
            DisableServiceIfRunning();
            OnStart(args);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            theForm = new Form1();
            Application.Run(theForm);
            OnStop();
            EnableServiceIfWasRunning();
        }
        else
        {
            ServiceBase.Run(new ServiceGui1());
        }
    }

Modify tickHandler as follows:

/// (summary)
/// Write a line to the log file.  For simplicity, assume no race conditions.
/// Modified to also display the line in the GUI if it is running.
/// (/summary)
/// (param name="state")(/param)
static private void tickHandler(object state)
{
    string line = string.Format("Line {0}{1}", ++lineNumber, Environment.NewLine);
    if (Environment.UserInteractive)
    {
        MethodInvoker action = () =>
        {
            theForm.listBox1.Items.Add(line);
            theForm.listBox1.TopIndex = theForm.listBox1.Items.Count - 1;
        };
        theForm.BeginInvoke(action);
    }
    File.AppendAllText(@"C:\ServiceGuiLog.txt", line);
}

You should be able to build and run the app and see the GUI pop up.

Step 6: Install and Start the Service

I've included batch files to install and uninstall the service.  They use the v4.0.30319 InstallUtil; modify the batch files if you need to use a different version.  Run services.msc to verify the service was installed.  Start it.  Verify that lines are being written to the file.

Step 7: Start the GUI

If everything is working correctly, you should see lines appearing in the listbox.  Verify that lines are still being written to the file.  In other words, the app is still acting like a service.

Step 8: Exit the GUI

The service will be restarted.  Verify that lines are still being written to the file.

It's Your Turn

And that's it. I hope you found this article interesting, and possibly even useful. You might be happy with ServiceGui just the way it is, or you might already be thinking of some enhancements you'd like to see. Let me know what clever modifications you make.

History

02 January 2017: Initial Version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Qodex Software
United States United States
I am a software architect, engineer, consultant, and educator based in Dallas. Projects and engagements have ranged from weather radar to Unix kernel to robotic controllers to enterprise financial systems, for a number of Well Known (and some not) companies.

I've been doing this since Z80s and CP/M were bleeding edge. After I outgrew FORTRAN, I had flings with a dozen languages; my current infatuation is C# and .NET.

I have a diverse and lengthy academic background, much of it in the area of Computer Science. Now I give back by teaching C++ and C# to impressionable youth at a Texas college.

Comments and Discussions

 
QuestionWorks like a charm :-) Pin
Odica Jaedenar15-Jan-21 6:54
Odica Jaedenar15-Jan-21 6:54 
AnswerRe: Works like a charm :-) Pin
Tim Bomgardner18-Mar-21 5:09
professionalTim Bomgardner18-Mar-21 5:09 
QuestionGood idea Pin
Member 1418940720-Mar-19 3:04
Member 1418940720-Mar-19 3:04 
QuestionClever solution, but isn't there a small problem? Pin
Jeff Bowman9-Jan-17 11:18
professionalJeff Bowman9-Jan-17 11:18 
AnswerRe: Clever solution, but isn't there a small problem? Pin
Tim Bomgardner10-Jan-17 9:36
professionalTim Bomgardner10-Jan-17 9:36 
GeneralRe: Clever solution, but isn't there a small problem? Pin
Jeff Bowman10-Jan-17 11:13
professionalJeff Bowman10-Jan-17 11:13 
GeneralRe: Clever solution, but isn't there a small problem? Pin
Tim Bomgardner13-Jan-17 11:52
professionalTim Bomgardner13-Jan-17 11:52 
GeneralRe: Clever solution, but isn't there a small problem? Pin
Jeff Bowman14-Jan-17 9:05
professionalJeff Bowman14-Jan-17 9:05 
QuestionPut all 'business logic' in a DLL - Then load DLL from an Service or USer-Mode Application Pin
Blake Miller9-Jan-17 8:20
Blake Miller9-Jan-17 8:20 
AnswerRe: Put all 'business logic' in a DLL - Then load DLL from an Service or USer-Mode Application Pin
Tim Bomgardner10-Jan-17 9:48
professionalTim Bomgardner10-Jan-17 9:48 
QuestionTopshelf Pin
bobfox3-Jan-17 14:14
professionalbobfox3-Jan-17 14:14 
QuestionWhy run without GUI? Pin
tgueth3-Jan-17 9:18
professionaltgueth3-Jan-17 9:18 
AnswerRe: Why run without GUI? Pin
KevinAG3-Jan-17 10:02
KevinAG3-Jan-17 10:02 
GeneralRe: Why run without GUI? Pin
tgueth3-Jan-17 10:11
professionaltgueth3-Jan-17 10:11 
AnswerRe: Why run without GUI? Pin
Tim Bomgardner4-Jan-17 7:35
professionalTim Bomgardner4-Jan-17 7:35 
SuggestionSuggestion Pin
markanthonygohara3-Jan-17 3:21
professionalmarkanthonygohara3-Jan-17 3:21 
QuestionPossible Extensions Pin
#realJSOP2-Jan-17 23:53
mve#realJSOP2-Jan-17 23:53 
AnswerRe: Possible Extensions Pin
Tim Bomgardner3-Jan-17 4:25
professionalTim Bomgardner3-Jan-17 4:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.