Saturday, October 27, 2012

Implementing a contact form plugin for ASP.NET MVC

The purpose of this post is to demonstrate the patterns presented in the Patterns for ASP.NET MVC Plugins series so far.
  1. Routes, Controllers and Configuration
  2. View Models
  3. Demo: Implementing a contact form plugin
I've chosen the contact form scenario because it's a very common requirement most developers are familiar with. Also, the implementation is short and straightforward. The idea is that you focus on the patterns rather than the actual functionality of the plugin. The same patterns can be used to implement more interesting plugins.

Model

The model for the plugin is a very simple one, only four fields that the user needs to enter.
public class ContactInput {

   [Required]
   [StringLength(100)]
   [Display(Order = 1)]
   public virtual string Name { get; set; }

   [Required]
   [DataType(DataType.EmailAddress)]
   [StringLength(254)]
   [RegularExpression(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*")]
   [Display(Order = 2)]
   public virtual string Email { get; set; }

   [Required]
   [StringLength(254)]
   [Display(Order = 3)]
   public virtual string Subject { get; set; }

   [Required]
   [StringLength(2000)]
   [DataType(DataType.MultilineText)]
   [Display(Order = 4)]
   public virtual string Message { get; set; }
}
This is what the series call an Input Model, which defines the data that an action takes as input.

Patterns used:

Configuration

The following is the class used to provide configuration settings for the plugin.
public class ContactConfiguration {

   public string From { get; set; }
   public string To { get; set; }
   public string CC { get; set; }
   public string Bcc { get; set; }
   public Func<ContactSender> ContactSenderResolver { get; set; }
}
ContactSender is the class that does the work. ContactSenderResolver is an optional setting used to provide a customized instance of ContactSender, we'll be using it later.

Patterns used:

Service

The following is the definition of ContactSender (some code ommited for clarity).
public class ContactSender {

   readonly SmtpClient smtpClient;
   ContactConfiguration config;
      
   public ContactConfiguration Configuration {
      get { return config; }
      set { config = value; }
   }

   public ContactSender() 
      : this(new SmtpClient()) { }

   public ContactSender(SmtpClient smtpClient) {

      if (smtpClient == null) throw new ArgumentNullException("smtpClient");

      this.smtpClient = smtpClient;
   }

   public virtual ContactInput CreateContactInput() {
      return new ContactInput();
   }

   protected virtual void InitializeContactInput(ContactInput input) { }

   public ContactInput Send() {

      ContactInput input = CreateContactInput();
      InitializeContactInput(input);

      return input;
   }

   public virtual bool Send(ContactInput input) {

      var message = new MailMessage {
         To = { this.config.To },
         ReplyToList = { new MailAddress(input.Email, input.Name) },
         Subject = input.Subject,
         Body = renderViewAsString("_MailHtml", input)
      };

      if (this.config.From != null)
         message.From = new MailAddress(this.config.From);

      if (this.config.CC != null)
         message.CC.Add(this.config.CC);

      if (this.config.Bcc != null)
         message.Bcc.Add(this.config.Bcc);

      try {
         this.smtpClient.Send(message);

      } catch (SmtpException ex) {
            
         LogException(ex);

         return false;
      }

      return true;
   }
}
Patterns used:

View Model

public class IndexViewModel {

   public ContactInput InputModel { get; private set; }

   public IndexViewModel(ContactInput inputModel) {
      this.InputModel = inputModel;
   }
}
Patterns used:

View

@model IndexViewModel

@using (Html.BeginForm()) { 
   @Html.AntiForgeryToken()
   var inputHtml = HtmlUtil.HtmlHelperFor(Html, Model.InputModel);
   @inputHtml.EditorForModel()

   <input type="submit" />
}
Patterns used:

Controller

[OutputCache(Location = OutputCacheLocation.None)]
public class ContactController : Controller {

   ContactConfiguration config;
   ContactSender service;

   public ContactController() { }

   public ContactController(ContactSender service) {
      this.service = service;
   }

   protected override void Initialize(RequestContext requestContext) {
         
      base.Initialize(requestContext);

      this.config = requestContext.RouteData.DataTokens["Configuration"] as ContactConfiguration
         ?? new ContactConfiguration();

      if (this.config.ContactSenderResolver != null)
         this.service = this.config.ContactSenderResolver();

      if (this.service == null)
         this.service = new ContactSender();

      this.service.Configuration = this.config;
   }

   [HttpGet]
   public ActionResult Index() {

      this.ViewData.Model = new IndexViewModel(this.service.Send());
         
      return View();
   }

   [HttpPost]
   public ActionResult Index(string foo) {

      ContactInput input = this.service.CreateContactInput();

      this.ViewData.Model = new IndexViewModel(input);
         
      if (!ModelBinderUtil.TryUpdateModel(input, this)) {
            
         this.Response.StatusCode = (int)HttpStatusCode.BadRequest;
         return View();
      }

      if (!this.service.Send(input)) {
            
         this.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
         return View();
      }

      return RedirectToAction("Success");
   }

   [HttpGet]
   public ActionResult Success() {
      return View();
   }
}
Patterns used:

Using the plugin

This is how you can register the plugin using MvcCodeRouting.
public static void RegisterRoutes(RouteCollection routes) {
         
   routes.MapCodeRoutes(
      baseRoute: "Contact",
      rootController: typeof(ContactController),
      settings: new CodeRoutingSettings {
         EnableEmbeddedViews = true,
         Configuration = new ContactConfiguration {
            To = "contact@example.com"
         }
      }
   );
}
If you visit /Contact you'll see the contact form.

Adding a field to the form

Let's add a second plugin instance, this time with an extra field. First, we need to inherit ContactInput and add the new property.
public class CustomContactInput : ContactInput {

   [Required]
   [Display(Name = "How did you hear about us?", Order = 3)]
   [UIHint("Source")]
   public virtual string Source { get; set; }
}
Next, inherit ContactSender and override CreateContactInput.
public class CustomContactSender : ContactSender {

   public override ContactInput CreateContactInput() {
      return new CustomContactInput();
   }
}
Note the use of UIHint("Source") on the new property, let's add that editor template.
@Html.DropDownList("", new[] { "Friend", "Advertisement", "Google", "Other" }
   .Select(s => new SelectListItem { Text = s, Value = s }), "")
Lastly, we register this new plugin instance.
routes.MapCodeRoutes(
   baseRoute: "CustomContact",
   rootController: typeof(ContactController),
   settings: new CodeRoutingSettings {
      EnableEmbeddedViews = true,
      Configuration = new ContactConfiguration {
         To = "info@example.com",
         ContactSenderResolver = () => new Models.CustomContactSender()
      }
   }
);
Note I'm also using a different destination address (To configuration setting). If you visit /CustomContact you can see the form with the new field.

Conclusions

Hopefully seeing the patterns in action makes their utility more clear. The goal is to provide a consistent experience for plugin consumers (application developers), minimize the amount of configuration required to get a plugin working in the host application and maximize the plugin's flexibility to customize its behavior.

Download source code

No comments:

Post a Comment