Av rating:
Total votes: 2
Total comments: 0


Joe Webb
Creating a Custom Content Formatter for SSNS 2005
24 April 2007

Microsoft SQL Server Notification Services 2005 (SSNS 2005) provides a foundation for developing and hosting notification applications. Once the technology's terminology, architecture, and core components are adequately understood, it's possible to build robust and scalable notification applications in relatively short order.

Often, these applications can be wholly created using only the components provided with the product. For example, a notification application designed to email a simple order acknowledgment to customers of an online store is a relatively straightforward task. The built-in SQL Server Event Provider may be employed to recognize new rows in the Orders table and the SMTP Delivery Channel is well suited for formatting notifications and handing them off to an email server for delivery.

In short, the built-in components for SSNS 2005 are ideal for use with notifications that are flat in structure. However, as is often the case in real-world applications, the requirements may be a bit more complex than the simple scenario just described. For example, our requirements might be to email to the customer a more complex, hierarchical order confirmation letter, complete with the line item order details.

Using SSNS 2005 to send notifications that are more hierarchical in structure, like the order detail confirmation letter, will necessitate the creation and use of a custom component.

Creating a custom component in Visual Studio .NET 2005

Rather than try to provide a built-in component for every occasion, Microsoft simply made available the SSNS 2005 API, which exposes the underlying SSNS classes that we, as database developers, can use to create our own custom components. These components can perform a variety of tasks, from collecting event data from proprietary data sources to sending notifications through most any means with which we can communicate (a fax server, a SMS gateway, or even an Instant Messenger service).

To examine the capabilities of the SSNS 2005 API, we'll continue with the order confirmation scenario. To satisfy the requirements, we will create a custom content formatter that will receive an order id from the Distributor, retrieve additional order and order detail information, and finally apply an XSLTransform to format the notification in HTML format.

Creating a Project

To create a custom component using Visual Studio .NET 2005, first create a new project (File | New | Project ). Select the Class Library template for your favorite programming language (in my case, C#) and provide the name and folder location.

Figure 1 – Creating an new project in Visual Studio .NET 2005

To make the SSNS classes available to the Custom_Formatter project, add a reference to the DLL that contains the classes. Navigate Project | Add Reference, select the Microsoft.SqlServer.NotificationServices reference and click OK

All code for the custom formatter will be added to the Custom_Formatter class in the Custom_Formatter.cs file.

For brevity in subsequent coding, add the following lines to the using section of the class.

using Microsoft.SqlServer.NotificationServices;
using System.Data.SqlClient;
using System.Data;
using System.Collections.Specialized;
using System.Collections;
using System.Xml;
using System.Xml.Xsl;
using System.IO;

Implementing the content formatter interface

The SSNS Distributor component expects that each custom formatter will provide the same consistent interface for it to use. To ensure that is the case, a custom formatter must implement the IContentFormatter interface. In C#, a class implements an interface using the following notation.

public class Custom_Formatter : IContentFormatter

The IContentFormatter interface defines three methods on which the Distributor relies for processing.

  • Initialize – this method is called when the Distributor first loads the custom content formatter class.
  • FormatContent – called for each notification to be formatted. Relevant information about each notification is passed to the method as parameters.
  • Close – Once the customer formatter has completed its processing tasks, the Distributor will call the Close method to shutdown the custom component.

The next step is to stub in the public methods defined by the interface, as demonstrated in the following section of code.

namespace MyDemo
{
   public class Custom_Formatter : IContentFormatter
   {
      //this method is called once when the distributor starts
      public void Initialize(
          StringDictionary arguments
         , bool digest)
      {
         //initialize the arguments here
      }

      //the workhorse
      //this method is called once for each notification
      //or set of notifications in the case of digest delivery
      public string FormatContent(
          string subscriberLocale
         , string deviceTypeName
         , RecipientInfo recipientInfo
         , Hashtable[] rawContent)
      {

      }

      //this method is called only once,
      //when the distributor is shutdown
      public void Close()
      {
         //clean up any resources consumed during the run
      }
   }
}

The Initialize method

The Initialize method is called first by the Distributor, when the custom content formatter is to be used for a notification. It is only called once per run and it is guaranteed to be called before any other method is invoked. This method is typically used to define and set class variables required for processing notifications.

The Initialize method has two parameters, the arguments parameter and the digest parameter. The arguments parameter is a StringDictionary data type. It contains a series of argument names and values as defined in the Application Definition File. In our example, the Distributor will pass in three arguments: the XsltBaseDirectoryPath, the XsltFileName, and the ConnectionString.

  • XsltBaseDirectoryPath specifies the location for the XSLTranform base directory. The exact XSLTranform file will reside in a sub-folder.
  • XsltFileName provides the name of the XSLTransform to be used to transform the raw notification data into an HMTL formatted string that may be sent via email.
  • ConnectionString specifies how the custom content formatter will make a connection to the production database to retrieve the additional order detail information named in our requirements.

The digest parameter is used by the customer content formatter to determine whether DigestDelivery is turned on for this SSNS application. For this example, we are not going use DigestDelivery so the code will largely ignore that argument.

The following code demonstrates how the Initialize method may be developed to process these parameters.

//class variables
private bool digest;
private SqlConnection cn;
private string XsltBaseDirectoryPath = "";
private string XsltFileName = "";
private string ConnectionString = "";

//this method is called once when the distributor starts
public void Initialize(
    StringDictionary arguments
   , bool digest)
{
   //initialize the arguments here
   this.XsltBaseDirectoryPath = arguments["XsltBaseDirectoryPath"];
   this.XsltFileName = arguments["XsltFileName"];
   this.ConnectionString = arguments["ConnectionString"];

   //initialize the digest variable
   this.digest = digest;

}

The FormatContent method

The FormatContent method is called by the Distributor for each notification (or set of notifications in the case of DigestDelivery) that needs to be formatted. In our example, this method will be charged with two tasks – gathering additional order detail information from the production database and formatting the notification using an XSLTransform. Although it is a best practice to develop custom content formatters to allow for multiple languages, this example, for brevity, assumes a United States English (en-US) language only.

The FormatContent method is listed below.

//the workhorse
//this method is called once for each notification
//or set of notifications in the case of digest delivery
public string FormatContent(
    string subscriberLocale
   , string deviceTypeName
   , RecipientInfo recipientInfo
   , Hashtable[] rawContent)
{
   //declare some variables
   string body = "";
   int notificationCount = rawContent.Length;
   bool success = true;


   //get the list orders
   ArrayList orders = new ArrayList();

   foreach (Hashtable content in rawContent)
   {
      //get the order id
      string orderId = Convert.ToString(content["OrderId"]);

      //add to the orders collection
      if (!orders.Contains(orderId))
      {
         orders.Add(orderId);
      }
   }

   //create a dataset to store the orders
  
DataSet orderDataSet = new DataSet("Orders");

  
//loop through each order
   for (int cnt = 0; cnt < orders.Count; cnt++)
   {
      string orderId = Convert.ToString(orders[cnt]);

      //retrieve the order row
      try
      {
         //get the order
         //DataTable prov = getPhysicianInfo(Convert.ToInt32(providerId));
         DataTable orderDataTable = getOrderInfo(Convert.ToInt32(orderId));

         //make sure we found the order
         if (orderDataTable.Rows.Count == 0)
         {
            //if we did not find it,
            //there is a problem!
            //exit the loop
            success = false;
            break;
         }

         //if the order table exists,
         //merge the current order into the existing table
         //if not add the order table to the dataset
         if (orderDataSet.Tables.Contains("Order"))
         {
            if (orderDataSet.Tables["Order"].Rows.Contains(orderId) == false)
               orderDataSet.Tables["Order"].Merge(orderDataTable);
         }
         else
         {
            //create a new datatable in the dataset
            orderDataSet.Tables.Add(orderDataTable);

            //create a primary key
            DataColumn[] orderPk =
{ orderDataSet.Tables["Order"].Columns["OrderId"] };
            orderDataSet.Tables["Order"].PrimaryKey = orderPk;

         }
      }
      catch (Exception ex)
      {
         success = false;
      }



      //retrieve the order details
      try
      {
         //get the order details
         //DataTable pat =
getPatientInfo(patientId, Convert.ToInt32(providerId));
         DataTable detailsDataTable = getOrderDetailInfo(orderId);

         //make sure we found the details
         if (detailsDataTable.Rows.Count == 0)
         {
            //if we cannot find the details
            //so be it
            continue;
         }

         //if the details table exists,
         //merge the current details table
         if (orderDataSet.Tables.Contains("OrderDetail"))
         {
            orderDataSet.Tables["OrderDetail"].Merge(detailsDataTable);
         }
         else
         {
            //add the table to the dataset
            orderDataSet.Tables.Add(detailsDataTable);

            //relate the order details to the order
            DataRelation detailsRelation = new DataRelation(
                "DetailsRelation"
                , orderDataSet.Tables["Order"].Columns["OrderId"]
                , orderDataSet.Tables["OrderDetail"].Columns["OrderId"]);
            detailsRelation.Nested = true;
            orderDataSet.Relations.Add(detailsRelation);
         }
      }
      catch (Exception ex)
      {
         success = false;
      }

   }


   if (success)
   {
      //extract the XML from the dataset
      XmlDataDocument orderXML = new XmlDataDocument(orderDataSet);


      //call the transform
      string transformPath = @XsltBaseDirectoryPath + @"\en-US\"
+ deviceTypeName + @"\"
+ @XsltFileName;
      body = Transform(orderXML, transformPath);
   }
   else
      body = "";


   //return the formatted body
   return body;
}

Unformatted notifications are passed from the Distributor to the FormatContent method in a Hashtable array named rawContent. Since multiple notifications may be passed in the Hashtable array, our code iterates through each item of the array and adds its information to the orders collection.

Once the orders have been added to the collection, each order is processed in a foreach loop. Within the loop, two private helper methods are called to retrieve additional information from the database; the getOrderInfo method returns a single row of information about the order in a DataTable while the getOrderDetailInfo returns all detail rows for a specific order.

Each DataTable is added to (or merged with an existing DataTable in) the orderDataSet DataSet. The private helper methods are shown below.

private DataTable getOrderInfo(
      int orderId)
{
   DataTable dt = new DataTable("Order");
   SqlDataAdapter da = new SqlDataAdapter();

   //get the order from the database
   if (openConnection())
   {
      //create the objects to get the data
      SqlCommand cmd = new SqlCommand("usp_GetOrder", cn);
      cmd.CommandType = CommandType.StoredProcedure;

      //order parameter
      SqlParameter parm = new SqlParameter("@OrderId", SqlDbType.Int, 4);
      parm.Direction = ParameterDirection.Input;
      parm.Value = orderId;
      cmd.Parameters.Add(parm);

      da.SelectCommand = cmd;


      try
      {
         //fill the data table
         int ret = da.Fill(dt);
      }
      catch (Exception ex)
      {
         //log the exception
      }

   }

   return dt;
}

private DataTable getOrderDetailInfo(
      string orderId)
{
   DataTable dt = new DataTable("OrderDetail");
   SqlDataAdapter da = new SqlDataAdapter();

   //get the order from the database
   if (openConnection())
   {
      //create the objects to get the data
      SqlCommand cmd = new SqlCommand("usp_GetOrderDetail", cn);
      cmd.CommandType = CommandType.StoredProcedure;

      //order parameter
      SqlParameter parm = new SqlParameter("@OrderId", SqlDbType.Int, 4);
      parm.Direction = ParameterDirection.Input;
      parm.Value = orderId;
      cmd.Parameters.Add(parm);

      da.SelectCommand = cmd;

      try
      {
         //fill the data table
         int ret = da.Fill(dt);
      }
      catch (Exception ex)
      {
         //log the exception
      }

   }

   return dt;
}


private bool openConnection()
{
   bool ret = true;

   //is the connection already open?
   if (cn.State == ConnectionState.Open)
      return ret;

   //get the connection string
   cn.ConnectionString = this.ConnectionString;

   //open the connection and return
   try
   {
      cn.Open();
   }
   catch (Exception ex)
   {
      ret = false;
      //log the exception
   }

   return ret;

}

Once all orders have been processed, the orderDataSet DataSet contains two DataTables, Order and OrderDetail.

Now that the order information has been collected from the production database, the notifications may be formatted using an XSLTransform. To do this, the code first creates an XmlDataDocument object from the orderDataSet. The XmlDataDocument, called orderXML, is passed as a parameter to another private helper method, named Transform. The Transform method applies the specified XSLTranform to the XmlDataDocument, resulting in a properly formatted notification in the body variable.

The Transform method is shown below.

public string Transform(XmlDataDocument orderXML, string xsltPath)
{
   string returnString = "";

   try
   {
      //create the transform object
      XslCompiledTransform transform = new XslCompiledTransform();

      //load the Xsltransform
      transform.Load(xsltPath);

      //create the output string
      StringWriter outputString = new StringWriter();


      //do the actual transform of Xml
      transform.Transform(orderXML, null, outputString);

      //capture the body
      returnString = outputString.ToString();

      //clean up
      outputString.Close();

   }
   catch (Exception ex)
   {
      //log exception
   }

   return returnString;

}

The FormatContent method returns to the Distributor a properly-formatted notification.

The Close method

Like the Initialize method, the Close method is called only one per run. The Close method is called after all notifications have been processed and the Distributor is closing down the custom content formatter. The Close method is the appropriate place to release any resources consumed during processing.

//this method is called only once,
//when the distributor is shutdown
public void Close()
{
   //clean up any resources consumed during the run
   if (cn.State == ConnectionState.Open)
      cn.Close();

}

After developing the three methods required by the IContentFormatter interface, and their supporting private helper methods, the project can be built and the used in the SSNS 2005 instance.

Configuring the SSNS instance to use a custom content formatter

Once the custom content formatter has been built, a SSNS 2005 application can be configured to use the custom component to format notifications before handing them off to a delivery service. As with all application-related items, the custom content formatter is declared in the Application Definition File (ADF).

Modifying the Application Definition File

Each notification class of an SSNS 2005 application contains a ContentFormatter node that defines the means by which notifications of the class are to be formatted.

Two elements are required for a custom content formatter, the ClassName and the AssemblyName. The ClassName element specifies the fully qualified name of the custom content formatter, including its namespace. The AssemblyName element defines the assembly in which the custom component may be found.

Additional arguments may be specified in the Arguments node. Three arguments are required in this example, XsltBaseDirectoryPath, XsltFileName, and ConnectionString, which correspond to the arguments found in the arguments parameter of the Initialize method.

The relevant section of the ADF is listed below.

<!-- custom content formatter -->
<ContentFormatter>
    <ClassName>MyDemo.Custom_Formatter</ClassName>
   <AssemblyName>E:\SSNS\Demo\bin\Custom_Formatter.dll</AssemblyName>
   <Arguments>
      <Argument>
         <Name>XsltBaseDirectoryPath</Name>
         <Value>E:\SSNS\Demo\bin\xslt</Value>
      </Argument>
      <Argument>
         <Name>XsltFileName</Name>
         <Value>MyTransform.xslt</Value>
      </Argument>
      <Argument>
         <Name>ConnectionString</Name>
         <Value>Data Source=(local);Initial Catalog=AdventureWorks;
Integrated Security=True;
Pooling=True
</Value>
      </Argument>
   </Arguments>
</ContentFormatter>

Once the ADF has been modified to use the custom content formatter, the SSNS instance should be updated using SQL Server Management Studio to apply the changes.

Conclusion

Although the built-in components provided with SSNS 2005 offer a healthy amount of capabilities, the SSNS architecture allows custom components to be created to satisfy the needs of most any requirement.



This article has been viewed 2900 times.
Joe Webb

Author profile: Joe Webb

Joe Webb, a Microsoft SQL Server MVP, serves as Chief Operating Manager for WebbTech Solutions, a Nashville-based IT consulting company. He has over 13 years of industry experience and has consulted extensively with companies in the areas of software development, database design, and technical training. He can be reached at joew@webbtechsolutions.com

Search for other articles by Joe Webb

Rate this article:   Avg rating: from a total of 2 votes.


Poor

OK

Good

Great

Must read
 
Have Your Say
Do you have an opinion on this article? Then add your comment below:
You must be logged in to post to this forum

Click here to log in.
 









Phil Factor
Bunnikins!
 When an IT manager is selected as a victim of office politics of a large corporate, it is time for him to engage in... Read more...



 View the blog
SQL Toolbelt 2008: Predominantly an Engineering Task
 The conversion of the Red-Gate tools to be compatible with SQL Server 2008 might not seem, on first... Read more...

Audit Crosschecks
 In this short article, the second of a 2-part series, William suggests a solution, using SQL Data... Read more...

Discovering Security Uses for SQL Compare
 Much of the security of SQL Server is implemented as part of the database schema. This provides some... Read more...

XML Jumpstart Workbench
 In which Robyn and Phil decide that the best way of starting to learn XML is to jump in and take a ride... Read more...

RSS Newsfeed Workbench
 Robyn and Phil decide to build an RSS newsfeed in TSQL, using the power of SQL Server's XML.  Read more...

Beginning SQL Server 2005 Reporting Services Part 1
 Steve Joubert begins an in-depth tour of SQL Server 2005 Reporting Services with a step-by-step guide... Read more...

Ten Common Database Design Mistakes
 Database design and implementation is the cornerstone of any data centric project (read 99.9% of... Read more...

SQL Server Full Text Search Language Features
 SQL Full-text Search (SQL FTS) is an optional component of SQL Server 7 and later, which allows fast... Read more...

Beginning SQL Server 2005 Reporting Services Part 2
 Continuing his in-depth tour of SQL Server 2005 Reporting Services, Steve Joubert demonstrates the most... Read more...

Executing SSIS Packages
 Nigel Rivett demonstrates how to execute all SSIS packages in a given folder using either an SSIS... Read more...

Over 150,000 Microsoft professionals subscribe to the Simple-Talk technical journal. Join today, it's fast, simple, free and secure.

Join Simple Talk