Building a Custom Content Formatter


As it does for event providers, SQL-NS offers a plug-in model for content formatters. If the built-in XsltFormatter doesn't meet your needs, you can easily build a custom content formatter to replace it.

This section looks at some of the reasons for building a custom content formatter and then describes the technical details of content formatter development. To illustrate the concepts with a practical example, we'll build a custom content formatter for the music store application.

Why Build a Custom Content Formatter?

Although the XsltFormatter does present a simple, convenient way to format notifications without writing custom code, the use of XSL transforms has some inherent limitations:

  • Beyond a basic level of complexity, XSL transforms become difficult to read, maintain, and debug.

  • XSL transforms do not provide an elegant way to do computations or iterative processing on the input data.

  • Accessing external data from an XSL transform is not straightforward.

  • It can be difficult to manage and maintain all the separate transform files required to do locale- and device-specific formatting.

If you build a custom content formatter, these limitations fall away.

Building a custom content formatter involves writing a class in managed code. With the expressiveness of the .NET languages and the full power of the .NET Framework at your disposal, implementing complex formatting in custom content formatter code tends to be easier than writing the equivalent XSL transforms.

In custom content formatter code, you have a great deal of flexibility in the way you can work with the input data. Using familiar programming constructs, you can perform computations such as calculating averages or totals, conditional processing, and iterative processing. Although it is possible to do all these things in XSL transforms, doing them in a procedural programming language is usually more intuitive for most developers.

One thing that almost always necessitates building a custom content formatter is the need for access to external data in the formatting process. In many SQL-NS applications, the notification data contains references to other data, stored externally, that the formatter needs to access to produce the final formatted notifications. For example, the notification data might contain a customer ID that refers to a customer record in an external database. When formatting such a notification, the formatter may need to access the customer record to obtain the customer's name. It's difficult to access external data in an XSL transform, but if you build a custom content formatter, you can use any of the data access APIs in the .NET Framework to do this.

In a custom content formatter, you are free to implement locale- and device-specific formatting in any way you choose. When the distributor invokes your formatter code, it passes you the locale and device type for which the notification should be formatted. You can use this information to make decisions about the final format of the string that your formatter returns. In a custom content formatter, the formatting code for each locale and device type is typically defined in the same source file. This makes it much easier to maintain than the separate transforms used by the XsltFormatter.

The Content Formatter Interface

To create a custom content formatter, you write a class that implements the IContentFormatter interface. This interface defines methods that the distributor uses to interact with content formatters. Listing 9.9 shows the definition of IContentFormatter.

Listing 9.9. The IContentFormatter Interface

 public interface IContentFormatter {     void Initialize(StringDictionary arguments, bool digest);     string FormatContent(         string            subscriberLocale,         string            deviceTypeName,         RecipientInfo     recipientInfo,         Hashtable[]       rawContent);     void Close(); } 

Note

IContentFormatter is in the SQL-NS namespace (Microsoft.SqlServer.NotificationServices) and is defined in the SQL-NS assembly. Any project that implements IContentFormatter must reference the SQL-NS assembly.


In your formatter class, you implement each of the IContentFormatter methods for your chosen formatting scheme. You then compile the formatter class into an assembly. To use the custom content formatter in a SQL-NS application, you specify the name of the formatter class and its containing assembly in a <ContentFormatter> element in the ADF. On startup, the distributor loads the assembly and creates an instance of the class.

At the start of processing, the distributor calls the content formatter's Initialize() method. The distributor passes Initialize() a dictionary of content formatter arguments, as specified in the content formatter declaration in the ADF. It also passes a Boolean flag indicating whether digesting is turned on.

For each notification (or set of notifications, in the case of digesting) that the distributor needs to format, it calls the FormatContent() method on the custom content formatter class. In the call to FormatContent(), the distributor passes the subscriber locale, device type, information about the recipient, and the raw notification data. The recipient information is an object of the RecipientInfo class (defined in the SQL-NS API) that has properties that return the subscriber ID and device address. The raw notification data is passed as an array of hash tables. There is one hash table in the array for each row of notification data that the formatter needs to process. Each hash table contains the values of the notification and computed fields in one row of notification data. The FormatContent() method returns a string that represents the formatted notification body. The distributor passes the string returned from FormatContent() to the delivery protocol for delivery.

Tip

The Hashtable class, used in the FormatContent() method signature, is defined in the System.Collections namespace. If you are writing your own class that implements IContentFormatter, you must reference this namespace in a using declaration.


The distributor calls the Close() method at the end of processing. In the Close() method, the content formatter should release any resources it is holding and prepare to shut down.

The distributor's contract with the content formatter guarantees that Initialize() will be called first, followed by a series of calls to FormatContent() (one for each notification or group of notifications that need to be formatted), followed by a call to Close(). FormatContent() will not be called after Close() unless Initialize() is first called again.

Implementing the Content Formatter Interface

To implement IContentFormatter, you write a class that provides implementations for the Initialize(), FormatContent(), and Close() methods. In this section, we implement a custom content formatter for the music store application's NewSong notifications. This formatter produces messages similar to the ones produced with XSL transforms in the previous section, but performs some additional processing to streamline the notification content.

Use the following instructions to open the Visual Studio solution containing the content formatter implementation:

1.

Navigate to the C:\SQL-NS\Samples\MusicStore\SongAlerts\CustomComponents\NewSongNotificationFormatter directory.

2.

Open the solution file NewSongNotificationFormatter.sln in Visual Studio.

The code for the content formatter class is in the NewSongNotificationFormatter.cs file. Listing 9.10 shows the implementation of the IContentFormatter methods in the content formatter class.

Listing 9.10. The NewSongNotificationFormatter Class

 namespace SongAlertsCustomComponents {     class NewSongNotificationFormatter : IContentFormatter     {         private bool digest;         public void Initialize(StringDictionary arguments, bool digest)         {             this.digest = digest;         }         public string FormatContent(             string          subscriberLocale,             string          deviceTypeName,             RecipientInfo   recipientInfo,             Hashtable[]     rawContent)         {             string body;             switch(subscriberLocale)             {                 case "en-US":                     body = FormatUSEnglish(deviceTypeName, rawContent);                     break;                 case "fr-FR":                     body = FormatFrench(deviceTypeName, rawContent);                     break;                 default:                     body = FormatUSEnglish(deviceTypeName, rawContent);                     break;             }             return body;         }         public void Close()         {             // Do nothing.         }         ... helper methods...       } } 

The purpose of the Initialize() method is to set up the content formatter for the processing it will need to do later. In the Initialize() method, the formatter typically processes the arguments it is given (the argument names and values are taken from the content formatter declaration in the ADF) and stores their values in private member variables. Our content formatter does not take any arguments, so the code in Listing 9.10 ignores the arguments parameter.

The digest parameter to Initialize() indicates whether digesting is turned on. At first, the need for this parameter may not be apparent. After all, the formatter can always tell whether to perform digesting by the number of notifications that it gets passed in each call to the FormatContent() method. To understand the need for the digest parameter, imagine a case in which FormatContent() is called with just one notification. This could happen for one of two reasons: Either digesting is turned off, or, even though digesting is turned on, the particular notification passed to FormatContent() is a singleton (there are no other notifications in the batch that satisfy the digesting criteria to be grouped with this notification). The digest parameter allows the content formatter to distinguish between these two situations. The distinction is important for formatters that always apply special formatting when digesting is turned on, even if the digest group contains just a single notification.

Note

Our formatter saves the value of the digest parameter in a private member variable. As we look at the formatting code later, you'll notice that this member variable isn't actually used. I've included the code that initializes its value in Initialize() simply to illustrate a pattern that some formatters follow.


Whatever initialization is done in the Initialize() method, the Close() method should do the corresponding shutdown operations. For example, if Initialize() opens a database connection, Close() should close that connection. Because we have not created any resources in Initialize() that require explicit cleanup, and because our FormatContent() implementation is stateless, the Close() method does nothing.

The FormatContent() method implements the actual formatting operations. The code in Listing 9.10 uses the value of the subscriber locale to select a helper method to which it delegates the formatting task. Our formatter explicitly supports U.S. English and French formatting, so it has helper methods for these locales. The U.S. English helper method is used as a default in the case that the subscriber locale is not either en-US or fr-FR.

Listing 9.11 shows some of the helper methods in the content formatter class that are used to format U.S. English notifications. The source file also contains the equivalent helper methods for French notifications, although I have not shown them here. The French methods have the same structure as the U.S. English ones.

Listing 9.11. Helper Methods in the Content Formatter Class

[View full width]

     class NewSongNotificationFormatter : IContentFormatter     {         ... IContentFormatter methods ...         private string FormatUSEnglish(             string      deviceTypeName,             Hashtable[] rawContent)     {             string body;             switch (deviceTypeName)             {                 case "Email":                     body = FormatUSEnglishEmail(                         deviceTypeName,                         rawContent);                     break;                 case "TextMessageDevice":                     body = FormatUSEnglishTextMessageDevice(                         deviceTypeName,                         rawContent);                     break;                 default:                     body = FormatUSEnglishEmail(                         deviceTypeName,                         rawContent);                     break;             }             return body;           }           private string FormatUSEnglishEmail(               string      deviceTypeName,               Hashtable[] rawContent)           {               StringBuilder body = new StringBuilder();               int notificationCount = rawContent.Length;               // Display the appropriate heading message,               // depending on the number of notifications.               if (notificationCount == 1)               {                   body.Append("There is a new song available for download from the music  store!\n");          }          else          {             body.AppendFormat(                 "There are {0} new songs available for download from the music store!\n",                  notificationCount);          }          // Extract the albums, artist names, and genres          // from the notification data into parallel array lists.          ArrayList albums = new ArrayList();          ArrayList artists = new ArrayList();          ArrayList genres = new ArrayList();          foreach (Hashtable content in rawContent)          {              string album = (string) content["AlbumTitle"];              string artist = (string) content["ArtistName"];              string genre = (string) content["Genre"];              if (!albums.Contains(album))              {                  albums.Add(album);                  artists.Add(artist);                  genres.Add(genre);              }          }          // For each album, display the album name and artist name,          // then show the list of songs.          for (int i = 0; i < albums.Count; i++)          {               string album = (string) albums[i];               string artist = (string) artists[i];               string genre = (string) genres[i];               body.Append("\n");               body.AppendFormat("Album Title: {0}\n", album);               body.AppendFormat("Artist Name: {0}\n", artist);               body.AppendFormat("Genre: {0}\n", genre);               body.Append("Songs:\n");               foreach (Hashtable content in rawContent)               {                   if ((string) content["AlbumTitle"] == album)                   {                       body.AppendFormat(                           "\tSong Title: {0}\n",                          content["SongTitle"]);                   }               }          }          return body.ToString();       }       private string FormatUSEnglishTextMessageDevice(           string      deviceTypeName,           Hashtable[] rawContent)       {           StringBuilder body = new StringBuilder();           int notificationCount = rawContent.Length;           // Display the appropriate heading message,           // depending on the number of notifications.           if (notificationCount == 1)           {               body.Append("New song available:\n");           }           else           {               body.AppendFormat(                   "{0} new songs available, including:\n",                   notificationCount);           }           // Display the data for one song.           body.Append("New songs:\n");           body.AppendFormat("Song Title: {0}\n",               rawContent[0]["SongTitle"]);           body.AppendFormat("Artist Name: {0}\n",               rawContent[0]["ArtistName"]);           body.AppendFormat({"Album Title: {0}\n",               rawContent[0]["AlbumTitle"]);           body.AppendFormat("Genre: {0}\n",               rawContent[0]["Genre"]);           return body.ToString();       }       ... more helper methods ...    } } 

The first helper method, FormatUSEnglish(), is the one called from FormatContent() when the subscriber locale is en-US, or anything other than fr-FR. This method looks at the device type (passed down from FormatContent()) and calls either FormatUSEnglishEmail() (to format a U.S. English notification for an Email device) or FormatUSEnglishTextMessageDevice() (to format a U.S. English notification for a TextMessageDevice device). Recall that device type strings can be any value you choose, as long as the content formatter understands them. Here the content formatter code is written to expect the strings we used when setting up subscriber devices.

Let's examine the FormatUSEnglishEmail() method (code shown in Listing 9.11). This method produces a formatted notification suitable for delivery to Email devices. The code begins by creating an object of the StringBuilder class (a .NET Framework utility class) that it uses to construct the formatted notification string incrementally.

By looking at the number of elements in the rawContent array of hash tables (each hash table contains the data from a single notification row), FormatUSEnglishEmail() determines the number of notifications it will need to format. Based on the number of notifications, it appends an appropriate heading message to the string builder. This results in a heading message that indicates the number of songs available for download. This is something that would have been more cumbersome to implement in an XSL transform.

After appending the heading message, the code performs some analysis on the input notification data to look for songs that have common album titles, artist names, and genres. In many cases, the notifications in a digest group represent different songs in a single album, by a single artist. All these songs would have the same genre. In the messages produced by the XSL transforms we used with the XsltFormatter, the album title, artist name, and genre were repeated for each song. Our custom content formatter attempts to streamline the notification messages by writing the album title, artist name, and genre once and then listing the song titles to which they apply beneath them.

This is achieved by constructing three parallel arrays from the notification data. The first array contains all the unique album titles in the set of notification data, and the second and third arrays contain the corresponding artist names and genres for those albums. Notice how the code refers to the notification data in the hash tables by using the index operator ([]) with the notification field names. After it has constructed the arrays, the code loops through the album's array and appends the information for each album to the string builder. It writes the album title, artist name, and genre, and then lists the songs individually. The song list is obtained by looping through the notification hash tables and appending the data from those in which the album title matches the current album title. In this way, all the essential notification data is eventually displayed without the unnecessary repetition. Again, the kind of analysis required to achieve this would have been difficult to do in an XSL transform but is fairly straightforward when writing C# code.

The FormatUSEnglishTextMessageDevice() method produces a smaller notification message, suitable for display on devices such as cell phones or pagers. Like the previous method, it uses a string builder to construct the formatted notification. This method produces a heading that indicates the number of notifications available and then displays the content of the first notification only.

After taking a look at the code, build the content formatter project in Visual Studio. This produces the content formatter assembly in the bin\Debug subdirectory of the NewSongNotificationFormatter project directory.

Declaring a Custom Content Formatter in the ADF

To use the custom content formatter in the music store application, we have to declare it in the ADF. Because each notification class can declare only one content formatter, the declaration of the custom content formatter replaces the XsltFormatter declaration in the NewSong notification class. Listing 9.12 shows the custom content formatter declaration.

Listing 9.12. Declaration of the Custom Content Formatter in the ADF

[View full width]

 <Application>   ...   <NotificationClasses>     <NotificationClass>       <NotificationClassName>NewSong</NotificationClassName>       ...       <ContentFormatter>         <ClassName>SongAlertsCustomComponents.NewSongNotificationFormatter</ClassName>         <AssemblyName>%_ApplicationBaseDirectoryPath_%\CustomComponents \NewSongNotificationFormatter\bin\Debug\NewSongNotificationFormatter.dll</AssemblyName>       </ContentFormatter>       ...     </NotificationClass>   </NotificationClasses>   ... </Application> 

Like the previous declaration of the XsltFormatter, the custom content formatter is declared in a <ContentFormatter> element. The class name specifies the name of the content formatter class that we just implemented. (Note that the class name is always given with the namespace prefix.) The <AssemblyName> element specifies the assembly in which the content formatter class is implemented. The <AssemblyName> element was not required for the built-in XsltFormatter, but it is required for the custom content formatter. The value specified points to the assembly produced by building the content formatter project in Visual Studio. Because our content formatter does not take any arguments, the <Arguments> element is omitted. If you build a custom content formatter that does require arguments, you can specify them in your content formatter declaration, using the same syntax used for the XsltFormatter arguments.

Testing the Custom Content Formatter

To test the custom content formatter, we'll feed some events into the application designed to match the subscriptions we added earlier. As in the previous test we did, these events will produce notifications for both supported languages and device types.

To see the custom content formatter in use, perform the following instructions:

1.

Make sure that you've built the custom content formatter project in Visual Studio.

2.

Add the custom content formatter declaration shown in Listing 9.12 to your ADF. You can find this code in the supplemental ADF, ApplicationDefinition-13.xml, in C:\SQL-NS\Chapters\09\SupplementaryFiles.

3.

Update your instance using the same steps you used in the previous chapter. (For reference, see steps 27 in the "Testing the FileSystemWatcherProvider in the Music Store Application" section, p. 251, in Chapter 8.) Because the instance uses argument encryption, you must use update_with_argument_key.cmd, not update.cmd, to invoke nscontrol update.

4.

Start the AddSongs program.

5.

Check the Submit Events for Songs Added box.

6.

Enter the following song data (make sure that you specify the artist name exactly as it appears here; otherwise, the events won't match the subscriptions):

Album Title: All for You
Artist Name: Diana Krall
Genre: Jazz
Song 1: I'm an Errand Girl for Rhythm
Song 2: Gee Baby, Ain't I Good to You

7.

Click the Add to Database button.

8.

When the form clears, enter the following song data (make sure that you specify the artist name exactly as it appears here; otherwise, the events won't match the subscriptions):

Album Title: The Essential Miles Davis
Artist Name: Miles Davis
Genre: Jazz
Song 1: Summertime
Song 2: Time After Time

9.

Wait about 30 seconds and then check the notifications output file, C:\SQL-NS\Samples\MusicStore\FileNotifications.txt.

Unless you deleted the file after the previous test run, the output file will still contain the notifications produced by the XsltFormatter. After these notifications, you should see the new notifications produced by the custom content formatter. Notice how much cleaner the notifications look as a result of the streamlining that the custom content formatter performed.

Cleanup: Preparing for The Next Chapter

In preparation for the work in the next chapter, you should clean up the SQL-NS instance and supporting files created in this chapter. In the next chapter, we will re-create the instance before continuing to develop it. Complete the following instructions to clean up the instance:

1.

From a Notification Services Command Prompt on your development machine, navigate to the music store sample's scripts directory by typing the following command:

 cd /d C:\SQL-NS\Samples\MusicStore\Scripts 


2.

Run cleanup.cmd to remove the music store instance and all associated databases.





Microsoft SQL Server 2005 Notification Services
Microsoft SQL Server 2005 Notification Services
ISBN: 0672327791
EAN: 2147483647
Year: 2006
Pages: 166
Authors: Shyam Pather

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net