Introduction
My goal is to create a Microsoft Windows utility to watch and administer, in a simple manner, Oracle Windows services on local and remote machines. The baseline operation of the utility mirrors the Service Manager that ships with Microsoft SQL Server 2000. I used Visual Studio .NET 2003 (.NET Framework 1.1.4322).
There are two personal reasons for building this tool: the first is to gain experience with design patterns; and the second is to help manage system resources. Oracle consumes significant machine resources. So, I shut down the Oracle services to help free up system resources for other development or machine tasks.
This tool will provide a quick and easy way to see the state of any Oracle Windows service and to start, pause, and stop that service.
Application Analysis
Oracle Service Manager (OSM) offers the user three mechanisms to control the services; two are user interfaces while the third is a “passive” background device:
- A traditional dialog interface;
- A system tray menu for quick access; and
- A timer to poll the service for state changes introduced elsewhere (e.g., using the Services Control Panel applet, or using one of the Oracle tools).
Analyzing the needs of each device above, reveals unique behavioral attributes. None operate in a vacuum as changes in one affect or update the other. For example, changing the service in the dialog changes the service represented in the tray menu, as well as the service polled by the timer. Similarly, starting the service, from the tray menu, results in interface updates in the dialog.
At this point, it is worth noting the default way Visual Studio wants you to program this tool which is to create a Windows Application (the dialog form), and place NotifyIcon
and Timer
components on the form. I consider this a trap of “visual programming”. In other words, this approach tightly couples the dialog, system tray and timer code. This “visual” approach usually results in code that is hard to read, hard to maintain, and difficult to enhance or debug. By keeping these components separate and distinct using well known design patterns, these problems can be addressed resulting in a very extensible solution.
Points of Interest
OSM presents us with two primary issues to solve. The first is about watching or observing, the state of a Windows service. The second is about the actions that can be taken on the Windows service.
These patterns are encapsulated in the accompanying ServiceStatePublisher library, described in detail elsewhere on this site. OSM will use this library extensively.
This was the first Windows GUI application I programmed using C# and .NET. I was dumbfounded by the size of the InitializeComponent
function for such a small dialog. I decided to try my luck at crafting this function by hand in the hopes of eventually applying a design pattern to reduce its size. The cost of hand-building the dialog would be debilitating the Windows Forms Designer.
Being curious as to how much hand construction costs, I decided to build the DialogObserver
and SystemTrayObserver
UI components by hand. These components are recognizable by the region tags which indicate “Manually generated Form code”. As a result, these forms cannot be read by the Windows Forms Designer. In contrast, I let Windows Forms Designer have its way when creating the About and Options dialogs as these are trivial forms.
I am once again excited about the possibilities of the Visual Studio .NET 2005 release, code named “Whidbey”. This problem associated with Designer generated code may be partially, no pun intended, solved with partial classes. The other mechanism that should help significantly is “Longhorn” and the use of XAML.
Below, I explain the main application parts, design decisions, and implementation.
The Main Object -- OracleServiceManager
I have chosen not to use the dialog as the startup object. Instead, I have used my own startup object OracleServiceManager
, derived from ApplicationContext
.
- After looking at the inheritance chain for
System.Windows.Forms.Form
, it seemed natural to use ApplicationContext
as the base class because it provides the ExitThread
function needed to properly exit the application.
public class OracleServiceManager : ApplicationContext
{
private static SingleInstance _singleInstance;
private static OracleServiceManager _instance;
private ServiceSubject _subject;
private SystemTrayObserver _trayObserver;
private DialogObserver _dialogObserver;
private TimerObserver _timerObserver;
private OsmResources _resources;
- A single instance of the application is enforced through the Singleton pattern. I have created my own class, but there are other great examples of the Singleton pattern on this site. The Singleton pattern is supported using the
SingleInstance
class. This class uses a mutex and class GUID to help enforce a single application instance across all OS processes.
static OracleServiceManager()
{
Type type = typeof(OracleServiceManager);
_singleInstance = new SingleInstance( type.GUID);
_instance = new OracleServiceManager();
}
internal static OracleServiceManager Instance
{
get { return _instance; }
}
- The application controller,
OracleServiceManager
, manages the lifetimes of several objects including:
OsmResources
for localization – described further below in more detail;
ServiceSubject
for the Observer Pattern Subject – described in the ServiceStatePublisher article; and
DialogObserver
, SystemTrayObserver
, and TimerObserver
for, you guessed it, the concrete observers.
private void InitializeApplication()
{
LoadConfig();
_resources = new OsmResources();
_title = GetAssemblyTitle();
_subject = new ServiceSubject();
_dialogObserver = new DialogObserver( _subject);
_trayObserver = new SystemTrayObserver( _subject);
_timerObserver = new TimerObserver( _subject, _pollingInterval,
_dialogObserver.SynchronizingObject);
Subject.ServerName = Environment.MachineName;
}
internal OsmResources Resources
{
get { return (_resources != null ? _resources : new OsmResources()); }
}
internal ServiceSubject Subject
{
get { return _subject; }
}
- A
static Main
method provides an entry point. But remember, the static constructor detailed above, runs before Main
is called – which can make things a bit tricky.
[STAThread]
static void Main( string[] args)
{
if (!_singleInstance.IsRunning)
{
if ( args.Length > 0)
Instance.SetThreadUICulture( args[0]);
Instance.InitializeApplication();
Application.Run( OracleServiceManager.Instance);
_singleInstance.Dispose();
}
else
MessageBox.Show( Instance.Resources.GetAppString( "AlreadyRunning"),
Instance.Resources.ApplicationName,
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
The Observers / Subscribers
There are three (3) observers: DialogObserver
, SystemTrayObserver
, and TimerObserver
The DialogObserver
and SystemTrayObserver
are so similar, I will only discuss one below. I will also discuss the TimerObserver
. While it too is similar, TimerObserver
is interesting because it has no interface and has some interesting challenges and decisions.
DialogObserver
- The
System.Windows.Forms.Form
class provides the base functionality for the dialog.
using System.Windows.Forms;
public class DialogObserver : Form
{
- The observer works by subscribing to the subject; asking the subject to notify it of any changes in state. The observer only responds to changes once notified by the subject – even if the observer was the cause of the change. To support the complex interactions in the dialog, two delegates are declared –
UpdateFunction
and ServiceAction
. UpdateFunction
is used to prevent the cascading events generated by message-driven programming. ServiceAction
, on the other hand, is used to broadly control the dialog when performing an action on a service, such as starting, pausing, or stopping.
protected delegate void UpdateFunction( ServiceSubject subject);
protected delegate void ServiceAction();
private bool _updating;
private UpdateFunction serverChanged;
private UpdateFunction serviceChanged;
private UpdateFunction serviceStateChanged;
private ServiceAction startService;
private ServiceAction pauseService;
private ServiceAction stopService;
public DialogObserver( ServiceSubject subject)
{
if (subject != null)
{
subject.ServerChanged += new EventHandler( this.OnServerChanged);
subject.ServiceChanged += new EventHandler( this.OnServiceChanged);
subject.StateChanged += new EventHandler( this.OnStateChanged);
}
serverChanged = new UpdateFunction( this.ServerChanged);
serviceChanged = new UpdateFunction( this.ServiceChanged);
serviceStateChanged = new UpdateFunction( this.ServiceStateChanged);
startService = new ServiceAction( App.Subject.Start);
pauseService = new ServiceAction( App.Subject.Pause);
stopService = new ServiceAction( App.Subject.Stop);
InitializeComponent();
}
- In preparation for the timer observer, there will be a need to marshal timer events on a worker thread back to the GUI thread. So, I have provided a property to use from the dialog observer. This property is used by the controller,
OracleServiceManager
. The property is passed to the TimerObserver
constructor as one of the optional parameters.
(See OracleServerManager::InitializeApplication()
above.)
internal ISynchronizeInvoke SynchronizingObject
{
get { return serviceCombo; }
}
- The implementation of the subscription events has one twist. All the events change the dialog controls which produces the cascading events problem found in message-driven GUI programming. I used the delegate mechanism to supply one function that stops the cascading effect. This results in almost identical subscription events.
I suppose I could have created my own EventArgs
class and added a Cause
attribute. This would have brought the subscription events from three functions down to one. The consequence of this approach is a conditional statement – an if
or switch
statement – to discern the cause and act accordingly. Personally, I try to limit my use of conditionals – they tend to be sources of errors for me. Of course, I don’t expect to eliminate their use – that’s not possible – but I try to keep any conditions as simple as possible. [Off soapbox].
protected void OnServerChanged( object sender, System.EventArgs e)
{
ServiceSubject subject = (ServiceSubject) sender;
Update( serverChanged, subject);
}
protected void OnServiceChanged( object sender, System.EventArgs e)
{
ServiceSubject subject = (ServiceSubject) sender;
Update( serviceChanged, subject);
}
protected void OnStateChanged( object sender, System.EventArgs e)
{
ServiceSubject subject = (ServiceSubject) sender;
Update( serviceStateChanged, subject);
}
private void Update( UpdateFunction function, ServiceSubject subject)
{
if (!Updating)
{
Updating = true;
function( subject);
Updating = false;
}
}
private void ServerChanged( ServiceSubject subject)
{
ClearServiceItems();
if (subject.ServerName != null)
{
AddServer( subject.ServerName);
SetServerItem( subject.ServerName);
SetStatusBarText( GetStringFromResources( "Connected"),
subject.ServerName);
AddOracleServicesToList( subject.ServerName);
}
else
{
if (serverCombo.Text.Length > 0)
SetStatusBarText( GetStringFromResources( "NotConnected"),
serverCombo.Text);
else
SetStatusBarText( GetStringFromResources( "Ready"));
}
}
private void ServiceChanged( ServiceSubject subject)
{
if (subject.Controller != null)
{
SetServiceItem( subject.Controller.DisplayName);
ServiceStateChanged( subject);
}
else
{
DisableActionButtons();
SetDefaultServiceStateImage();
}
}
private void ServiceStateChanged( ServiceSubject subject)
{
if (subject.State != ServiceStateUnknown.Instance)
{
SetActionButtonState( subject);
SetServiceStateImage( subject.State);
SetStatusBarText( subject.State.Status, subject.ServerName,
subject.Controller.ServiceName);
}
else
{
DisableActionButtons();
SetDefaultServiceStateImage();
}
}
- The following code contains the dialog events to which we respond. There are a couple of points here. First, I intercept the
Closing
event and do not allow the dialog to close, but rather hide it. Most of the work in the dialog is in ServerComboValidating
and ActionClick
. The former checks if the chosen server name is valid AND there is a machine on the network with that name – this is done in a supporting function IsServerValid
. ActionClick
, on the other hand, is another use of a delegate to streamline programming. That is, ActionClick
encapsulates the cursor changes, and dialog enabling/disabling when performing an action like start, stop or pause.
private void ClosingDialog( object sender, CancelEventArgs e)
{
if (sender == this)
{
e.Cancel = true;
this.Hide();
}
}
private void ServerComboValidating( object sender, CancelEventArgs e)
{
string serverName = serverCombo.Text.ToUpper();
ServiceSubject subject = App.Subject;
if (subject.ServerName != serverName)
{
SetStatusBarText( GetStringFromResources( "Connecting"),
serverName);
if (IsServerValid( serverName))
{
subject.ServerName = serverName;
}
else
{
string msg = GetStringFromResources( "NoNetwork");
MessageBox.Show( this, msg, App.Title, MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
subject.ServerName = null;
e.Cancel = true;
}
}
}
private void ServiceComboChangedIndex( object sender, EventArgs e)
{
ServiceSubject subject = App.Subject;
subject.Controller = new ServiceController( serviceCombo.Text,
subject.ServerName);
}
private void RefreshClick( object sender, EventArgs e)
{
App.Subject.RefreshServices();
}
private void StartClick( object sender, EventArgs e)
{
ActionClick( startService, START);
}
private void PauseClick( object sender, EventArgs e)
{
ActionClick( pauseService, PAUSE);
}
private void StopClick( object sender, EventArgs e)
{
ActionClick( stopService, STOP);
}
private void ActionClick( ServiceAction action, string actionName)
{
Cursor.Current = Cursors.WaitCursor;
if (VerifyUserAction( actionName))
{
this.Enabled = false;
action();
this.Enabled = true;
}
Cursor.Current = Cursors.Default;
}
- There are a number of supporting functions which are in the region labeled Dialog Support Functions. If you’re interested, take a look. Most of the support functions do small, but important, tasks – like controlling the statusbar caption, and the enabled status of controls, etc. I suspect most people reading this article will have a strong grasp on these concepts, so I have not included them. If the feedback indicates I should discuss them, I’ll update the article.
TimerObserver
- This observer does not derive from any base class, instead it uses one of the three timers provided by the .NET Framework – specifically, the
System.Timers.Timer
class. This Timer
class uses .NET worker threads. In addition, the resolution offered by this class is more than sufficient for my needs. However, the class is built in such a way that the other two timers could be used instead with little rework.
This observer has some other interesting characteristics. The one I’d like to call attention to is the delegate ChangeStateCallback
that assists in marshalling the event back to the GUI thread. Also, note the Dispose
method which cleans up and discards the timer object.
Please note: I have removed any references to the System.Diagnostics.Debug
object for clarity. The code in the project still has these references – and you may find it useful if you wish to see how the messages are processed.
public class TimerObserver
{
private delegate void ChangeStateCallback( ServiceState state);
private System.Timers.Timer _timer;
private DateTime _stopTime;
private ServiceState _state;
private ISynchronizeInvoke _syncObject;
private ChangeStateCallback changeStateCallback;
public TimerObserver( ServiceSubject subject, double interval) :
this( subject, interval, null)
{
}
public TimerObserver( ServiceSubject subject, double interval,
ISynchronizeInvoke synchronizingObject)
{
if (subject != null)
{
subject.ServerChanged += new EventHandler( this.OnServerChanged);
subject.ServiceChanged += new EventHandler( this.OnServiceChanged);
subject.StateChanged += new EventHandler( this.OnStateChanged);
}
_timer = new System.Timers.Timer( interval);
_timer.Elapsed += new ElapsedEventHandler( this.OnTimerElapsed);
SynchronizingObject = synchronizingObject;
changeStateCallback = new ChangeStateCallback( this.ChangeState);
}
public void Dispose()
{
StopTimer();
_timer.Dispose();
_timer = null;
}
- The
Elapsed
event has three interesting points. First, it is called on the worker thread. Second, as a result, it needs to decide – based on the existence of a synchronizing object – if it must marshal back to the GUI thread. And third, as noted in the documentation and elsewhere on this site, this timer has a side-effect of possibly being called after it was told to stop. This could raise an event that we do not want. Solving this issue is pretty straightforward – track the stop time and compare it to the event signal time.
private void OnTimerElapsed( object sender, ElapsedEventArgs e)
{
if (e.SignalTime < StopTime)
{
ServiceSubject subject = OracleServiceManager.Instance.Subject;
ServiceController service = new ServiceController(
subject.Controller.ServiceName, subject.ServerName);
ServiceContext context = new ServiceContext( service);
if (State.GetType() != context.State.GetType())
{
object[] stateArray = new object[] { context.State };
if (SynchronizingObject != null)
SynchronizingObject.BeginInvoke( changeStateCallback,
stateArray);
else
ChangeState( context.State);
}
}
}
- Similar to the
DialogObserver
, the TimerObserver
also subscribes to all three (3) events. There are no surprises here. The code is short and almost self explanatory. The timer is stopped when the server changes, and starts when a new service is selected. The state is also recorded when any event occurs. The recorded state is used as a comparison to the current state in the Elapsed
event above.
private void OnServerChanged( object sender, EventArgs e)
{
ServiceSubject subject = (ServiceSubject) sender;
StopTimer();
State = subject.State;
}
private void OnServiceChanged( object sender, EventArgs e)
{
ServiceSubject subject = (ServiceSubject) sender;
State = subject.State;
StopTimer();
if (subject.Controller != null)
StartTimer();
}
private void OnStateChanged( object sender, EventArgs e)
{
ServiceSubject subject = (ServiceSubject) sender;
State = subject.State;
}
Resources
OsmResources
provides methods needed to obtain localized icon and string resources. The identifiers for the resources have a particular naming convention to assist in retrieving the appropriate resources. I have not localized the application as yet, but future updates will include some localizations. I hope to translate to Spanish. Any volunteers for French, German, Italian, or Russian? If you take the time to localize the string resources (there're about 40 of them), then I will compile and include them in the distribution.
The OsmResources
class is straightforward. It makes use of a common naming convention to retrieve the requested icon or string resource. The class also uses the System.Resources.ResourceManager
to get the wonderful default behavior the .NET Framework provides. This default behavior allows for graceful degradation when the requested resource is not available in the satellite DLL. For more information, take a look at MSDN for how .NET handles globalization and localization.
Configuration & Options
My thanks to Nick Parker for extending the default read-only behavior of .NET configuration support. I used Nick’s Configuration
class to enable read-write support for the OSM configuration file. I wanted to make use of the default .NET support for configuration classes, and was saddened when I learned they were read-only. The Configuration
class stores the values managed in the options dialog and used by the application for polling frequency and action verification. Not a very interesting job, but someone’s gotta do it – and this class does it nicely. Thanks Nick.
Future Directions
Like all projects, there is never really an end – only an end to the beginning. And so it is with this project. I kept the project simple even when I noticed obvious areas for improvement or great enhancement suggestions. I have kept a list below of enhancements and improvement areas. I’m not sure when I’ll get around to incorporating these ideas…perhaps one rainy Sunday afternoon.
- Adapt OSM to work for multiple kinds of Windows databases including SQL Server, DB2, Sybase, MySQL, etc.
- Create a toolbar observer and package it in a Visual Studio add-in.
- Add a test bench – which I wanted to do but didn’t -- there’s always next project for test-first/test-driven design. BTW, I’m still looking for an example of how test first design, or test driven development, is applied to GUIs.
- Create localized resources for Spanish, French, German, and others.
Special Thanks
This project, while simple, has had its share of contributors – people who for a little bribery, like a drink, or maybe a lunch, provided invaluable help. Jason Pugh and Rick Barraza have provided me with guidance and support. Both have helped me test the solution, keep the project simple, and made great suggestions for the future noted above. Rick has lent his creative hand to manufacturing the icons – not normally a pixel pusher, but rather vector oriented, I was grateful to have his helping hand [and eye] – check out Rick’s site.
History
- 14 May 2004 - Initial release.