Working with forms and Microsoft Content Management Server 2002
Introduction
This article continues on the Building Websites with Content Management Server 2002 series. In the last article, we learned how to use the data binding features in .NET create navigation controls that can be used on MCMS templates. In this article, we look at the complex task of integrating forms in an MCMS managed site. We will investigate the different approaches available, the pros and cons of each. We will take a deep dive into one of these of these approaches and show some techniques to enable you to overcome this challenge.
The main focus will be on using native .NET controls, OO principles and basic web concepts, to build a framework that can be used when integrating forms and form controls into a MCMS site. We will use data binding, user controls, custom placeholder controls and several classes available in the .NET framework.
It is assumed that the reader has a basic understanding of ASP.NET, data-binding, Visual Studio 2003, and MCMS 2002.
The problem:
When creating web applications with MCMS, it is often that we find ourselves having to create a web form. A web form is an HTML form that will gather user input on the client and perform action on the server. Some examples are a login form, a registration or a feedback form. Out of the box, this functionality is not inherently apparent and integrating this functionality can be tricky.
Within the context of a MCMS template, the developer has access to the current application context, which allows access to the current posting, channel, etc. If you are not in the context of a posting, the current channel navigation controls fail. If you have programmed for this scenario, then you are left having to “set the context” or create a way to circumvent this behavior.
Common approaches:
There are a few approaches that can be used to tackle the problem. We will covet the implementation, the pros and cons of each approach.
Using different templates for each form:
This can be done by creating a separate template gallery to contain each form template. By doing so, you can create a posting for each template and form in the designated channel. This approach is outside the scope of this article.
Pros:
The form is always in the context of a posting so the navigation controls never have to “figure out” where they are.
You can lock down the forms gallery to only allow administrators access
You can style each template separately
Navigation can be simplified by not having to generate different URL’s for forms
Cons
The template to posting ratio is reduced to 1:1 thereby increasing site maintenance
A change to the layout of the site requires a change to each template. This can be very time consuming on a large site since most form pages are usually rendered the same.
Using separate .NET forms, outside of the MCMS channel structure
This is done by creating .NET pages that contain the forms and form controls and maintaining them on the file system as opposed in the channel structure. This approach is outside the scope of this article.
Pros
Faster implementation time since a template does not have to be created for each form.
Cons
No immediate access to context. The developer will have to set the context to a common channel if they need access to the current context.
Navigation controls built for MCMS channels and postings cannot be shared unless you program for this scenario.
Navigation can be difficult since you have to generate different URL’s for each form vs. a posting or channel.
Creating a custom placeholder to display forms controls
This is the most complex of the approaches listed; however, you will realize the most benefits from it. We will cover the implementation of this approach this article.
Pros
Allows the business user to select the form if desired, while maintaining control over the implementation.
New forms are easy to implement, add and test.
The form is always in the context of a posting so the navigation controls never have to “figure out” where they are.
You can lock down the forms gallery to only allow administrators access
You can style each template separately
Navigation can be simplified by not having to generate different URL’s for forms.
There is one template that controls the layout for all forms on the site
User controls can be shared across different templates and/or projects.
Cons
Difficult to implement
Unable to use client side validation in controls
Designing the solution:
We begin by identifying all of the moving parts involved in building the forms control picker placeholder. Much like an ingredients list for a recipe, we will need:
2 or more User controls to pick from
1 template
1 Custom placeholder
1 configuration file to hold the controls to pick from – Web.config
1 Site to test our new controls
Building the forms controls:
We will build 2 controls in this article, a login form and a feedback form. The login form control will obtain an email address and password from the user and validate it. It will display a confirmation message when the user is logged in.
The feedback form will gather the user’s email address, a feedback category and a short comment. Once the form is filled out it will display a confirmation message.
Note: Since the article will focus on concepts rather than building an actual application, the screens will be built using “Mock-itecture”. They will perform simple validation and display different screens by hiding and displaying panels. When building a real application, you would probably use a different approach.
The login form:
Begin by creating a new user control called “LoginFormControl”. We will place labels, text boxes, validators and a button as displayed below. Enclose the form in an ASP panel. Below the form, add another ASP Panel and add a label and a button. This will display the confirmation message.
Once the form is designed, double click on each button to “wire-up” the events on the form. We will perform some basic validation on the email address such as check for null and format. For the password, we will check for null.
In the code behind, we will add some logic to each of the buttons to perform some basic validation on the user data.
///
/// Handles the login button click event. Performs validation and displays a confirmation message of the credentials are correct
///
///
///
private void loginButton_Click(object sender, System.EventArgs e)
{
//Check to see if the user entered valid credentials
if (validateUserCredentials())
{
//The user is valid. Display the confirmation screen
string welcomeText = "Welcome {0} to our site.";
loginConfirmLabel.Text = string.Format(welcomeText, userId.Text);
loginConfirmPanel.Visible = true;
loginFormPanel.Visible = false;
}
else
{
//The user is not valid, display the failure message
statusDisplay.Text = string.Format("Please enter a valid user id/password.
Use {0} and {1}", TEST_USER_ID, TEST_PASSWORD);
statusDisplay.Visible = true;
}
}
///
/// Resets the display of the form to thier intial values.
///
private void resetDisplay()
{
statusDisplay.Text = string.Empty;
userId.Text = string.Empty;
statusDisplay.Visible = false;
loginConfirmPanel.Visible = false;
loginFormPanel.Visible = true;
}
///
/// Validates the user id and the password.
///
///
private bool validateUserCredentials()
{
bool retVal = false;
//Test the user input against our userid and password on file
if (userId.Text.Equals(TEST_USER_ID) && userPassword.Text.Equals(TEST_PASSWORD))
{
retVal = true;
}
return retVal;
}
///
/// Handles the reset button click event. Reset the display
///
///
///
private void resetButton_Click(object sender, System.EventArgs e)
{
resetDisplay();
}
}
Test the controls by dragging them onto a .NET page and running the application. Enter the following text and click each button to validate that the desired results are achieved.
Form Name:
Login form
Text
Field
Action
Desired result
Fooemail
email address
Click login
Error message displayed
foo@email.com
email address
[leave blank]
Password
Click login
Error message displayed
foo@email.com
email address
password
Password
Click login
Confirmation message is displayed
The feedback form
For the feedback form, create a user control called “FeedBackFormControl”. Add 3 labels, 1 text box, 1 drop down box, 1 multi-line text box, and 2 buttons. Add 3 validators, 2 for the email address and one for the password. Also add a validation summary to the top of the form.
///
/// Handles the submit button click event. Performs validation and displays a confirmation message of the user entered valid data
///
///
///
private void submitButton_Click(object sender, System.EventArgs e)
{
if (Page.IsValid)
{
//Display the confirmation screen if the page passes all validation
string confirmText = "Thank you for your feedback.
A confirmation message will be sent to {0}.";
feedbackConfirmLabel.Text = string.Format(confirmText, email.Text);
feedbackConfirmPanel.Visible = true;
feedbackFormPanel.Visible = false;
}
else
{
//The user entered invalid data, display a status message
statusDisplay.Text = "Please enter a valid email address and a comment";
statusDisplay.Visible = true;
}
}
///
/// Resets the display of the form to thier intial values.
///
private void resetDisplay()
{
email.Text = string.Empty;
commentText.Text = string.Empty;
statusDisplay.Text = string.Empty;
statusDisplay.Visible = false;
feedbackConfirmPanel.Visible = false;
feedbackFormPanel.Visible = true;
}
///
/// Handles the reset button clck event. Resets the display of the form to thier intial values.
///
///
///
private void resetButton_Click(object sender, System.EventArgs e)
{
resetDisplay();
}
Test the controls by dragging them onto a test web form, compiling the application and running the application. Enter the following text and click each button to validate that the desired results are achieved.
Feedback form
Form Name:
Feedback form
Text
Field
Action
Desired result
Fooemail
email address
Click submit
Error message displayed
foo@email.com
email address
Site Feedback
Topic
Click Submit
Error message displayed
foo@email.com
email address
password
Password
Click login
Confirmation message is displayed
Building the custom forms picker placeholder
Now that the controls are complete, we will build the custom placeholder that will allow the content contributor to select the appropriate form. We will begin by creating the configuration list to store our list of approved controls, and then we will build custom control picker control.
Create a custom configuration section in web.config. This will be used to store our list of approved controls for the picker. Note: The configuration section declaration must exist at the top of the file, directly under the Configuration node.
The approved controls list is a collection of keys and values. The key is what is stored in the placeholder and the path is used to perform the LoadControl in our custom control.
Once our list has been created, we can build our control picker. Follow the instructions in the MCMS help file to create a new custom placeholder project. Note: for maintainability, add the new project to the existing solution. Add a new class called WebUserControlPicker to the project. This class will be used to display our list of controls. Inherit from the BasePlaceHolder control and add the stubs for the overridden methods. Add an additional override for the EnsureChildControls method to the class. This will be used to guarantee that the custom control is loaded, initialized and available before ViewState is loaded for the page. For additional information related to this method, see the Controls lifecycle topic on MSDN.
protected override void CreateAuthoringChildControls(BaseModeContainer authoringContainer)
{
}
protected override void CreatePresentationChildControls(BaseModeContainer presentationContainer)
{
}
protected override void LoadPlaceholderContentForAuthoring(PlaceholderControlEventArgs e)
{
}
protected override void LoadPlaceholderContentForPresentation(PlaceholderControlEventArgs e)
{
}
protected override void SavePlaceholderContent(PlaceholderControlSaveEventArgs e)
{
}
protected override void EnsureChildControls()
{
}
We will need to add some helper members, properties, and methods to the class. The code below details the members and methods required.
private System.Web.UI.WebControls.DropDownList controlsList;
private System.Web.UI.WebControls.PlaceHolder baseAuthoringContainer = null;
private System.Web.UI.WebControls.PlaceHolder presentationContainer = null;
private System.Web.UI.WebControls.PlaceHolder presentationControl = null;
private string _selectedControlName = String.Empty;
private bool _childrenLoaded = false; //Property for ensuring the child controls get loaded 1 time
///
/// The currently selected control name. This is used by the control to obtain the key to the path in order to load the correct control
///
public string SelectedControlName
{
get
{
if (_selectedControlName == string.Empty)
{
_selectedControlName = GetTextFromCMS();
}
return _selectedControlName;
}
set
{
_selectedControlName = value;
}
}
///
/// Reference to the current XmlPlaceholder
///
private XmlPlaceholder BoundXmlPlaceholder
{
get { return (XmlPlaceholder)this.BoundPlaceholder; }
}
///
/// Gets the controls list from
///
///
/// can be used in this placeholder
protected NameValueCollection GetControlList()
{
NameValueCollection controlList = new NameValueCollection();
controlList = (NameValueCollection)System.Configuration.ConfigurationSettings.GetConfig("UserControlsList");
return controlList ;
}
Since there are several states the web author and hence, our control, can be in, we need methods to assist with the proper loading and populating of the control. When the posting is in the “New” state, the control does not have a value set. We protect against this by determining the web author state and taking the appropriate action.
///
/// Gets the data stored in CMS for the correct mode
///
private void LoadFromCMS()
{
EnsureChildControls();
WebAuthorContext webAuthorContext = WebAuthorContext.Current;
//Depending on the current web author mode, we need to prepare the controls list differently
try
{
switch (webAuthorContext.Mode)
{
case WebAuthorContextMode.AuthoringNew:
this.controlsList.SelectedIndex = 0;
break;
case WebAuthorContextMode.AuthoringReedit:
//Author is changing the control, select the current one
try
{
ListItem selectedControl = this.controlsList.Items.FindByText(SelectedControlName);
if (selectedControl != null)
selectedControl.Selected = true;
}
catch (System.NullReferenceException)
{
//Could not find the control with the current key. It may have been removed from the config file
this.controlsList.SelectedIndex = 0;
}
break;
case WebAuthorContextMode.PresentationPublished:
case WebAuthorContextMode.PresentationUnpublished:
case WebAuthorContextMode.PresentationUnpublishedPreview:
case WebAuthorContextMode.AuthoringPreview:
//Nothing special to do here, carry on
break;
default:
break;
}
}
catch (Exception exp)
{
Literal errorMsg = new Literal();
errorMsg.Text = "
this.presentationContainer.Controls.Add(errorMsg);
}
}
///
/// Retreives the text from the CMS repository. The text stored and retrieved is the key used for the controls list.
///
///
private string GetTextFromCMS()
{
string cmsText = string.Empty;
try
{
cmsText = this.BoundXmlPlaceholder.XmlAsString;
XmlPlaceholderDefinition phd = (XmlPlaceholderDefinition)this.BoundXmlPlaceholder.Definition;
//See if the current placeholder control requires validation if so, retreive the data correctly
if ( phd.CheckForValidity phd.CheckForWellFormedness )
{
if ( cmsText != String.Empty)
{
XmlDocument xmlText = new XmlDocument();
xmlText.LoadXml(cmsText);
if (xmlText.DocumentElement.FirstChild != null && xmlText.DocumentElement.FirstChild.Value != null)
{
cmsText = xmlText.DocumentElement.FirstChild.Value;
}
}
}
}
catch
{
cmsText = string.Empty;
//Do nothing here, the value is null
}
return cmsText;
}
The helper methods also help to ensure the control is still a valid control in our placeholder. If the control could not be loaded, the placeholder fails gracefully. You could possibly guard against this by serializing the control into the placeholder, but that is beyond the scope of this article.
In the CreateAuthoringChildControls method, add code to populate the list from the configuration file.
baseAuthoringContainer = new System.Web.UI.WebControls.PlaceHolder();
this.Controls.Add(baseAuthoringContainer);
if (!this.AuthoringChildControlsAreAvailable)
{
baseAuthoringContainer.Visible = false;
}
this.controlsList = new DropDownList();
this.controlsList.Width = this.Width;
//Get the list from Web.config and populate the list
this.controlsList.DataSource = GetControlList();
this.controlsList.DataBind();
this.baseAuthoringContainer.Controls.Add(this.controlsList);
For presentation, we add code to the CreatePresentationChildControls method to instantiate the control that was chosen and adding it to the presentation container.
//Load the control for presentation and add it to the parent container
//Only do this if the control has been initialized
if (presentationControl != null)
presentationContainer.Controls.Add(presentationControl);
In the LoadPlaceholderContentForAuthoring method, we call our helper method to retrieve the correct key to the control.
LoadFromCMS();
For presentation, we make sure that the control has been added to the controls collection by adding code to the LoadPlaceholderContentForPresentation Method
LoadFromCMS();
EnsureChildControls();
When saving, we need to store the key to the control back to the MCMS repository. We do this by adding code to the SavePlaceholderContent Method
EnsureChildControls();
try
{
string xmlText;
XmlPlaceholderDefinition phd = (XmlPlaceholderDefinition)this.BoundXmlPlaceholder.Definition;
//Set the selected control here
SelectedControlName = this.controlsList.SelectedValue;
try
{
if ( !phd.CheckForValidity && !phd.CheckForWellFormedness )
xmlText = System.Security.SecurityElement.Escape(SelectedControlName);
else
xmlText = "
}
catch
{
xmlText = (!phd.CheckForValidity && !phd.CheckForWellFormedness)? "" : "
}
this.BoundXmlPlaceholder.XmlAsString = xmlText;
}
catch (Exception exp)
{
string newExceptionMessage = String.Format( "[{0} \"{1}\"] Error saving placeholder content: {2}", this.GetType().Name, this.ID, exp.Message);
Exception overriddenException = new Exception(newExceptionMessage, exp);
throw overriddenException;
}
You may have noticed the check for the XMLPLaceholder properties. This is done to guard against the changing of the property once the value has been set.
And finally, in the EnsureChildControls method, we make sure that our control is loaded and initialized. This is the key to guarantying that the selected control has been added to the control hierarchy before ViewState for the page has been loaded. This also ensures that the control will receive any events that must be processed as part of the Postback.
//Make sure that the children are created only once
if (!_childrenLoaded)
{
//Load the control for presentation
if ( SelectedControlName != string.Empty )
{
presentationContainer = new System.Web.UI.WebControls.PlaceHolder();
presentationControl = new System.Web.UI.WebControls.PlaceHolder();
System.Web.UI.Control controlToLoad = this.Page.LoadControl(GetControlList()[SelectedControlName]);
this.presentationControl.Controls.Add(controlToLoad);
_childrenLoaded = true;
}
//Call the base function
base.EnsureChildControls ();
}
Tying it all together:
Build the custom placeholder project and add the control to the designer toolbox. Create a new template gallery item (TGI) and add an XMLPlaceHolder to the template definition. Create a new ASP.NET template and drag the control picker placeholder onto the template. Connect the template to the TGI and then bind the control picker control to the XMLPlaceholder.
Save the templates and test the by creating a new posting and selecting the form control template. Notice that the dropdown box is populated with our list of controls stored in Web.config.
Select the Login Form control and save the new posting as “login”. Approve the posting and attempt the same tests you performed with the control. Notice that the functionality has not changed and the control continues to function as designed.
Follow the same steps to create the feedback posting and test. You have now created a single template that can be used to host forms and a placeholder that can dynamically load the form controls.
The outlined technique can be used anywhere you want to a content contributor to select snippets of pre-approved functionality while maintaining control over the implementation. The new placeholder can be used to add custom navigation, callouts, banners or any other kind of snippet to a posting.
Gotchas and troubleshooting:
Of course there are caveats but I feel that the benefits far outweigh the gotchas.
My Event is not firing
While building the controls, it was trial and error to determine the earliest and the latest we could add the control to the control tree. Initial testing yielded that by overriding the EnsureChildControls, we could achieve everything we wanted to do. If you experience problems with event bubbling (if you click a button and the event is not received by control), you may have created the control too late and .NET has already raised the event, but your control was not initialized to receive it.
Ugly URL’s after a Postback
Because of the way CMS handles URL’s, the Postback will be to a CMS URL and not the friendly name. To get around this, you can use some client side JavaScript to fix the URL in published mode. Add it to the bottom of the template, a footer control or anywhere on the bottom of the page. This will ensure that the page has loaded on the client.
<%
if (CmsHttpContext.Current.UserCanModifySite && WebAuthorContext.Current.Mode.Equals(WebAuthorContextMode.PresentationPublished))
{ %>
<% } %>
Unable to Postback if controls have Client Side validation
This condition happens if you add 2 or more controls that validate on form, for instance a search box and login form. This happens because there is no way in .NET to register the validation JavaScript to a specific control. This is done because the ASP.NET validation scripts are shared on a page. The workaround for the issue is to disable Client-Side validation by setting the EnableClientScript property to False the for the validator controls. This causes the Postback to perform the validation server side. If server roundtrips are a concern, this technique must be evaluated thoroughly.
Conclusion
As you can see, using the native .NET controls gives an MCMS developer a lot of flexibility. The built in data binding capabilities allow you to further detach business logic from presentation, giving the developer full control over the layout and functionality built into the templates. This technique allows the developer full control over the functionality while giving the content contributor access to more content options.