First time at my blog? Check out the table of contents! x

March 2008 Entries

If you are searching for instructions on how to write your own DotNetNuke scheduled task, then you've come to the right blog post. This article discusses the steps that I take in order to write a task that I need DotNetNuke to perform periodically for me.

DotNetNuke has several little-known gems that can add some powerful functionality to your web site. Scheduled tasks are one of those gems that a lot of people don't know about, or at least don't take advantage of. As we've seen in the past, the documentation on the DotNetNuke web site is either out of date, incorrect, or incomplete. So to remedy this, and hopefully get more people writing their own scheduled task, I'm going to show you how I go creating a custom scheduled task. I'll also share some working examples that will help you get started quickly.

On with the show.

 

Start Using DLLs

As far as I am aware, scheduled tasks need to be compiled into a DLL file. This seem to be implied in the document, though I haven't seen anywhere that explicitly states this. If you are already building DNN modules using the Web Application Project (WAP) method, then all you need to do is create your scheduled task inside your normal project. WAPs will compile nicely up into one or more DLL files without any fuss.

If you haven't taken the jump to using WAP, and you're using WST (Web Site Project), or you don't write custom DNN modules at all, then you have the option of creating your scheduled task inside its own Class Library project. You will need Visual Studio 2005 or 2008 to do this. Class Library projects compile up into a DLL file quite nicely. If you are using VB, make sure that you sent the root Namespace of your project to blank. This is KEY! You will also need to add a reference to the DotNetNuke.dll to your Class Library project. DotNetNuke.dll can be found in the bin directory of your DotNetNuke installation.

If you don't have access to Visual Studio 2005 or 2008, and are using Visual Web Developer Express to write DNN modules, then you might be out of luck. Visual Web Developer Express doesn't compile into DLLs for distribution. However, you might want to look into using Visual Studio Express... it might be able to compile to DLL. Perhaps one of the other readers can suggest a method.

 

Sub-Class From SchedulerClient

Once you have nailed down a method for rolling your code into a DLL file, you can begin writing your scheduled task. All of your scheduled tasks begin by sub-classing from the DotNetNuke.Services.Scheduling.SchedulerClient class.

Example - C#:

class ScheduledTaskExample : SchedulerClient

Example - VB:

Public Class ScheduledTaskExample
    Inherits SchedulerClient

 

Overload Your Class Constructor

The next thing that DotNetNuke requires from a scheduled task is an overloaded constructor that accepts a DotNetNuke.Services.Scheduling.ScheduleHistoryItem object. This overloaded constructor must also call the constructor of its parent class. This is done by using the base() syntax in C#, and the MyBase.New() syntax in VB. The final thing that your constructor must do is assign the ScheduleHistoryItem parameter to the ScheduleHistoryItem property of the class. Here is an example of a constructor that meets these requirements:

Example - C#:

public ScheduledTaskExample(ScheduleHistoryItem objScheduleHistoryItem)
    : base()
{
    ScheduleHistoryItem = objScheduleHistoryItem;
}

Example - VB:

Public Sub New(ByVal objScheduleHistoryItem As ScheduleHistoryItem)
    MyBase.New()
    Me.ScheduleHistoryItem = objScheduleHistoryItem
End Sub

 

Override DoWork() Method

The DoWork() method is the entry point for the execution of your scheduled task. When the DotNetNuke portal determines that it is time for your scheduled task to run, it creates an instance of your class and calls its DoWork() method. So this is where you put all your custom functionality.

There are a few things to keep in mind when writing this method. First, you need to include logic that alerts the portal to whether or not your task succeeded at whatever it needed to do. The way you do this is by enclosing your code within a try-catch block. If your code runs as planned, then you set the Succeeded property of the ScheduledHistoryItem member to true. Otherwise, you set the Succeeded property to False. In the catch portion of your try-catch block you will also want to set the Succeeded property to False so that when an exception is thrown, the scheduled task will report that it failed.

Example - C#:

public override void DoWork()
{
    try
    {
        // do some stuff ...

        // then report success to the scheduler framework
        ScheduleHistoryItem.Succeeded = true;
    }

    // handle any exceptions
    catch (Exception exc)
    {
        // report a failure
        ScheduleHistoryItem.Succeeded = false;

        // log the exception into
        // the scheduler framework
        ScheduleHistoryItem.AddLogNote("EXCEPTION: " + exc.ToString());

        // call the Errored method
        Errored(ref exc);

        // log the exception into the DNN core
        Exceptions.LogException(exc);
    }
}

Example - VB:

Public Overrides Sub DoWork()
    Try
        ' do some stuff ...


        ' then report success to the scheduler framework
        ScheduleHistoryItem.Succeeded = True

    Catch ex As Exception
        ' handle any exceptions

        ' report a failure
        ScheduleHistoryItem.Succeeded = False

        ' log the exception into
        ' the scheduler framework
        ScheduleHistoryItem.AddLogNote("EXCEPTION" + ex.ToString())

        ' call the Errored method
        Errored(ex)

        ' log the exception into the DNN core
        Exceptions.LogException(ex)

    End Try
End Sub

 

You will notice in the examples above that there are a few other things you should do when an exception occurs. You should log the exception into the scheduler framework which allows you to view the exception in your scheduler's history page. This is done by calling the AddLogNote() method on the ScheduleHistoryItem member. Secondly, you need to call the parent class's Errored() method so it can take its own necessary actions. Lastly, you should log the exception into the DNN core exception log by calling the LogException method on the DotNetNuke.Services.Exceptions.Exceptions class.

 

Compile & Install

Once you are finished overriding the DoWork() method you are ready to compile and install your new scheduled task. Compiling instructions will vary depending upon what tool and project type you are using, so I will not discuss those details here. Once you are compiled up into a DLL file you will want to copy the DLL file into the bin directory of your DNN Web site installation.

To install your new task you go into Host menu > Schedule  page and click on the Add Item to Schedule link.

 

image

 

Then enter the full class name, followed by a comma, followed by the name of your DLL. Like so:

 

image

 

Configure it up, hit Update, and you should be good to go.

 

Some Tips

Since scheduled tasks do not run in the context of a Web page execution, nor in the context of any particular DotNetNuke portal, you will often need to find alternative ways to access the information that your task requires. Instead of Server.Globals.MapPath() use System.Web.HttpRuntime.AppDomainAppPath to get the file path to your DNN installation. If your task uses some of the DNN framework methods that require you to pass the ID of a portal, then you can either use a PortalController to get all of the portal IDs and loop through them, or you can access particular portal IDs by storing and retrieving them using System.Configuration.ConfigurationSettings.AppSettings. AppSettings allows you to store configuration information inside the .config files within the .Net configuration hierarchy. Likewise for tab IDs and module IDs, you may have to manually store and retrieve these from in a .config file.

Another thing to keep in mind is that the Add/GetSetting methods on the ScheduleHistoryItem class really suck. You might be tempted to use it, but you'll quickly be scratching your head because there are no easy ways to update an existing setting. So you will need to find an alternate method to store settings for your scheduled task.

 

Working Examples!!!

Here is an example of a working scheduled task. It writes out a file to the root folder of your web application.

C# Version - ScheduledTaskExample.cs:

using System;
using System.IO;
using System.Web;

using DotNetNuke.Services.Scheduling;
using DotNetNuke.Services.Exceptions;

namespace Kemmis.Examples.DotNetNuke
{
    class ScheduledTaskExample : SchedulerClient
    {
        public ScheduledTaskExample(ScheduleHistoryItem objScheduleHistoryItem)
            : base()
        {
            ScheduleHistoryItem = objScheduleHistoryItem;
        }

        public override void DoWork()
        {
            try
            {
                // perform some task                
                String strPath = HttpRuntime.AppDomainAppPath + "CS_DID_IT.TXT";
                using (StreamWriter sw = new StreamWriter(strPath, true))
                {
                    sw.WriteLine(DateTime.Now.ToString() + " - C# DID IT!");
                    sw.Close();
                }

                // report success to the scheduler framework
                ScheduleHistoryItem.Succeeded = true;
            }
            catch (Exception exc)
            {
                ScheduleHistoryItem.Succeeded = false;
                ScheduleHistoryItem.AddLogNote("EXCEPTION: " + exc.ToString());
                Errored(ref exc);
                Exceptions.LogException(exc);
            }
        }
    }
}

 

VB Version - ScheduledTaskExample.vb:

Imports DotNetNuke.Services.Scheduling
Imports DotNetNuke.Services.Exceptions

Imports System
Imports System.Web
Imports System.IO

Namespace Kemmis.Examples.DotNetNuke

    Public Class ScheduledTaskExample
        Inherits SchedulerClient

        ' requires a special constructor which
        ' accepts a ScheduleHistoryItem 
        Public Sub New(ByVal objScheduleHistoryItem As ScheduleHistoryItem)
            MyBase.New()
            Me.ScheduleHistoryItem = objScheduleHistoryItem
        End Sub


        Public Overrides Sub DoWork()
            Try
                ' perform some tasks
                Dim strPath As String = HttpRuntime.AppDomainAppPath + "VB_DID_IT.TXT"
                Using sw As StreamWriter = New StreamWriter(strPath, True)
                    sw.WriteLine(DateTime.Now.ToString() + " - VB DID IT!")
                    sw.Close()
                End Using

                ' report success to the scheduler framework
                ScheduleHistoryItem.Succeeded = True

            Catch ex As Exception

                ScheduleHistoryItem.Succeeded = False
                ScheduleHistoryItem.AddLogNote("EXCEPTION" + ex.ToString())
                Errored(ex)
                Exceptions.LogException(ex)

            End Try
        End Sub
    End Class

End Namespace

 

At the risk of appearing overly verbose, I want to present the topic of DotNetNuke permissions by working from the database level all the way up through the data providers, controllers, and info classes. One of the benefits of using layers of abstraction is that it allows programmers like you and me to work with frameworks without having to know what the cogs inside are doing. However, I find that if you're going to be using the classes in the DotNetNuke framework, then you really must know what is going on behind the scenes. Since there is very little documentation at this time, the only way to learn how to work with the framework is to dig deep. So let's do it.

The Common Patterns

If you haven't picked up on it by now, just about everything within the DotNetNuke framework follows the same few patterns. The tables in the database have associated stored procedures (sprocs) that are used for CRUD (create, read, update, delete) operations. Inside the data provider classes there are methods for CRUD operations that call the sprocs in the database. For each genera of framework functionality there exists controller classes that interact with the data provider in order to provide strongly-typed classes that encapsulate the information from the database. In one way or another, the controller classes have the data from the database "hydrated" into the info classes that you end up accessing in your code (i.e. UserInfo, PermissionInfo, ModuleInfo...). The controller also "un-hydrates" the data from the info classes when in order to send the information back to the data provider. This is how the permissions system works in the DotNetNuke framework.

In The Database

For this portion of the article I will assume that you are using MSSQL and the SqlDataProvider. For those of you who are using some other database, you are freaks! Get with the show! But really, your job will be so much less painful if you use MSSQL with .Net. I say this coming from several years of developing .Net against FoxPro, so trust me on this. But I digress.

The DNN database contains several tables that are pertinent to our discussion. First we have the Permission table. This table holds the different types of permission that you can give to a user or a role. For example "View", "Edit", and "Full Access" are three of the permissions in this table. The PermissionCode column is a string of text that identifies what part of the system or what module the permission is used by. The built-in permissions for the framework begin with the text "SYSTEM_". If your code needs to give custom permissions to its entities, then you could use this table in order to define those permissions. You just need to create your own PermissionCode string as well as set the ModuleDefID column to the ID of the definition for your module. My guess is that there are facilities in the DNN module definition system to register this information for you. Also in the Permission table there is a PermissionKey table. This column, along with PermissionCode in essence establish a compound key for the record to be referenced with.

The other tables that we are interested in are the FolderPermission, ModulePermission, and TabPermission tables. These guys hold granular permission settings for you, you guessed it, folders, module instances, and pages, respectively. Records in these tables will contain the ID of a user or a role, along with the ID of a folder, module instance, or a tab. Records in these tables also have a foreign key to the Permission table so to specify what type of permission you are giving the user or role.

Sprocs

As I stated before, the DotNetNuke framework utilizes stored procedures in its SqlDataProvider to do all its CRUD operations in the database. There are too many to list here, but suffice it to say there is a stored procedure for just about every method defined in the DotNetNuke abstract data provider class (DotNetNuke.Data.DataProvider). Inside the SqlDataProvider, the SqlHelper class is used to execute these sprocs. So if you ever wonder where the SQL code is, the sprocs are where you need to look.

The Info Classes

In order to provide strongly-typed entities to work with, the DNN framework has classes that hold the information from a single record in the database. The FolderPermission, ModulePermission, and TabPermission classes in the DotNetNuke.Security.Permissions namespace are used to encapsulate records from the FolderPermission, ModulePermission, and TabPermission tables, respectively. These classes typically have a public property for each column of their respective table in the database.

The Controller Classes

When you work with permission, you don't have to interact with the data provider directly. You instead use an instance of a controller class. The FolderPermissionController, ModulePermissionController, and TabPermissionController classes in the DotNetNuke.Security.Permissions namespace contain methods that will perform most of the common CRUD operations on the FolderPermission, ModulePermission, and TabPermission tables. When you need to retrieve information from one of the tables in the database you call a Get method on one of these controller, and you will receive an Info object, or a collection of Info objects back. When you want to create or update records in one of the tables in the database you call an Add or Update method on one of these controllers, and pass it a populated instance of one the associated Info classes. This pattern keeps your code clean, and abstracts all of the data access to its own layer.

On To The Good Stuff - Examples

Here is an example of how you can add a new ModulePermission for either a user or a role.

ModulePermissionExample1CS.ascx:

<%@ Control Language="C#" AutoEventWireup="true" CodeFile="ModulePermissionExample1CS.ascx.cs"
    Inherits="ModulePermissionExample1CS" %>
<asp:DropDownList ID="ModulesDropDownList" runat="server" Width="300px">
</asp:DropDownList>
<br />
<asp:DropDownList ID="UsersDropDownList" runat="server" Width="300px">
</asp:DropDownList>
<br />
<asp:DropDownList ID="RolesDropDownList" runat="server" Width="300px">
</asp:DropDownList>
<br />
<asp:DropDownList ID="PermissionsDropDownList" runat="server" Width="300px">
</asp:DropDownList>
<br />
<asp:Button ID="Button1" runat="server" Text="Add Permission" 
    onclick="Button1_Click" />

 

ModulePermissionExample1CS.ascx.cs:

using System;

using DotNetNuke.Entities.Modules;
using DotNetNuke.Security.Permissions;
using DotNetNuke.Entities.Users;
using DotNetNuke.Security.Roles;

public partial class ModulePermissionExample1CS : PortalModuleBase
{
    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
            // populate Modules drop-down lists
            ModuleController moduleController 
                = new ModuleController();
            // get ArrayList of ModuleInfo
            ModulesDropDownList.DataSource 
                = moduleController.GetAllTabsModules(PortalId, false);
            ModulesDropDownList.DataTextField = "ModuleTitle";
            ModulesDropDownList.DataValueField = "ModuleID";
            ModulesDropDownList.DataBind();
            ModulesDropDownList.Items.Insert(0, "- Select Module Instance -");

            // populate Users drop-down lists
            // get ArrayList of UserInfo
            UsersDropDownList.DataSource 
                = UserController.GetUsers(PortalId, false);
            UsersDropDownList.DataTextField = "Username";
            UsersDropDownList.DataValueField = "UserID";
            UsersDropDownList.DataBind();
            UsersDropDownList.Items.Insert(0, "- Select User -");

            // populate Roles drop-down list
            RoleController roleController 
                = new RoleController();
            // get ArrayList of RoleInfo
            RolesDropDownList.DataSource = roleController.GetRoles();
            RolesDropDownList.DataTextField = "RoleName";
            RolesDropDownList.DataValueField = "RoleID";
            RolesDropDownList.DataBind();
            RolesDropDownList.Items.Insert(0, "- Select Role -");

            // populate Permissions drop-down list
            PermissionController permissionController 
                = new PermissionController();
            PermissionsDropDownList.DataSource 
                = permissionController.GetPermissionByCodeAndKey("SYSTEM_MODULE_DEFINITION", "");
            PermissionsDropDownList.DataTextField = "PermissionName";
            PermissionsDropDownList.DataValueField = "PermissionID";
            PermissionsDropDownList.DataBind();
            PermissionsDropDownList.Items.Insert(0, "- Select Permission -");
        }
    }
    protected void Button1_Click(object sender, EventArgs e)
    {
        // create instance of ModulePermissionController
        // to do all the work for us
        ModulePermissionController modPermissionController
            = new ModulePermissionController();

        // create instance of ModulePermissionInfo to
        // hold our permission information
        ModulePermissionInfo modPermInfo
            = new ModulePermissionInfo();

        // i'm not sure if AllowAccess is actually used
        // but the framework seems to set this to true
        // when it adds ModuleInfo instances to the DB
        modPermInfo.AllowAccess = true;

        // set id of the permission that was selected
        modPermInfo.PermissionID
            = int.Parse(PermissionsDropDownList.SelectedValue);

        // set id of the Module, not the TabModule
        // ... i know, this is confusing
        modPermInfo.ModuleID = int.Parse(ModulesDropDownList.SelectedValue);

        if (UsersDropDownList.SelectedIndex > 0)
        {
            // set id of user
            modPermInfo.UserID = int.Parse(UsersDropDownList.SelectedValue);

            // save the ModulePermissionInfo to the db
            modPermissionController.AddModulePermission(modPermInfo);
        }
        else if (RolesDropDownList.SelectedIndex > 0)
        {
            // set id of role
            modPermInfo.RoleID = int.Parse(RolesDropDownList.SelectedValue);

            // save the ModulePermissionInfo to the db
            modPermissionController.AddModulePermission(modPermInfo);
        }

        // set drop-down indexes back to 0
        ModulesDropDownList.SelectedIndex = 0;
        UsersDropDownList.SelectedIndex = 0;
        RolesDropDownList.SelectedIndex = 0;
        PermissionsDropDownList.SelectedIndex = 0;
    }
}

 

Who Is Rafe

rafe

Rafe Kemmis

I am an audacious web developer with a double bachelor of science in Computer Science and Mathematics. I specialize in Microsoft ASP.Net, Silverlight, and Adobe ActionScript.

Questions?

Always a thoughtful response. You may post your question on an article, or contact me directly.

Hire Me.

I provide custom solutions to complex problems. I can help your business no matter how large or small.

Contact me now.

Subscribe