With the Windows Service mostly finished, we can focus on the actual plug-ins. We need to create a class (plug-in) that can be loaded at run time by referencing its file name and class name in the Windows Service's config file. Enter the "Interface".
An interface is a forced contract. We must define the contract, and then another class implements that interface, meaning the class must fully create all of the members of the interface.
For example, if we define a simple method in our interface:
string[] Execute();
any class that wishes to implement this interface must also define and implement it as so:
public string[] Execute() {
return new string[]{"Ive been Implemented"};
}
Of course the actual code provided within the implementation can be anything that is relevant to the task at hand.
Why are we using an interface for plug-ins? We use an interface because the contract is being met when we code against a set of classes that are based on the same interface; therefore, you can then create or cast objects as the type of the interface. If you do not fully understand interfaces, I recommend you read my article on OOP techniques at http://www.15seconds.com/Issue/020508.htm. We will build on top of the content found there.
We want to keep the interface separate from the actual Windows Service. Let's add a New C# Class Library to our existing solution. I named mine "ServicesCore".
Rename the default Class1.cs to "IServices.cs". Our interface is pretty small and easy to create.
The "Execute()" method takes in no parameters. It only returns a "string[]". I've decided a typical result of all plug-ins will be a string[], so we can transmit messages (most likely errors) out of the plug-in and to the caller. This will be the main method called on each plug-in to perform its job.
I forced the "ToString()" method, so we can easily get the name of the plug-in at any time. This could be useful for logging situations or conditional actions based on the specific plug-in.
Step Two: Creating a Schedule Manager To Manage Our Plug-Ins
n our new project, we need to create two classes. The first will hold an individually scheduled item. It will contain items such as:
the instance of the plug-in itself
its schedule
its file name, and class name
It will also use its own internal schedule to execute the plug-in. Name this "Schedule.cs"
The second class will hold a collection of all the scheduled items, iterate its entire collection, and instruct each individual schedule to run. Name this "ScheduleManager.cs"
Most of the code for this class (see figure 1.4) deals with implementing two other interfaces, the IEnumerable and IEnumerator classes. These interfaces let us create and use a custom collection. If you do not understand how this works, please consult the documentation.
Figure 1.3 Schedule Source Listing
using System;
namespace scheduler_net {
public class Schedule {
protected Scheduler.Net.IServices plugin=null;
protected string assemblyName;
protected string className;
protected long scheduleTicks;
protected System.DateTime lastRan=System.DateTime.MaxValue;
public Schedule(Scheduler.Net.IServices PlugIn, string AssemblyName,
string ClassName, long ScheduleTicks) {
plugin=PlugIn;
assemblyName=AssemblyName;
className=ClassName;
scheduleTicks=ScheduleTicks;
}
public Scheduler.Net.IServices PlugIn{get{return
plugin;}set{plugin=value;}}
public string AssemblyName{get{return
assemblyName;}set{assemblyName=value;}}
protected string ClassName{get{return
className;}set{className=value;}}
protected long ScheduleTicks{get{return
scheduleTicks;}set{scheduleTicks=value;}}
protected System.DateTime LastRan{get{return
lastRan;}set{lastRan=value;}}
public bool CanRunNow {
get {
if(lastRan==System.DateTime.MaxValue) return true;
System.TimeSpan ts = new
System.TimeSpan(System.DateTime.Now.Ticks - lastRan.Ticks);
return (ts.Ticks > scheduleTicks);
}
}
public string[] Execute() {
string[] tmp;
if(CanRunNow) {
tmp = this.PlugIn.Execute();
this.lastRan=System.DateTime.Now;
} else
tmp = new string[]{this.PlugIn.ToString()+ ": Not
Scheduled to run at this time."};
return tmp;
}
}
}
Figure 1.4 ScheduleManager Listing
using System;
namespace scheduler_net {
public class ScheduleManager : System.Collections.IEnumerable,
System.Collections.IEnumerator {
public ScheduleManager() {
Reload();
}
public void Reload() {
try {
string pluginlist =
System.Configuration.ConfigurationSettings.AppSettings["pluginlist"];
string[] plugins = pluginlist.Split(";".ToCharArray());
string location =
System.Reflection.Assembly.GetEntryAssembly().Location;
location = location.Substring(0,
location.LastIndexOf(@"\")+1);
foreach(string plugin in plugins) {
try {
string[] pluginproperties =
plugin.Split(",".ToCharArray());
System.Reflection.Assembly asm =
System.Reflection.Assembly.LoadFrom(location + pluginproperties[1]);
System.Type type =
asm.GetType(pluginproperties[0]);
Scheduler.Net.Schedule newSched = new
Scheduler.Net.Schedule((Scheduler.Net.IServices)Activator.CreateInstance(type),
pluginproperties[1], pluginproperties[0], Convert.ToInt64(pluginproperties[2]));
this.Add(newSched);
}catch(Exception plugInFailure) {
string bar=plugInFailure.ToString();
}
}
}catch(Exception loadupFailure) {
string foo=loadupFailure.ToString();
}
}
public string[] Execute() {
System.Collections.ArrayList log = new
System.Collections.ArrayList();
log.Add("System: Log Started");
foreach(Scheduler.Net.Schedule sched in this) {
foreach(string logitem in sched.Execute()) {
log.Add(logitem);
}
}
log.Add("System: Log Ended");
string[] tmp = new string[log.Count];
log.CopyTo(tmp);
return tmp;
}
protected System.Collections.ArrayList scheduledItems = new
System.Collections.ArrayList();
protected int counter=-1;
public System.Collections.IEnumerator GetEnumerator() {
return scheduledItems.GetEnumerator();
}
public object Current {get{return scheduledItems[counter];}}
public object this[int index] {
get {return scheduledItems[index];}
set {scheduledItems[index]=value;}
}
public void Reset() {
counter=-1;
}
public bool MoveNext() {
counter++;
return(counter<=scheduledItems.Count);
}
public void Add(object Item) {
scheduledItems.Add(Item);
}
public void Remove(object Item) {
scheduledItems.Remove(Item);
}
public bool Contains(object Item) {
return scheduledItems.Contains(Item);
}
public int IndexOf(object Item) {
return scheduledItems.IndexOf(Item);
}
}
}
Let's examine the "Reload()" method in figure 1.4. It reads in from our config file the value for "pluginlist". Here is an example of that node in the config file:
Next, the method splits apart this string into sections for each plug-in based on the ";" delimiter, which splits up each plug-in. Then for each plug-in, it splits out the class name, the assembly file, and its schedule (or ticks). I chose ticks for the duration of wait time before the next plug-in run time. This approach makes coding easy but doesn't lend itself well to power and flexibility. For example, you cannot specify start or end values, or even an exclusion list (don't run this plug-in between the hours of 3 am to 5 am), etc.
You now see the reason why I chose to use an interface for the plug-in.
We use reflection to load up the plug-in assembly and get a reference to the specified type. Next we use the System.Activator to create an instance of that type. We now have a reference to that plug-in, and since they all use our interface we can cast this instance to that interface when we create our new schedule item. Casting it as the interface allows us to use the methods specified in the interface's contract. That means calling the "Execute()" and "ToString()" methods.
The end result will be a single instance of a schedule item, which can be added to our schedule manager's collection. Now that we have a collection of scheduled items, we need a way to automatically iterate over the collection and call each item's "Execute()" method. The "Execute()" method in the ScheduleManager does just this.
Step Three: Finalizing the Service
With the plug-in interface, scheduler, and loader developed, we only need to add the instance of our schedule manager to our service.
First create the variable at class scope:
Scheduler.Net.ScheduleManager sm;
And then in the ServiceTimer_Tick event handler, we create an instance of our ScheduleManager class, which will automatically call the "Reload()" function in its constructor to load up the entire collection of plug-ins.
if(sm==null) sm = new Scheduler.Net.ScheduleManager();
Lastly, let's call the Execute() method to execute all the plug-ins based on their individual schedules.
sm.Execute();
Step Four: Installing the Service
Included in the .NET Framework SDK there is a utility named "InstallUtil.exe". This utility is used to execute installers on a given assembly.
In part 1 step 2, we discussed how to use the ServiceInstaller and ServiceProcessInstaller to install our service on the desired machine. Executing "InstallUtil.exe" on our Windows Service assembly will (hopefully) install the Windows Service.
To install our service from the command line type:
installutil scheduler_net.exe
and to uninstall it, add the "/u" parameter:
installutil scheduler_net.exe /u
Finally, launch the services plug-in in the Microsoft Management Console and start the service.
Take time now to review the project that I have included for download. The complete source is intact, and you should only need to install the service using the included binary.
Now it's time for plug-in creation.
Creating a Simple Plug-In : MoreOver.com
MoreOver.com gives us free access to news feeds for personal use, so I felt it was a good place to start. We can simply hold a list of the categories (http://w.moreover.com/categories/category_list.html) on our config file, which our plug-in will use to populate the database with news items from each list.
Step One: Setup the Database
I have created a simple table based on the sample XML file to hold in-coming data. Figure 2.1 shows the structure of the table.
Figure 2.1 The MoreOver Table Diagram
Next we need one stored procedure to insert the data into this table.
create procedure addArticle
@url nvarchar(150),
@headline_text nvarchar(250),
@source nvarchar(100),
@media_type nvarchar(50),
@cluster nvarchar(50),
@tagline nvarchar(50),
@document_url nvarchar(150),
@harvest_time nvarchar(50),
@access_registration nvarchar(50),
@access_status nvarchar(50)
as
if exists(select url from [DataFeeds].[dbo].[MoreOver] where url=@url)
begin
UPDATE [DataFeeds].[dbo].[MoreOver]
SET [url]=@url, [headline_text]=@headline_text, [source]=@source,
[media_type]=@media_type, [cluster]=@cluster, [tagline]=@tagline,
[document_url]=@document_url, [harvest_time]=@harvest_time,
[access_registration]=@access_registration, [access_status]=@access_status
where url=@url
end
else
begin
INSERT INTO [DataFeeds].[dbo].[MoreOver]([url], [headline_text], [source], [media_type], [cluster], [tagline], [document_url], [harvest_time],
[access_registration], [access_status])
VALUES( @url,
@headline_text,
@source,
@media_type,
@cluster,
@tagline,
@document_url,
@harvest_time,
@access_registration,
@access_status)
end
GO
Step Two: Implementation
With the database setup and read, we implement the actual plug-in. Let's add a whole new C# class library to this solution, named "MoreOverPlugin", and then set it up for our plug-in. Add a reference for this project to our "ServicesCore" project, and rename the default to "MoreOverPlugin.cs".
In the .NET Framework SDK there is a tool named "xsd.exe". This tool is used to generate schema and class files from a given source. It takes an XML file and generates a schema (XSD) file. From XSD files we generate class files (CS).
Let's use this tool to automatically create a class. Download one of the XML feeds from MoreOver.com and save it in the project directory for our plug-in (I gave it the name "moreover.xml"). Dump to a DOS prompt, navigate to that folder, and then execute this command:
We're now done with the DOS prompt. If you return to the VS .NET solution and view all the files in the project, you'll see the new "moreover.cs" file. Right click it and include it in this project. Using the xsd.exe tool to generate your classes means that you can also serialize between XML and classes easily. I'll show more on this later.
In the article's download package, I've supplied a plug-in template. It has all the necessary functions required by the interface, including some error handling and logging members. It's easiest to simply copy the template code into your plug-ins, and then modify that. Do this for the MoreOverPlugins project.
Now let's dive into the "Execute()" function line by line. If you feel more comfortable seeing the entire method, open up the "MoreOverPlugin.cs" file from the download package.
I previously discussed creating specific sections in our config file for our plug-ins. This line creates a NameValueCollection for our custom settings node in that file.
In the MoreOver plug-in, either specify the categories directly in the config file or specify the stored procedure as the "categoriessource", which will return the list. View my version of the plug-in to understand how to use either the settings in the config file or query the database to get the list of categories. The result of either will give us a System.Collections.Hashtable of categories (named "categories") to use in our "Execute()" method.
Step Three: Program Flow
The method flow is pretty straight forward:
1. for every category we need to download
foreach(string cat in categories.Keys) {
a. download the xml file based on the category name
request = new Scheduler.Net.HttpRequest("http://p.moreover.com/cgi-
local/page?o=xml&c="+cat.Replace(" ", "%20"),
Scheduler.Net.HttpRequest.HttpMethodValues.GET);
System.IO.TextReader reader = new
System.IO.StreamReader(request.GetHttpStream());
b. serialize this xml to our class we generated previously
System.Xml.Serialization.XmlSerializer serializer = new
System.Xml.Serialization.XmlSerializer(typeof(Scheduler.Net.moreovernews));
Scheduler.Net.moreovernews moNews =
(Scheduler.Net.moreovernews)serializer.Deserialize(reader);
c. for every article item we just downloaded
foreach(Scheduler.Net.moreovernewsArticle article in moNews.Items) {
i. save it
SaveArticle(article);
I want to review three things from the flow process in more detail. First is the "Scheduler.Net.HttpRequest" class. I wrote this class to simplify HTTP GET and POST requests. It's a wrapper for the "System.Net.HttpWebRequest" class. Feel free to view that source and use it in your own solutions.
The second item is serialization (1b). Remember that we used the utility "xsd.exe" to generate class files for the moreover XML feed. The first line creates an XmlSerializer based on the generated class's type. The XmlSerializer deserializes our XML stream that we retrieved from the HttpRequest into an instance of the moreovernews type.
Third is saving the article using the "SaveArticle()" method. In this method we call the stored procedure, load it up with all the parameters that I pull from my deserialized class, and execute the query.
Step Four: Installing the Plug-In
To install the plug-in, stop the Windows Service, add the needed items to the config file, and then start the service. Along with the above portions that I included for the MoreOverSettings in the config file, we need to update the "pluginlist" entry in the main "appSettings" node. The new version is:
Notice it is scheduled to only download new content once per day (36000000000). Change this to your preference. Now start the Windows Service, and it will immediately download the content and update your database as per the schedule.
You have now developed your first plug-in. You do not need to continue with the next section "Creating an E-Mail Queue" if you want to start developing your own plug-ins; skip to the conclusion.
Creating an E-Mail Queue
We will create an automated e-mail-queue sending application in this plug-in. Sometimes one of the most valuable features in an application is the ability to communicate with external parties via e-mail. We are all aware that the Internet can be unstable. In a common application that sends an e-mail, the developer typically uses an existing object (COM or .NET Assembly) to connect to a mail server and send it appropriate commands for sending e-mail. In .NET this is simplified with the System.Web.Mail namespace (ms-help://MS.VSCC/MS.MSDNVS/cpref/html/frlrfSystemWebMail.htm), which is simply a wrapper for the COM based CDOSYS (Collaboration Data Objects for Windows 2000) message component.
Given the fact that the Internet can be unstable, problems may occur if your application attempts to directly connect to your mail server and send out the e-mail. What happens if at that given time the mail server is down or any given network point in between has failed? The e-mail will not be sent, and it will most likely be lost. This plug-in will solve this issue.
Step One: Setup the Database
As we did in the MoreOver plug-in, setup the table and the appropriate stored procedure in the database.
Figure 2.1 The EmailQueue Table
Figure 2.2 Get Email Queue Stored Procedure
CREATE Procedure GetEmailQueue
as
SELECT [Attachments], [Bcc], [Body], [BodyEncoding], [BodyFormat], [Cc], [From], [Headers], [Priority], [Subject], [To],
[UrlContentLocation], [UrlContentBase], [Server] FROM [DataFeeds].[dbo].[EmailQueue] where sent=0
update emailqueue set sent=1
GO
Notice the simplicity of the stored procedure in this example. I'm sure most of you will want to add more functionality, including only updating "sent=1" for items which had no errors during the actual send from the plug-in.
Step Two: Implementation
Open VS .NET and create a new C# Class Library; name it "EmailSenderPlugin". Since we are not using the existing solution, we will add a reference to our "ServicesCore.dll". This allows us to develop our plug-in independent of anything else. Rename the default "Class1.cs" to "EmailSenderPlugin.cs" and also copy the code from the template plug-in to this class (don't forget to name the namespace and class appropriately. Mine is named "EmailSenderPlugin. SendEmails").
Our Settings for this plug-in look similar to (only relevant sections included):
a. Create a System.Web.Mail.MailMessage for the item
System.Web.Mail.MailMessage msg = new System.Web.Mail.MailMessage();
If you refer to my copy of the plug-in, you will see that there are about 60 lines of code needed to fully create the MailMessage from the database reader. Notice that if there is no "From" address specified in the table, we will pull the default out of the config file.
c. Send the Message
System.Web.Mail.SmtpMail.Send(msg);
Install it the same way you installed the MoreOver plug-in. Here is my new pluginlist config entry:
Notice how the EmailSenderPlugin is set very short, 5 seconds. This is because we want very little, if any, delay for outgoing mails.
That's all there is for this plug-in. You now have automated offline e-mail-queue processing.
Conclusion
In this article you've experienced just how easy it is to create an extensible Windows Service in .NET using a fairly basic OOP principle (Interfaces). If you would like to work on improving the core of this application or would like to make your custom plug-ins available to everyone else, you can check out the SourceForge project at http://www.sourceforge.net/projects/scheduler-net/. Send me an e-mail with your SourceForge Unix name if you would like to be added as a developer to this project.
About the Author
Robert Chartier has developed IT solutions for more than nine years with a diverse background in both software and hardware development. He is internationally recognized as an innovative thinker and leading IT architect with frequent speaking engagements showcasing his expertise. He's been an integral part of many open forums on cutting-edge technology, including the .NET Framework and Web Services. His current position as vice president of technology for Santra Technology (http://www.santra.com) has allowed him to focus on innovation within the Web Services market space.
He uses expertise with many Microsoft technologies, including .NET, and a strong background in Oracle, BEA Systems, Inc.'s BEA WebLogic, IBM, Java 2 Platform Enterprise Edition (J2EE), and similar technologies to support his award-winning writing. He frequently publishes to many of the leading developer and industry support Web sites and publications. He has a bachelor's degree in Computer Information Systems.
In the second part of his series on building N-tier web applications using ASP.NET 2.0 and SQL Server 2005, Thiru Thangarathinam covers the business logic and user interface layers. In the process, he also examines some new features in ASP.NET 2.0 that greatly simplify the development process.
[Read This Article][Top]
Code reusuability is one of the major goals of any good object-oriented programmer. While the ASP.NET framework has made code reusuability easier and more elegant than it was in classic ASP, one area where reusuability could be improved is at the UI level. This article outlines a technique that you can use in ASP.NET 1.x that allows every page in your web application to inherit not only the functionality of a base page, but its UI as well.
[Read This Article][Top]
In this article, Gayan Peiris looks at creating an ASP.NET web application that will display the usage details of a selected SharePoint site. Building such an application enables SharePoint administrators to gather all SharePoint usage data from a central location. [Read This Article][Top]
In this article, Gayan Peiris examines using the SharePoint Object Model
to access SharePoint site information from an ASP.NET web application.
It should be of particular interest to SharePoint administrators who
can use the included code as a starting point for development of
their own web-based SharePoint administration application.
[Read This Article][Top]
In this article, David Every outlines a step-by-step account of how he solved the problems he encountered while implementing an auto-deployment process. He also describes how to create a stable process for automated remote .NET deployment featuring "side-by-side" capability. [Read This Article][Top]
Most default SharePoint Server Web Parts can be connected across organizations. The third article in this series shows how to develop connectable Web Parts that consume information provided by other Web Parts. [Read This Article][Top]
Automatic daily builds is a well known software engineering best practice. This article introduces a strategy for implementing and promoting daily builds and offers tips and tricks for preventing and fixing breaks. [Read This Article][Top]
Building an application can be more than pressing F5. With an increasing
number of quality packages being released, developers for the .NET platform now have options to create a very sophisticated build process. Aaron Junod describes a sample build environment and shows how a number of tools can work together to make reliable, predictable, and value-added builds. [Read This Article][Top]
The first part of this three part series walks through the process of creating a mobile blog application using a BASIC development environment for Palm OS devices called NS Basic.
Subsequent articles will focus on synchronizing the data to the desktop using C# and creating an installer. [Read This Article][Top]
This short article provides source code for a classic ASP online database functions testing application and shows how to configure and use the tool for either SQL Server or Oracle. [Read This Article][Top]
Mailing List
Want to receive email when the next article is published? Just Click Here to sign up.