ASP.NET Controls – CommunityServer Captcha ControlAdapter, a practical case
- by nmgomes
The ControlAdapter is available since .NET framework version 2.0 and his main goal is to adapt and customize a control render in order to achieve a specific behavior or layout. This customization is done without changing the base control. A ControlAdapter is commonly used to custom render for specific platforms like Mobile. In this particular case the ControlAdapter was used to add a specific behavior to a Control. In this post I will use one adapter to add a Captcha to all WeblogPostCommentForm controls within pontonetpt.com CommunityServer instance. The Challenge The ControlAdapter complexity is usually associated with the complexity/structure of is base control. This case is precisely one of those since base control dynamically load his content (controls) thru several ITemplate. Those of you who already played with ITemplate knows that while it is an excellent option for control composition it also brings to the table a big issue: “Controls defined within a template are not available for manipulation until they are instantiated inside another control.” While analyzing the WeblogPostCommentForm control I found that he uses the ITemplate technique to compose it’s layout and unfortunately I also found that the template content vary from theme to theme. This could have been a problem but luckily WeblogPostCommentForm control template content always contains a submit button with a well known ID (at least I can assume that there are a well known set of IDs). Using this submit button as anchor it’s possible to add the Captcha controls in the correct place. Another important finding was that WeblogPostCommentForm control inherits from the WrappedFormBase control which is the base control for all CommunityServer input forms. Knowing this inheritance link the main goal has changed to became the creation of a base ControlAdapter that could be extended and customized to allow adding Captcha to: post comments form contact form user creation form. And, with this mind set, I decided to used the following ControlAdapter base class signature :public abstract class WrappedFormBaseCaptchaAdapter<T> : ControlAdapter where T : WrappedFormBase
{
}Great, but there are still many to do …
Captcha
The Captcha will be assembled with:
A dynamically generated image with a set of random numbers
A TextBox control where the image number will be inserted
A Validator control to validate whether TextBox numbers match the image numbers
This is a common Captcha implementation, is not rocket science and don’t bring any additional problem. The main problem, as told before, is to find the correct anchor control to ensure a correct Captcha control injection.
The anchor control can vary by:
target control theme
Implementation
To support this dynamic scenario I choose to use the following implementation:private List<string> _validAnchorIds = null;
protected virtual List<string> ValidAnchorIds
{
get
{
if (this._validAnchorIds == null)
{
this._validAnchorIds = new List<string>();
this._validAnchorIds.Add("btnSubmit");
}
return this._validAnchorIds;
}
}
private Control GetAnchorControl(T wrapper)
{
if (this.ValidAnchorIds == null || this.ValidAnchorIds.Count == 0)
{
throw new ArgumentException("Cannot be null or empty", "validAnchorNames");
}
var q = from anchorId in this.ValidAnchorIds
let anchorControl = CSControlUtility.Instance().FindControl(wrapper, anchorId)
where anchorControl != null
select anchorControl;
return q.FirstOrDefault();
}
I can now, using the ValidAnchorIds property, configure a set of valid anchor control Ids.
The GetAnchorControl method searches for a valid anchor control within the set of valid control Ids. Here, some of you may question why to use a LINQ To Objects expression, but the important here is to notice the usage of CSControlUtility.Instance().FindControl CommunityServer method. I want to build on top of CommunityServer not to reinvent the wheel.
Assuming that an anchor control was found, it’s now possible to inject the Captcha at the correct place. This not something new, we do this all the time when creating server controls or adding dynamic controls:protected sealed override void CreateChildControls()
{
base.CreateChildControls();
if (this.IsCaptchaRequired)
{
T wrapper = base.Control as T;
if (wrapper != null)
{
Control anchorControl = GetAnchorControl(wrapper);
if (anchorControl != null)
{
Panel phCaptcha = new Panel {CssClass = "CommonFormField", ID = "Captcha"};
int index = anchorControl.Parent.Controls.IndexOf(anchorControl);
anchorControl.Parent.Controls.AddAt(index, phCaptcha);
CaptchaConfiguration.DefaultProvider.AddCaptchaControls(
phCaptcha,
GetValidationGroup(wrapper, anchorControl));
}
}
}
}
Here you can see a new entity in action: a provider. This is a CaptchaProvider class instance and is only goal is to create the Captcha itself and do everything else is needed to ensure is correct operation.public abstract class CaptchaProvider : ProviderBase
{
public abstract void AddCaptchaControls(Panel captchaPanel, string validationGroup);
}
You can create your own specific CaptchaProvider class to use different Captcha strategies including the use of existing Captcha services like ReCaptcha.
Once the generic ControlAdapter was created became extremely easy to created a specific one. Here is the specific ControlAdapter for the WeblogPostCommentForm control:public class WeblogPostCommentFormCaptchaAdapter : WrappedFormBaseCaptchaAdapter<WrappedFormBase>
{
#region Overriden Methods
protected override List<string> ValidAnchorIds
{
get
{
List<string> validAnchorNames = base.ValidAnchorIds;
validAnchorNames.Add("CommentSubmit");
return validAnchorNames;
}
}
protected override string DefaultValidationGroup
{
get { return "CreateCommentForm"; }
}
#endregion Overriden Methods
}
Configuration
This is the magic step.
Without changing the original pages and keeping the application original assemblies untouched we are going to add a new behavior to the CommunityServer application.
To glue everything together you must follow this steps:
Add the following configuration to default.browser file:<?xml version='1.0' encoding='utf-8'?>
<browsers>
<browser refID="Default">
<controlAdapters>
<!-- Adapter for the WeblogPostCommentForm control in order to add the Captcha and prevent SPAM comments -->
<adapter controlType="CommunityServer.Blogs.Controls.WeblogPostCommentForm" adapterType="NunoGomes.CommunityServer.Components.WeblogPostCommentFormCaptchaAdapter, NunoGomes.CommunityServer" />
</controlAdapters>
</browser>
</browsers>
Add the following configuration to web.config file:<configuration>
<configSections>
<!-- New section for Captcha providers configuration -->
<section name="communityServer.Captcha" type="NunoGomes.CommunityServer.Captcha.Configuration.CaptchaSection" />
</configSections>
<!-- Configuring a simple Captcha provider -->
<communityServer.Captcha defaultProvider="simpleCaptcha">
<providers>
<add name="simpleCaptcha" type="NunoGomes.CommunityServer.Captcha.Providers.SimpleCaptchaProvider, NunoGomes.CommunityServer"
imageUrl="~/captcha.ashx"
enabled="true"
passPhrase="_YourPassPhrase_"
saltValue="_YourSaltValue_"
hashAlgorithm="SHA1"
passwordIterations="3"
keySize="256"
initVector="_YourInitVectorWithExactly_16_Bytes_"
/>
</providers>
</communityServer.Captcha> <system.web>
<httpHandlers>
<!-- The Captcha Image handler used by the simple Captcha provider -->
<add verb="GET" path="captcha.ashx" type="NunoGomes.CommunityServer.Captcha.Providers.SimpleCaptchaProviderImageHandler, NunoGomes.CommunityServer" />
</httpHandlers>
</system.web>
<system.webServer>
<handlers accessPolicy="Read, Write, Script, Execute">
<!-- The Captcha Image handler used by the simple Captcha provider -->
<add verb="GET" name="captcha" path="captcha.ashx" type="NunoGomes.CommunityServer.Captcha.Providers.SimpleCaptchaProviderImageHandler, NunoGomes.CommunityServer" />
</handlers>
</system.webServer>
</configuration>
Conclusion
Building a ControlAdapter can be complex but the reward is his ability to allows us, thru configuration changes, to modify an application render and/or behavior.
You can see this ControlAdapter in action here and here (anonymous required).
A complete solution is available in “CommunityServer Extensions” Codeplex project.