Click here to Skip to main content
Click here to Skip to main content

Windows 7 Trigger Start Service

, 17 Jan 2010
Rate this:
Please Sign up or sign in to vote.
Implement services that start/stop automatically to some events like Device attached, Network Availability, Firewall port modified, Domain join, Group Policy change etc

Table of Contents

Introduction

When the word multitasking comes into mind, we always think of foreground processes that is opened in the taskbar. But this is not true. There are two types of tasks that runs on every computer. First is foreground Tasks, and another is background tasks. Foreground tasks are the process which have user interfaces so that user can easily communicate with the process easily using the application. On the other hand, Background processes are generally hidden processes that runs in our machine and invokes specific requirement of the system to enhance the user experience. Windows Service is the most common way of developing a background process in Windows Environment. 

There are lots of services that we all the time while we are logged into windows. To name a few:
  • Server: It is the main service that enables you to share files, printers etc over network.
  • Shell Hardware Detection: It invokes the hardware detection wizard automatically when new hardware is attached with your computer.
  • Network Connections : Invokes Network Connection Folder for network management. Etc...

There are lots of services there in our windows environment which works in background, polls throughout the session with few active long running threads.This results in slower performance, larger number of useless process running, killing up a portion of CPU, and hence slow performance of the overall user experience.

If you want to know more about windows 7 usage other than Service Triggers just visit :
Windows 7 : New Features Explained Using .NET
 

Introduction to Service Triggers


Windows 7 (Later with Windows Server 2008) puts forth a new concept to start services based on some events. The background services once configured to the computer will automatically start itself when the event occurs to the system. Thus eliminates the concept of running background services infinitely. Thus we will have lesser number of background threads running on the system and also lesser amount of memory is blocked, and hence better performance of Foreground process. Removing background threads also reduces the overall power consumption of the system as well.

To elaborate the scenario, lets say you are about to build a service that gets some updates from the server only when the computer is connected to the Internet. So what will be your steps :

  • Create a service and override the OnStart event of the Service do an Infinite loop and check Whether network is available.
  • Ping the Server if service is available or not.
  • If service is available do the respective updates, otherwise poll it again and do the same job over and over.
Now if you look into the service, you would have noticed that you are running the service throughout the day without running a single line of statement. I mean if the computer doesnt connect to the internet, every time the Ping to server fails and hence the loop is going on without anything to do. In such a scenario, Start triggers comes into play.

In this article you will learn how to configure the Service Triggers, so that it enables and disables your service according to your need.

Types of Service Events

By the type of Service Events I mean the types of events that currently the API supports that you can use in your application.
eventyp.JPG
  1. Device Join : This trigger is fired when any device is joined to the system. Say printer is attached, USB device is installed like (Pen Drive, Bluetooth Dongle etc)
  2. Domain Join : This trigger is fired when the computer is attached to a Domain. Generally we track this event to do specific tasks which you want to execute when computer domain server is changed.
  3. Firewall Event : Say you have exempted one port in the Firewall. This event will be triggered when any change in firewall settings is made.
  4. Group Policy Change : Group policies defines the actual restrictions to the system that windows will impose during the session is on progress. If certain Group policies are modified, this event gets triggered automatically.
  5. Network IP Availability : This event is triggered when the first IP address on the TCP/IP networking stack becomes available to the system. You might look into the events like FirstIPAvailable, or LastIPAvailable and run /stop your service accordingly.
  6. Custom Event : You can also define your own custom events which is generated by Event Tracing of windows.

Implementation

To implement service triggers in your application you have two choices:

  1. Configure using Command Tool sc.exe (Service Configuration Command Line) together with TriggerInfo switch.
  2. Use ChangeServiceConfig2 API to configure the service programmatically.
Before we start creating the actual service let see what are the main components of a Trigger. Triggers consists of :
  1. A Trigger Event Type
  2. A Trigger Event Sub Type
  3. The Action that you want to take in response to the trigger
  4. Trigger specific Data Items that you need to pass. The Data depends on the actual used Event type
Now lets start configuring service triggers:

Using TriggerInfo option of sc.exe

Windows 7 and Windows 2008 introduced a new option to Service Configuration tool sc.exe to configure and query services that have supported triggers associated with it. Open CMD and type sc triggerinfo you will see all the options that are available to handle trigger info.

Usage:
sc <server> triggerinfo [service Name] <option1> <option2>...
The server is optional which defaults to Local Computer.

The options that you might use are :
  1. start/device/UUID/HwId1/.. : Specifies a start of a service when specific device of UUID (ClassID) and HwId1, HwId2 ... are attached.
  2. start/custom/UUID/data0/.. : Specifies a start of a custom service provided by Event Tracer based on hexadecimal data passed.
  3. stop/custom/UUID/data0/.. : Stops the custom ETW service
  4. start/strcustom/UUID/data0/.. : Specifies a start of a custom service when data is passed as string
  5. stop/strcustom/UUID/data0/.. : Stops the custom string based custom service
  6. start/networkon :Determines the start of a service when first IP is available
  7. stop/networkoff : Stops the service when last IP is unavailable
  8. start/domainjoin : Starts the service when domain is joined
  9. stop/domainleave : Stops the service when Domain is left
  10. start/portopen/parameter : Configures the service to start when a specified port is opened through the firewall. You can mention the parameter with ;(semicolon) delemeter. viz. portnumber;protocolname;imagepath;servicename
  11. stop/portclose/parameter : Stops the service when port is unavailable.
  12. start/machinepolicy : Starts a service when machine policy is modified.
  13. start/userpolicy : Starts a service when user group policy is modified

Therefore to configure a service that will start automatically when the system is attached to a Domain server you can just execute:
sc triggerinfo myservice start/domainjoin
The service "myservice" will automatically configured to start when domain is joined to the local computer.

To query if the service is configured properly as Triggered Start you can use qtriggerinfo switch with the servicename in sc.exe.

Usage:
sc <server> qtriggerinfo [service Name] <buffer size>

Thus if you call
sc qtriggerinfo myservice

triggerinfo1.jpg

It will return you the info regarding how the service is configured to Trigger start when domain is joined.

Using ChangeServiceConfig2 API

Services can also be configured to trigger by invoking ChangeServiceConfig2 API method with SERVICE_CONFIG_TRIGGER_INFO. The SERVICE_TRIGGER_INFO structure points to an array of SERVICE_TRIGGER structures, each specifying one trigger event.

The structure of SERVICE_TRIGGER and SERVICE_TRIGGER_INFO looks like:

public struct SERVICE_TRIGGER
{
    /// The trigger event type.
    public ServiceTriggerType dwTriggerType;

    /// The action to take when the specified trigger event occurs.
    public ServiceTriggerAction dwAction;

    /// Points to a GUID that identifies the trigger event subtype. The value
    /// of this member depends on the value of the dwTriggerType member.
    public IntPtr pTriggerSubtype;

    /// The number of SERVICE_TRIGGER_SPECIFIC_DATA_ITEM structures in the 
    /// array pointed to by pDataItems. 
    public uint cDataItems;

    /// A pointer to an array of SERVICE_TRIGGER_SPECIFIC_DATA_ITEM 
    /// structures that contain trigger-specific data. 
    public IntPtr pDataItems;
}
public struct SERVICE_TRIGGER_INFO
{
    /// The number of triggers in the array of SERVICE_TRIGGER structures 
    /// pointed to by the pTriggers member. 
    public uint cTriggers;

    /// A pointer to an array of SERVICE_TRIGGER structures that specify the 
    /// trigger events for the service. 
    public IntPtr pTriggers;

    /// This member is reserved and must be NULL.
    public IntPtr pReserved;
}

The structure is predefined and should not be changed. Now to call the API we will declare the API methods and corresponding structures. We will use the ServiceNative.cs provided by Microsoft which you can find in the sample code associated with this application. It uses advapi32.dll to P/Invoke native methods likeChangeServiceConfig2, QueryServiceConfig2 etc and also defines related structs and enumerations like SERVICE_TRIGGER, SERVICE_TRIGGER_INFO, SERVICE_TRIGGER_SPECIFIC_DATA_ITEM etc.
You can call ChangeServiceConfig2 to configure the service and hence check if the service is properly installed or not using QueryServiceConfig2.

SERVICE_TRIGGER contains few important important members. dwTriggerType defines the event type that you are about to define for which the service is going to react. The value of dwTriggerType can be :

  1. SERVICE_TRIGGER_TYPE_DEVICE_INTERFACE_ARRIVAL (1): The event is triggered when a specific device type defined in pTriggerSubtype as interface guid that arrives
  2. SERVICE_TRIGGER_TYPE_IP_ADDRESS_AVAILABILITY (2) : The event is triggered when first Network IP is available and vice versa.
  3. SERVICE_TRIGGER_TYPE_DOMAIN_JOIN (3) : The event is triggered when system is joined to a Domain. pTriggerSubtype should be DOMAIN_JOIN_GUID (1ce20aba-9851-4421-9430-1ddeb766e809) or DOMAIN_LEAVE_GUID(ddaf516e-58c2-4866-9574-c3b615d42ea1).
  4. SERVICE_TRIGGER_TYPE_FIREWALL_PORT_EVENT (4) : This event is triggred when firwall specific port is opened or 60 seconds after the port is blocked. The pTriggerSubtype should be specified with FIREWALL_PORT_OPEN_GUID(b7569e07-8421-4ee0-ad10-86915afdad09) or FIREWALL_PORT_CLOSE_GUID(a144ed38-8e12-4de4-9d96-e64740b1a524). You can use pDataItems to define port, protocol or executable path.
  5. SERVICE_TRIGGER_TYPE_GROUP_POLICY(5) : This event triggered when Machine Group policy is modified. pTriggerSubtype can have MACHINE_POLICY_PRESENT_GUID(659FCAE6-5BDB-4DA9-B1FF-CA2A178D46E0) or USER_POLICY_PRESENT_GUID(54FB46C8-F089-464C-B1FD-59D1B62C3B50).
  6. SERVICE_TRIGGER_TYPE_CUSTOM(20) : This event is triggrred by Event Tracer. pTriggerSubtype could be used to define event provider's GUID.

The dwAction defines the action that we are going to take when the event is triggered. dwAction can have two values :

  1. SERVICE_TRIGGER_ACTION_SERVICE_START : Specified to start the service. 
  2. SERVICE_TRIGGER_ACTION_SERVICE_STOP : Specified to stop the service.

Now lets create a service that will detect if network is connected and based on which It will produce a log entry(to make it the most simple) in text file. We can check the log file and see our custom message for testing. In your original application, the corresponding code will be executed on these events.  

To write log we use :

private void WriteLog(string message)
{
        using (FileStream fs = new FileStream(
                 Environment.GetFolderPath(Environment.SpecialFolder.System),
                 FileMode.Append, FileAccess.Write))
        {
           using (StreamWriter sw = new StreamWriter(fs))
           {
               sw.WriteLine(DateTime.Now.ToString() + "\t" + message);
               sw.Close();
            }
            fs.Close();
         }
 }

This function will write the message with DateTime to the system folder. From the OnStart and OnStop event of the service we wrote some silly messages to detect actually what is happenning.

Finally we need to configure the service by calling ChangeServiceConfig2 after the service gets installed in the machine. To do this the best candidate should be the AfterInstall event of the ProjectInstaller. The event occurs Just after the service is installed and thus it is the most right place to do this.

Right click on the Designer view of the and add Installer.

trigsrv2.jpg

A project installer will be added In the project installer. The project installer will have two objects created within it. Lets rename the ServiceInstaller1 to triggerStartServiceInstaller (You can leave it as it is) and change the ServiceName to WindowsTriggerService.

trigsrv1.jpg

Open the code window and write the code below inside the AfterInstall of the Project installer.

if (Environment.OSVersion.Version >= new Version(6, 1))
{
       //Means we find TriggerInfo supported Operating system
       this.IpArrivalTriggerConfigure(triggerStartServiceInstaller.ServiceName);
}
else
{
       throw new NotSupportedException();
}

You should note that Trigger start services are introduced in Windows 7 and you can use it in Windows 2008 R2. Thus we need to avoid installing it in lower version of windows. IpArrivalTriggerCapture is my own function that is used to configure the service. triggerStartServiceInstaller represents the serviceInstaller. You need to configure the Serviceinstaller 's servicename property to your own service.

To create the configuration section first, we need to Marshall the structs that points to the Network FirstIP arrival GUID and LastIP Removal GUID to its native pointers. Lets allocate a pointer and Marshall the structure to a pointer using the following code :

private IntPtr GetGUIDStructToPointer(Guid structure)
{
      IntPtr pointer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(Guid)));
      Marshal.StructureToPtr(structure, pointer, false);
      return pointer;
}
Marshall.AllocHGlobal is used to allocate memory pointer where we can marshall the structure. The pointer is returned.
 private SERVICE_TRIGGER AllocateServiceTrigger(ServiceTriggerAction action, 
                  ServiceTriggerType type, IntPtr subType)
 {
         SERVICE_TRIGGER st = new SERVICE_TRIGGER();
         st.dwTriggerType = type;
         st.dwAction = action;
         st.pTriggerSubtype = subType;
}

We use AllocateServiceTrigger to get the SERVICE_TRIGGER. We define ServiceTriggerAction, ServiceTriggerType and its SubType. The Device Data can also be configured here using SERVICE_TRIGGER_SPECIFIC_DATA_ITEM to pDataItems member, but it is not required in current context.

private IntPtr GetServiceTriggerInfo(IntPtr serviceTriggers, uint no_of_trigger)
{
        SERVICE_TRIGGER_INFO serviceTriggerInfo = new SERVICE_TRIGGER_INFO();
        serviceTriggerInfo.cTriggers = no_of_trigger;
        serviceTriggerInfo.pTriggers = serviceTriggers;
       // Marshal the SERVICE_TRIGGER_INFO struct to native memory
       IntPtr ServiceTriggerInfoPointer = Marshal.AllocHGlobal(Marshal.SizeOf(typeof(
                SERVICE_TRIGGER_INFO)));
       Marshal.StructureToPtr(serviceTriggerInfo, ServiceTriggerInfoPointer, false);
      return ServiceTriggerInfoPointer;
}

In this method I have allocated SERVICE_TRIGGER_INFO structure, marshall it to the memory and return back the pointer to the base location.
Finally lets look into the IPArrivalTriggerConfigure method :

private bool IpArrivalTriggerConfigure(string serviceName)
{
       Guid NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID = new Guid("4F27F2DE-14E2-430B-A549-7CD48CBC8245");
       Guid NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID = new Guid("CC4BA62A-162E-4648-847A-B6BDF993E335");
       using (ServiceController sc = new ServiceController(serviceName))
       {
           try
           {

                    IntPtr GuidIpAddressArrivalPointer = this.GetGUIDStructToPointer(
                                                  NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID);
                    IntPtr GuidIpAddressRemovalPointer = this.GetGUIDStructToPointer(
                                                 NETWORK_MANAGER_LAST_IP_ADDRESS_REMOVAL_GUID);
                    SERVICE_TRIGGER serviceTrigger1 = this.AllocateServiceTrigger(
                                                ServiceTriggerAction.SERVICE_TRIGGER_ACTION_SERVICE_START, 
                                                ServiceTriggerType.SERVICE_TRIGGER_TYPE_IP_ADDRESS_AVAILABILITY, 
                                                GuidIpAddressArrivalPointer);
                    SERVICE_TRIGGER serviceTrigger2 = this.AllocateServiceTrigger(
                                                ServiceTriggerAction.SERVICE_TRIGGER_ACTION_SERVICE_STOP, 
                                                ServiceTriggerType.SERVICE_TRIGGER_TYPE_IP_ADDRESS_AVAILABILITY, 
                                                GuidIpAddressRemovalPointer);

                    IntPtr pServiceTriggers = Marshal.AllocHGlobal(
                                             Marshal.SizeOf(typeof(SERVICE_TRIGGER)) * 2);
                    Marshal.StructureToPtr(serviceTrigger1, pServiceTriggers, false);
                    Marshal.StructureToPtr(serviceTrigger2, new IntPtr((long)pServiceTriggers 
                                           + Marshal.SizeOf(typeof(SERVICE_TRIGGER))), false);

                    IntPtr ServiceTriggerInfoPointer = this.GetServiceTriggerInfo(pServiceTriggers, 2);

                    bool bSuccess = ServiceNative.ChangeServiceConfig2(
                    sc.ServiceHandle.DangerousGetHandle(),
                    ServiceConfig2InfoLevel.SERVICE_CONFIG_TRIGGER_INFO, ServiceTriggerInfoPointer);
                    int errorCode = Marshal.GetLastWin32Error();

                    // Clean up the native resources
                    Marshal.FreeHGlobal(GuidIpAddressArrivalPointer);
                    Marshal.FreeHGlobal(GuidIpAddressRemovalPointer);
                    Marshal.FreeHGlobal(pServiceTriggers);
                    Marshal.FreeHGlobal(ServiceTriggerInfoPointer);

                    // If the service failed to be set as trigger start, throw the error 
                    // returned by the ChangeServiceConfig2 function.
                    if (!bSuccess)
                    {
                        Marshal.ThrowExceptionForHR(errorCode);
                        return false;
                    }
                    else return true;
       }
       catch { return false; }
       }
}

In the above code we first create pointers to the respective GUIDs to invoke the NETWORK_MANAGER_FIRST_IP_ADDRESS_ARRIVAL_GUID. The pointer is used to AlloacateServiceTrigger object. As we are registering the service to start and stop both, we have to create two object of SERVICE_TRIGGER. We Allocate an array of SERVICE_TRIGGER and get the pointer to the structure. Finally we create a pointer to the SERVICE_TRIGGER_INFO using GetServiceTriggerInfo method.

The API ChangeServiceConfig2 is called to configure the Service with ServiceTriggerInfoPointer.

bool bSuccess = ServiceNative.ChangeServiceConfig2(
                    sc.ServiceHandle.DangerousGetHandle(),
                    ServiceConfig2InfoLevel.SERVICE_CONFIG_TRIGGER_INFO, 
                    ServiceTriggerInfoPointer);
The first argument is Service Handle pointer. We will get the ServicePointer from ServiceController.ServiceHandle, we pass SERVICE_CONFIG_TRIGGER_INFO as second argument as it determines that the Service we are now going to configure is ServiceTrigger. And finally ServiceTriggerInfoPointer is the pointer to configure the service as ServiceTrigger.

Query a Service Using QueryServiceConfig2

Now let us look on how we can call QueryServiceConfig2 to determine if the service is already registered to Start Trigger Service.  Lets look at the code below :

private bool IsQueryService(string serviceName)
{
    using (ServiceController sc = new ServiceController(serviceName))
    {
         int outBytes = -1;
         ServiceNative.QueryServiceConfig2(
            sc.ServiceHandle.DangerousGetHandle(),
            ServiceConfig2InfoLevel.SERVICE_CONFIG_TRIGGER_INFO, IntPtr.Zero, 0,
            out outBytes);
        if (outBytes == -1)
        {
              // Failed to allocate bytes
              return false;
        }
        IntPtr pBuffer = Marshal.AllocHGlobal(outBytes);
        try 
        {
            if (ServiceNative.QueryServiceConfig2(sc.ServiceHandle.DangerousGetHandle(),
                ServiceConfig2InfoLevel.SERVICE_CONFIG_TRIGGER_INFO, pBuffer,
                outBytes, out outBytes)!= 0)
            {
                //win32 Exception occurred.
                return false;
            }
            SERVICE_TRIGGER_INFO sti = (SERVICE_TRIGGER_INFO)
            Marshal.PtrToStructure(pBuffer, typeof(SERVICE_TRIGGER_INFO));
            return (sti.cTriggers > 0);
       } 
       catch{}
       finally
       {  
            // Free the memory 
            if (pBuffer != IntPtr.Zero)
            { 
                Marshal.FreeHGlobal(pBuffer);
            }
      }
  }// end using
} 

If you inspect the above method, I have called the API QueryServiceConfig2 two times,  the first call is made with null pointer. This call will fail as we are not passing any service handler to it and this is basically to determine how many bytes needed to allocate for the current system to query the service.

For the next call, which will give you the actual result we first allocate a buffer with the same size as retrieved from the first call with Null Pointer.

ServiceNative.QueryServiceConfig2(sc.ServiceHandle.DangerousGetHandle(),
                        ServiceConfig2InfoLevel.SERVICE_CONFIG_TRIGGER_INFO, pBuffer,
                        outBytes, out outBytes)

We pass the Service handle to the API as first argument. The second argument is the Trigger Info Configuration(0x00000008) which is already defined in the ServiceNative. pBuffer is the allocated Buffer pointer with the outBytes.

The API will return 1 if the Trigger is found, otherwise returns 0.
To get the actual TriggerInfo object we Marshall the pBuffer to SERVICE_TRIGGER_INFO as below :

SERVICE_TRIGGER_INFO sti = (SERVICE_TRIGGER_INFO)
                        Marshal.PtrToStructure(pBuffer, typeof(SERVICE_TRIGGER_INFO));

Thus we find the information of the Trigger info easily using API QueryServiceConfig2.  

To Summarize 

Before we conclude let us Summarize the concept :

  • There are two option to Configure a Service to Trigger Start, First using sc.exe with Triggerinfo attribute, and another programmatically. As InstallUtil which installs .NET services does not support TriggerInfo switch, we did using ChangeServiceConfig2 API method defined in advapi32.dll
  • ChangeServiceConfig2 requires a pointer to ServiceTriggerInfo structure.
  • The SERVICE_TRIGGER_INFO should be assigned to SERVICE_TRIGGER objects each of which points to the service events that we want to configure.
  • SERVICE_TRIGGER configures the type of each event.

Steps to Test the Application

  • Install the application. You can either use
    Installutil.exe windowstriggerservice.exe, (if current windows supports Trigger Start service, the command prompt will print "Configuring trigger-start service") or you can use Setup Application. After you install you can inspect if the service console if the service is installed properly or not. You can see the service listed in the Services.msc applet.

    trigsrv3.jpg
  • Check in Windows\System32 folder and check the file TriggerServiceLog.txt is present.

    trigger1.jpg
  • Now to check even further, open Command Prompt and type
    sc qtriggerinfo WindowsTriggerService

    qsrvTrig.jpg
    You can see if the Appropriate Triggers are installed as shown in the figure
  • Now, Open Network properties and Disable Network.
    trigger2.jpg
  • Inspect the TriggerServiceLog.txt file to see if there is any changes made. You will find a message "Service Stopped as LastIP removed"
  • Finally, Enable the Network again and inspect the file. You will see that the trigger is started automatically again.

References

History

4 Jan 2010 : Initial Post
11 Jan 2010 : Added the use of QueryServiceConfig2 to query for Trigger Service

Conclusion   

I have tried to build the sample application as simple as possible to ensure that even a newbie could understand what is required to do.
Thanks for Reading, also love to see your feedback.


License

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

About the Author

Abhishek Sur
Architect
India India
Did you like his post?
 
Oh, lets go a bit further to know him better.
Visit his Website : www.abhisheksur.com to know more about Abhishek.
 
Abhishek also authored a book on .NET 4.5 Features and recommends you to read it, you will learn a lot from it.
http://bit.ly/EXPERTCookBook
 
Basically he is from India, who loves to explore the .NET world. He loves to code and in his leisure you always find him talking about technical stuffs.
 
Presently he is working in WPF, a new foundation to UI development, but mostly he likes to work on architecture and business classes. ASP.NET is one of his strength as well.
Have any problem? Write to him in his Forum.
 
You can also mail him directly to abhi2434@yahoo.com
 
Want a Coder like him for your project?
Drop him a mail to contact@abhisheksur.com
 
Visit His Blog

Dotnet Tricks and Tips



Dont forget to vote or share your comments about his Writing
Follow on   Twitter   Google+

Comments and Discussions

 
GeneralMy vote of 5 PinmemberPhil J Pearson28-Sep-10 5:13 
GeneralGUI for adding/editing/removing Service Triggers PinmemberHoward Robinson10-Jan-10 6:30 
GeneralRe: GUI for adding/editing/removing Service Triggers PinmemberAbhishek Sur10-Jan-10 6:33 
Hey Howard,
 
I have already tried that. Its a good tool that you produced. Great one.
I like it.
 
Cheers.
Rose | [Rose]
 
Abhishek Sur
Don't forget to click "Good Answer" if you like this Solution.
My Latest Articles-->

Windows7 API Code Pack

Simplify Code Using NDepend
Basics of Bing Search API using .NET

GeneralA really good job here! PinmemberKentuckyEnglishman5-Jan-10 7:27 
GeneralRe: A really good job here! PinmemberAbhishek Sur5-Jan-10 21:35 
GeneralYou are doing great ! PinmvpAbhijit Jana4-Jan-10 6:52 
GeneralRe: You are doing great ! PinmemberAbhishek Sur4-Jan-10 7:01 
Generalexcellent article PinmemberRavenet3-Jan-10 23:11 
GeneralRe: excellent article PinmemberAbhishek Sur3-Jan-10 23:12 
GeneralGreat Article PinmemberKavan Shaban3-Jan-10 10:19 
GeneralRe: Great Article PinmemberAbhishek Sur3-Jan-10 10:27 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.140721.1 | Last Updated 17 Jan 2010
Article Copyright 2010 by Abhishek Sur
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid