Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Task Scheduler Library for .NET

0.00/5 (No votes)
9 Jul 2009 5  
A library for .NET that encapsulates the Task Scheduler COM object.

Important Notice!!!

This library has been significantly changed and updated to include all the functionality available in the Task Scheduler 2.0 library available on Vista/Server 2008 and later. The new library is available as a community project on GitHub and is available via NuGet. This new library automatically supports all current Windows platforms and uses the most current library. It compensates for missing features from both libraries where possible. The model is simpler and more modular. I won't be continuing any work on the code posted here in CodeProject as it does not conform to the constructs of the updated library.

The basics of building .NET Interop wrappers around COM libraries that are documented here are what I used for the new project. If you’re trying to understand how to do that, look here.

Thanks.

Introduction

The Task Scheduler is a component delivered with Internet Explorer since version 4.0, and is made up of a set of COM objects. The user can interact with tasks through the "Scheduled Tasks" entry in the Control Panel. The Task Scheduler objects provide an excellent means of scheduling events for your programs. I have never really liked the "pseudo object-oriented" approach taken by Microsoft in their design of COM objects. I have wrapped the Task Scheduler COM objects into a series of .NET classes. These classes expose the entire library and make it conform to more of a ".NET" style of class design.

In this article, I will present the library and the things I learned along the way about using .NET development and COM Interop services. This article has the following sections:

Learnings

One of the things I wanted to do was provide a single library that would give full access to the Task Scheduler. This meant that I could not use the Visual Studio .NET model of importing COM objects which creates a new .dll file for each imported class. To accomplish this, I had to use some .NET Framework SDK tools to create the .cs file that would provide access to the COM objects via COM Interop services. Since Microsoft decided to not provide a type library for the Task Scheduler, I created one with the MIDL tool from the Platform SDK and the mstask.idl file provided by Microsoft. Once I had the library, I was able to use the TLBIMP tool from the .NET Framework SDK to create an assembly. I then used the Object Browser along with the original .idl file to effectively cut and paste the class, method, and property definitions to my own .cs file. During this part of my adventure, I discovered two important things. First, the order of methods in the interop classes must match the order in the .idl file, and if an interface derives from another interface, you have to provide the base interface's methods in the interop (there is no derivation allowed). Second, the TLBIMP tool does some special things that aren't initially apparent in the area of attributes.

  • All structures must have the [StructLayout(LayoutKind.Sequential)] attribute defined.
  • All unions must have the [StructLayout(LayoutKind.Explicit)] attribute defined, and each block of the union must have the [FieldOffset(0)] attribute defined.
  • All parameters that convert from a wide string to System.String must have the [MarshalAs(UnmanagedType.LPWStr)] attribute defined.
  • All parameters that pass a COM interface must have the [MarshalAs(UnmanagedType.Interface] attribute defined.
  • All parameters that pass a .idl defined structure must have the [MarshalAs(UnmanagedType.Struct)] attribute defined.
  • Dealing with pointers to variable length parameters is a black art.
  • If the .idl file says a parameter is [in], the parameter is [In] in C#.
  • If the .idl file says a parameter is [out], the parameter is [Out] in C#.
  • If the .idl file says a parameter is [in, out], mark the parameter as ref and specify if it is really [In] and/or [Out].
  • If the .idl file says a parameter is an [out] pointer to a value, you do not need to make it a pointer in C#.
  • If the .idl file says a parameter is a pointer to a pointer (**) and it is not an interface, you have to make it a System.IntPtr and deal with converting it in the code.
  • When getting an interface pointer where the .idl file has it as [out] ISomeInterface** pSomeInt, you will use [Out, MarshalAs(UnmanagedType.Interface)] out ISomeInterface SomeInt.
  • If a method returns an HRESULT as an out or retval parameter, it must have the [MarshalAs(UnmanagedType.Error] attribute and be passed as a System.Int32.

There are two bits of coding that were nontrivial. The first was the Hidden property on the Task class. Warning, this was a hack. I realized that the tasks are actually just files in a special directory. Once they are created, you have to use the IPersist interface to save them. This interface lets you find out the path of the file. Using this information, I experimented and found that setting the hidden attribute on the file does hide the task from the UI but does not seem to have an effect on the COM object finding it. I did have to clear that attribute before saving it though.

The second challenge was demystifying the black art of variable length arrays. This was done in the Task.Tag property. I decided to use serialization to get and put information into the tag. This provides a way to encode and decode objects into a byte stream. To put the object into the array, I first make sure it is serializable. I then serialize it into a MemoryStream and pass the stream's buffer into the COM object.

set
{
   if (!value.GetType().IsSerializable)
      throw new ArgumentException("Objects set as Data for Tasks must " + 
                                  "be serializable", "value");
   BinaryFormatter b =3D new BinaryFormatter();
   MemoryStream stream =3D new MemoryStream();
   b.Serialize(stream, value);

   iTask.SetWorkItemData((ushort)stream.Length, stream.GetBuffer());
}

To get the object out of the array, I had to use the Marshal.Copy function to convert the IntPtr to a byte array. I then converted the byte array into a BinaryFormatter object so that I could convert the data back into an object instance. The code below shows the process.

get
{
   ushort DataLen;
   IntPtr Data;
   iTask.GetWorkItemData(out DataLen, out Data);
   byte[] bytes =3D new byte[DataLen];
   Marshal.Copy(Data, bytes, 0, DataLen);
   MemoryStream stream =3D new MemoryStream(bytes, false);
   BinaryFormatter b =3D new BinaryFormatter();
   return b.Deserialize(stream);
}

Library Documentation

Included with the library is an HTML help file describing all of the objects defined. The following graphic provides a broad overview of the classes.

The Scheduler class represents the machine specific instance of the system task scheduler. It has very little use other than providing access to the list of tasks through its Tasks property. The Tasks property exposes a TaskList instance that provides an indexer which allows access to individual tasks by name. The TaskList class also has methods that allow for the creation and deletion of tasks by name. Since TaskList implements the IEnumerable interface, you can also enumerate all tasks using the foreach construct.

A task is represented by a Task instance. The Task class exposes all of the properties of a task which allow you to define what will run when the task is triggered. The only property that must be set for proper execution is ApplicationName, which specifies the full path of the executable which will be run when triggered. The Task class also provides some properties that give information about the execution of the task and some methods that allow for the running and termination of a task.

Each task has a list of triggers that determine when the task will be run. These are accessed through the Triggers property which exposes a TriggerList instance. TriggerList provides an indexer which allows access to individual triggers by their position in the list. The TriggerList class also has methods that allow for the addition and removal of triggers. TriggerList implements the IList interface so you can also enumerate all tasks using the foreach construct.

The Trigger class is an abstract class that forms the foundation of the different types of triggers that can be specified for a task. There are eight different specializations that provide different ways to specify the time a task will run. See the help file for details about each of the trigger classes.

Example Code

// Write out information on all tasks and triggers

// Can optionally specify computer (e.g. @"\\hostname")

Scheduler sched = new Scheduler();   

foreach (Task t in sched.Tasks) 
{
   Console.WriteLine(t.ToString());
   foreach (Trigger tr in t.Triggers)
      Console.WriteLine(tr.ToString());
}

// Set only trigger on an existing task to be an idle trigger

Task t1 = sched.Tasks["Disk Cleanup"];
if (t1 != null)
{
   t1.Triggers.Clear();
   t1.Triggers.Add(new OnIdleTrigger());
   t1.Save();
}

// Create a new task with one of each kind of trigger

Task t2;
try 
{
   t2 = sched.Tasks.NewTask("Testing");
   t2.ApplicationName = "notepad.exe";
   t2.Comment = "Testing Notepad";
   t2.Creator = "Author";
   t2.Flags = TaskFlags.Interactive;
   t2.Hidden = true;
   t2.IdleWaitDeadlineMinutes = 20;
   t2.IdleWaitMinutes = 10;
   t2.MaxRunTime = new TimeSpan(1, 0, 0);
   t2.Parameters = @"c:\test.log";
   t2.Priority = System.Diagnostics.ProcessPriorityClass.High;
   t2.WorkingDirectory = @"c:\";
   t2.Triggers.Add(new RunOnceTrigger(DateTime.Now + TimeSpan.FromMinutes(1.0)));
   t2.Triggers.Add(new DailyTrigger(8, 30, 1));
   t2.Triggers.Add(new WeeklyTrigger(6, 0, DaysOfTheWeek.Sunday));
   t2.Triggers.Add(new MonthlyDOWTrigger(8, 0, DaysOfTheWeek.Monday | 
                                               DaysOfTheWeek.Thursday, 
                                               WhichWeek.FirstWeek | 
                                               WhichWeek.ThirdWeek));
   int[] days = {1,8,15,22,29};
   t2.Triggers.Add(new MonthlyTrigger(9, 0, days, MonthsOfTheYear.July));
   t2.Triggers.Add(new OnIdleTrigger());
   t2.Triggers.Add(new OnLogonTrigger());
   t2.Triggers.Add(new OnSystemStartTrigger());
   t2.SetAccountInformation("DOMAIN\\username", "mypassword");
   t2.Save();
}
catch {}

// Remove the idle trigger from the task

Trigger trigger = new OnIdleTrigger();
int idx = t2.Triggers.IndexOf(trigger);
if (idx != -1)
   t2.Triggers.RemoveAt(idx);

// Delete a task

sched.Tasks.Delete("Testing");

History

22 January 2002

   

Original posting.

1 April 2002

 

Converted all uint variables to int on public classes and enums to make it work better in VB.

2 April 2002

 

Added exception handling to some task properties that should not generate errors.

14 May 2002

 

Incorporated fixes to the Trigger.Equals method (it works now) and the MonthlyTrigger class to properly set days of month. Enhanced the Scheduler.TargetComputer property and the corresponding constructor to allow for computer names without preceding "\\". Thanks to Bob Grimshaw for the fixes.

25 March 2008

 

Moved project to CodePlex and updated to include Task Scheduler 2.0 functionality and support.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here