JavaScript EditorFreeware javascript editor     Javascript code


Main Page

Previous Page
Next Page

Solution

The "Solution" section of this chapter is thinner than those found in most of the other chapters. In fact, in this chapter you've been presented with some new controls and features that won't be part of a common custom framework, but will be used in most of the upcoming chapters as parts of other classes to be developed. Other features, such as exception handling and logging, and the transaction management, are already built in and are so easy to use that they don't need to be encapsulated within custom business classes. The discussion about the DAL and BLL design will be extremely useful for the next chapters, because they follow the design outlined here. Understand that ASP.NET 2.0 already has a number of built-in common services to handle many of your general framework-level needs, which allows you to focus more of your time and efforts on your specific business problems. The rest of the chapter shows the code for the small base classes for the DAL and the BLL, the custom configuration section, and the code for raising and handling web events.

TheBeerHouse Configuration Section

Following is the code (located in the /App_Code/ConfigSection.cs file) for the classes that map the <theBeerHouse> custom configuration section, and the inner <contactForm> element, whose meaning and properties were already described earlier:

namespace MB.TheBeerHouse
{
   public class TheBeerHouseSection : ConfigurationSection
   {
      [ConfigurationProperty("defaultConnectionStringName",
         DefaultValue = "LocalSqlServer")]
      public string DefaultConnectionStringName
      {
         get { return (string)base["defaultConnectionStringName"]; }
         set { base["connectionStdefaultConnectionStringNameringName"] = value; }
      }

      [ConfigurationProperty("defaultCacheDuration", DefaultValue = "600")]
      public int DefaultCacheDuration
      {
         get { return (int)base["defaultCacheDuration"]; }
         set { base["defaultCacheDuration"] = value; }
      }

      [ConfigurationProperty("contactForm", IsRequired=true)]
      public ContactFormElement ContactForm
      {

         get { return (ContactFormElement) base["contactForm"]; }
      }

   }

   public class ContactFormElement : ConfigurationElement
   {
      [ConfigurationProperty("mailSubject",
         DefaultValue="Mail from TheBeerHouse: {0}")]
      public string MailSubject
      {
         get { return (string)base["mailSubject"]; }
         set { base["mailSubject"] = value; }
      }

      [ConfigurationProperty("mailTo", IsRequired=true)]
      public string MailTo
      {
         get { return (string)base["mailTo"]; }
         set { base["mailTo"] = value; }
      }

      [ConfigurationProperty("mailCC")]
      public string MailCC
      {
         get { return (string)base["mailCC"]; }
         set { base["mailCC"] = value; }
      }
   }
}

The TheBeerHouseSection class must be mapped to the <theBeerHouse> section through a new element under the web.config file's <configSections> section. Once you've defined the mapping you can write the custom settings, as follows:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <configSections>
      <section name="theBeerHouse"
         type="MB.TheBeerHouse.TheBeerHouseSection, __code"/>
   </configSections>

   <theBeerHouse defaultConnectionStringName="LocalSqlServer">
      <contactForm mailTo="thebeerhouse@wrox.com"/>
   </theBeerHouse>
   <!-- other configuration sections... -->
</configuration>

To make the settings easily readable from any part of the site, we will add a public field of type TheBeerHouseSection in the Globals class that was added to the project in the previous chapter, and set it as follows:

namespace MB.TheBeerHouse
{
   public static class Globals
   {

      public readonly static TheBeerHouseSection Settings =
         (TheBeerHouseSection)WebConfigurationManager.GetSection("theBeerHouse");

      public static string ThemesSelectorID = "";
   }
}

To see how these settings are actually used, let's create the Contact.aspx page, which enables users to send mail to the site administrator by filling in a form online. Figure 3-22 is a screenshot of the page at runtime.

Image from book
Figure 3-22

The following code is the markup for the page, with the layout structure removed to make it easier to follow:

Your name: <asp:TextBox runat="server" ID="txtName" Width="100%" />
<asp:RequiredFieldValidator runat="server" Display="dynamic" ID="valRequireName"
   SetFocusOnError="true" ControlToValidate="txtName"
   ErrorMessage="Your name is required">*</asp:RequiredFieldValidator>

Your e-mail: <asp:TextBox runat="server" ID="txtEmail" Width="100%" />

<asp:RequiredFieldValidator runat="server" Display="dynamic" ID="valRequireEmail"
   SetFocusOnError="true" ControlToValidate="txtEmail"
   ErrorMessage="Your e-mail address is required">*</asp:RequiredFieldValidator>
<asp:RegularExpressionValidator runat="server" Display="dynamic"
   ID="valEmailPattern"  SetFocusOnError="true" ControlToValidate="txtEmail"
   ValidationExpression="\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*"
   ErrorMessage="The e-mail address you specified is not well-formed">*
</asp:RegularExpressionValidator>

Subject: <asp:TextBox runat="server" ID="txtSubject" Width="100%" />
<asp:RequiredFieldValidator runat="server" Display="dynamic" ID="valRequireSubject"
   SetFocusOnError="true" ControlToValidate="txtSubject"
   ErrorMessage="The subject is required">*</asp:RequiredFieldValidator>

Body: <asp:TextBox runat="server" ID="txtBody" Width="100%"
   TextMode="MultiLine" Rows="8" />
<asp:RequiredFieldValidator runat="server" Display="dynamic" ID="valRequireBody"
   SetFocusOnError="true" ControlToValidate="txtBody"
   ErrorMessage="The body is required">*</asp:RequiredFieldValidator>

<asp:Label runat="server" ID="lblFeedbackOK" Visible="false"
   Text="Your message has been successfully sent." SkinID="FeedbackOK" />
<asp:Label runat="server" ID="lblFeedbackKO" Visible="false"
   Text="Sorry, there was a problem sending your message." SkinID="FeedbackKO" />

<asp:Button runat="server" ID="txtSubmit" Text="Send" OnClick="txtSubmit_Click" />
<asp:ValidationSummary runat="server" ID="valSummary"
   ShowSummary="false" ShowMessageBox="true" />

When the Send button is clicked, a new System.Net.Mail.MailMessage is created, with its To, CC and Subject properties set from the values read from the site's configuration; the From and Body are set with the user input values, and then the mail is sent:

protected void txtSubmit_Click(object sender, EventArgs e)
{
   try
   {
      // send the mail
      MailMessage msg = new MailMessage();
      msg.IsBodyHtml = false;
      msg.From = new MailAddress(txtEmail.Text, txtName.Text);
      msg.To.Add(new MailAddress(Globals.Settings.ContactForm.MailTo));
      if (!string.IsNullOrEmpty(Globals.Settings.ContactForm.MailCC))
         msg.CC.Add(new MailAddress(Globals.Settings.ContactForm.MailCC));
      msg.Subject = string.Format(
         Globals.Settings.ContactForm.MailSubject, txtSubject.Text);
      msg.Body = txtBody.Text;
      new SmtpClient().Send(msg);
      // show a confirmation message, and reset the fields
      lblFeedbackOK.Visible = true;
      lblFeedbackKO.Visible = false;
      txtName.Text = "";
      txtEmail.Text = "";
      txtSubject.Text = "";

      txtBody.Text = "";
   }
   catch (Exception)
   {
      lblFeedbackOK.Visible = false;
      lblFeedbackKO.Visible = true;
   }
}

The SMTP settings used to send the message must be defined in the web.config file, in the <mailSettings> section, as shown here:

<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
   <system.web> <!-- some settings here...--> </system.web>
   <system.net>
      <mailSettings>
         <smtp deliveryMethod="Network "from="thebeerhouse@wrox.com">
            <network defaultCredentials="true" host="(localhost)" port="25" />
         </smtp>
      </mailSettings>
   </system.net> </configuration>

The DataAccess Base DAL Class

The DataAccess class (located in /App_Code/DAL/DataAccess.cs) contains just a few properties, such as ConnectionString,EnableCaching,CachingDuration and Cache, and the ExecuteReader, ExecuteScalar and ExecuteNonQuery wrapper methods discussed in this chapter. Note that, except for the Cache property that just returns a reference to the current context's Cache object, the properties are not set directly in the class itself because they may have different values for different DAL classes. I don't plan to use the cache from the DAL because, as I said earlier, I prefer to implement caching in the BLL so it works regardless of which DAL provider is being used. However, I've put the caching-related properties in the DAL's base class also, in case there may be a need for some very provider-specific caching someday. Here's the complete code:

namespace MB.TheBeerHouse.DAL
{
   public abstract class DataAccess
   {
      private string _connectionString = "";
      protected string ConnectionString
      {
         get { return _connectionString; }
         set { _connectionString = value; }
      }

      private bool _enableCaching = true;
      protected bool EnableCaching
      {
         get { return _enableCaching; }
         set { _enableCaching = value; }
      }

      private int _cacheDuration = 0;

      protected int CacheDuration
      {
         get { return _cacheDuration; }
         set { _cacheDuration = value; }
      }

      protected Cache Cache
      {
         get { return HttpContext.Current.Cache; }
      }

      protected int ExecuteNonQuery(DbCommand cmd)
      {
         if (HttpContext.Current.User.Identity.Name.ToLower() == "sampleeditor")
         {
            foreach (DbParameter param in cmd.Parameters)
            {
               if (param.Direction == ParameterDirection.Output ||
                  param.Direction == ParameterDirection.ReturnValue)
               {
                  switch (param.DbType)
                  {
                     case DbType.AnsiString:
                     case DbType.AnsiStringFixedLength:
                     case DbType.String:
                     case DbType.StringFixedLength:
                     case DbType.Xml:
                        param.Value = "";
                        break;
                     case DbType.Boolean:
                        param.Value = false;
                        break;
                     case DbType.Byte:
                        param.Value = byte.MinValue;
                        break;
                     case DbType.Date:
                     case DbType.DateTime:
                        param.Value = DateTime.MinValue;
                        break;
                     case DbType.Currency:
                     case DbType.Decimal:
                        param.Value = decimal.MinValue;
                        break;
                     case DbType.Guid:
                        param.Value = Guid.Empty;
                        break;
                     case DbType.Double:
                     case DbType.Int16:
                     case DbType.Int32:
                     case DbType.Int64:
                        param.Value = 0;
                        break;
                     default:
                        param.Value = null;
                        break;
                  }

               }
            }
            return 1;
         }
         else
            return cmd.ExecuteNonQuery();
      }

      protected IDataReader ExecuteReader(DbCommand cmd)
      {
         return ExecuteReader(cmd, CommandBehavior.Default);
      }

      protected IDataReader ExecuteReader(DbCommand cmd, CommandBehavior behavior)
      {
         return cmd.ExecuteReader(behavior);
      }

      protected object ExecuteScalar(DbCommand cmd)
      {
         return cmd.ExecuteScalar();
      }
   }
}

The only unusual aspect of this code is the ExecuteNonQuery method, which actually calls the ExecuteNonQuery method of the command object passed to it as an input, but only if the current context's user name is not "sampleeditor". If the user's name is "sampleeditor", then none of their inserts/updates/deletes will actually be processed; the method always returns 1, and sets all output parameters to a default value, according to their type. SampleEditor is a special account name for test purposes — this user can go into almost all protected areas, but cannot actually persist changes to the database, which is just what we want for demo purposes, as discussed earlier in this chapter.

The BizObject BLL Base Class

This class (found in /App_Data/BLL/BizObject.cs) defines a number of properties that can be useful for many of the module-specific BLL classes. For example, it returns the name and IP of the current user, the current context's Cache reference, and more. It also contains some methods to encode HTML strings, convert a null string to an empty string, and purge items from the cache:

namespace MB.TheBeerHouse.BLL
{
   public abstract class BizObject
   {
      protected const int MAXROWS = int.MaxValue;

      protected static Cache Cache
      {
         get { return HttpContext.Current.Cache; }
      }

      protected static IPrincipal CurrentUser
      {
         get { return HttpContext.Current.User; }

      }

      protected static string CurrentUserName
      {
         get
         {
            string userName = "";
            if (HttpContext.Current.User.Identity.IsAuthenticated)
               userName = HttpContext.Current.User.Identity.Name;
            return userName;
         }
      }

      protected static string CurrentUserIP
      {
         get { return HttpContext.Current.Request.UserHostAddress; }
      }

      protected static int GetPageIndex(int startRowIndex, int maximumRows)
      {
         if (maximumRows <= 0)
            return 0;
         else
            return (int)Math.Floor((double)startRowIndex / (double)maximumRows);
      }

      protected static string EncodeText(string content)
      {
         content = HttpUtility.HtmlEncode(content);
         content = content.Replace("  ", "&nbsp;&nbsp;").Replace("\n", "<br>");
         return content;
      }

      protected static string ConvertNullToEmptyString(string input)
      {
         return (input == null ? "" : input);
      }

      protected static void PurgeCacheItems(string prefix)
      {
         prefix = prefix.ToLower();
         List<string> itemsToRemove = new List<string>();

         IDictionaryEnumerator enumerator = BizObject.Cache.GetEnumerator();
         while (enumerator.MoveNext())
         {
            if (enumerator.Key.ToString().ToLower().StartsWith(prefix))
               itemsToRemove.Add(enumerator.Key.ToString());
         }

         foreach (string itemToRemove in itemsToRemove)
            BizObject.Cache.Remove(itemToRemove);
      }
   }
}

The most interesting method is PurgeCacheItems, which takes as input a string, and cycles through the cached items and collects a list of all items whose key starts with the input prefix, and finally deletes all those items.

Configuring the Health Monitoring System

We need a custom event class to log when a record of any type is deleted by an administrator or an editor, through the site's administrative area. The RecordDeletedEvent defined here could have inherited directly from the framework's WebBaseEvent base class, but instead, I've created a custom WebCustom Event class that inherits from WebBaseEvent and adds nothing to it; then I created the RecordDeleted Event, making it inherit from WebCustomEvent, and with a new constructor that takes the name of the entity deleted (e.g., a category, a product, an article, etc.) and its ID. Here's the code:

namespace MB.TheBeerHouse
{
   public abstract class WebCustomEvent : WebBaseEvent
   {
      public WebCustomEvent(string message, object eventSource, int eventCode)
         : base(message, eventSource, eventCode) { }
   }

   public class RecordDeletedEvent : WebCustomEvent
   {
      private const int eventCode = WebEventCodes.WebExtendedBase + 10;
      private const string message =
         "The {0} with ID = {1} was deleted by user {2}.";

      public RecordDeletedEvent(string entity, int id, object eventSource)
         : base(string.Format(message, entity, id,
              HttpContext.Current.User.Identity.Name), eventSource, eventCode)
      { }
   }
}

Important 

Note that for custom events to be dynamically loaded correctly when the application starts, they must be placed in their own pre-compiled assembly. This is because the ASP.NET runtimes try to load them before the App_Code files are dynamically compiled, so the custom event type wouldn't be found if you placed the source code there. Because of this, you must create a separate secondary project for the source code, and reference the compiled .dll file (named MB.TheBeerHouse.Custom Events.dll) from the main web project. If you add the Library Project to the solution containing the web project, you'll be able to reference the project instead of the compiled file, so that an updated version of the DLL will be generated and copied into the web project's bin folder every time you compile the solution.

The reason for using a custom base class is so we can add a rule to the web.config file's <health Monitoring> section to log all events that inherit from this WebCustomEvent class, instead of registering them all individually — a big time saver if you have many of them, and this frees us from worrying about the possibility that we might forget to register some of them:

<healthMonitoring heartbeatInterval="10800" >
   <providers>
      <remove name="SqlWebEventProvider" />
      <add name="SqlWebEventProvider" connectionStringName="LocalSqlServer"
         buffer="false" bufferMode="Notification"
         maxEventDetailsLength="1073741823"
         type="System.Web.Management.SqlWebEventProvider,System.Web,
            Version=2.0.0.0,Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a" />
   </providers>
   <eventMappings>
      <add name="TBH Events"
         type="MB.TheBeerHouse.WebCustomEvent, MB.TheBeerHouse.CustomEvents" />
   </eventMappings>
   <rules>
      <clear />
      <add name="TBH Events" eventName="TBH Events"
         provider="SqlWebEventProvider" profile="Critical" />
      <add name="All Errors" eventName="All Errors"
         provider="SqlWebEventProvider" profile="Critical" />
      <add name="Failure Audits" eventName="Failure Audits"
         provider="SqlWebEventProvider" profile="Critical" />
      <add name="Heartbeats" eventName="Heartbeats"
         provider="SqlWebEventProvider" profile="Critical" />
   </rules>
</healthMonitoring>

This is an example of how you can record a delete event from methods of your business classes to be developed in the upcoming chapters (this example assumes we deleted an article with ID = 4):

new RecordDeletedEvent("article", 4, null).Raise();

Previous Page
Next Page


JavaScript EditorFreeware javascript editor     Javascript code