Now that XMod Pro (XMP) for DotNetNuke (DNN) has been out for a few months, I'm starting to get a few questions about leveraging it's extensibility. Thus far, I've been responding to requests directly and haven't yet created any documentation for wider distribution. This series will help rectify that. We'll start with the most popular extension – custom form controls.
Building custom form controls for XMod Pro (XMP) is a great way to implement your own custom functionality, encapsulate business rules, or fill a gap in XMP's existing functionality. Plus, if you've built a control you think others would benefit from, you can package it up and give it away or sell it. XMod has a thriving ecosystem of add-on developers and XMod Pro's growing customer base will provide additional opportunities to add some revenue to your bottom-line during these difficult economic times.
Before we begin, I am assuming you are fairly comfortable with creating server controls in ASP.NET. That is beyond the scope of this article and there are may sources on the internet and in printed form that you can turn to for guidance. Rather, this article is meant to provide you with the information needed to apply your server control building-knowledge to creating custom XMod Pro controls. Let's begin.
Primarily, we are dealing with building controls that participate in the data-binding process of XMP. If you want to use a control merely for display purposes (say a tab control), you don't need to go any further. You can use 3rd party controls or your own custom server controls in XMod Pro forms just by using the <Register>
tag to register them with XMP. They cannot be used to add or edit data, though. For that, you'll need to implement XMP's IDataBoundControl interface.
IDataBoundControl
All controls that participate in the data-binding process of XMP must implement the IDataBoundControl interface. It is pretty simple and, as you might expect, contains properties and functions used by XMP to communicate with your control when loading and saving data.
The fully qualified namespace for the interface is KnowBetter.XModPro.Web.Controls.IDataBoundControl. To use it, you'll need to add a reference in your project to KnowBetter.XModPro.Web.Controls.dll.
The properties in the interface should be familiar to you since you find them on most all of XMP's built-in controls.
Public Interface IDataBoundControl
Property DataField() As String
Property DataType() As System.Data.DbType
Property Nullable() As Boolean
Sub SetValue(ByVal BoundValue As Object)
Function GetValue() As Object
End Interface
- DataField: This is the name of the column in the
<SelectCommand>
and <SubmitCommand>
to which the control is bound.
- DataType: This is the data type that should be used for the column. If the column in the database is an "int", the customer would put Int32 in here. XMP does not support all the System.Data.DbType values, but it covers the major ones (see the help file for details).
- Nullable: If the column can contain a NULL value (i.e. DbNull in .NET), this property will be set to True but the end user. Your control will be responsible for handling nulls.
- SetValue: This method is called by XMP when it is loading data from the
<SelectCommand>
. It will pass your control the value retrieved for DataField. This value may be DbNull and your control should react accordingly.
- GetValue: When XMP is ready to send form control values to the
<SubmitCommand>
, it will call this function on your control. If your control has its Nullable property set to True, then you should return DbNull from this function if your control has what it considers to be an empty value – an empty string, for instance, in a text box.
As you can see, what you have to implement for the interface is quite simple – a couple of properties that describe the column to which the control is bound, and a Set
and Get
function to set and retrieve a value from your control.
Now let's look at how the interface is implemented.
Implementing IDataSourceControl
The sample code we'll be looking at isn't all that useful as a custom control per se. We're focusing on the interface implementation. So, let's look at building a custom TextBox control that automatically HTML encodes its value. Again, I'm assuming you can setup your own server control project in Visual Studio and reference the KnowBetter.XModPro.Web.Controls.dll.
Imports KnowBetter.XModPro.Web.Controls
Public Class HtmlEncodedTextBox
Inherits System.Web.UI.WebControls.TextBox
Implements IDataBoundControl
...
End Class
First (line 1), we Import the KnowBetter.XModPro.Web.Controls namespace which is where the interface and some other helper items reside.
Next, in line 3, we've given our control the clunky but descriptive name of "HtmlEncodedTextBox". For simplicity, we're not going to re-invent the TextBox. Instead we'll simply inherit from ASP.NET's TextBox control (line 4) and augment it.
Finally, in line 5, we declare we're implementing the IDataBoundControl interface. After typing this line and hitting ENTER, the stubs for the interface will be automatically inserted into your code:
Public Property DataField() As String Implements KnowBetter.XModPro.Web.Controls.IDataBoundControl.DataField
Get
End Get
Set(ByVal value As String)
End Set
End Property
Public Property DataType() As System.Data.DbType Implements KnowBetter.XModPro.Web.Controls.IDataBoundControl.DataType
Get
End Get
Set(ByVal value As System.Data.DbType)
End Set
End Property
Public Function GetValue() As Object Implements KnowBetter.XModPro.Web.Controls.IDataBoundControl.GetValue
End Function
Public Property Nullable() As Boolean Implements KnowBetter.XModPro.Web.Controls.IDataBoundControl.Nullable
Get
End Get
Set(ByVal value As Boolean)
End Set
End Property
Public Sub SetValue(ByVal BoundValue As Object) Implements KnowBetter.XModPro.Web.Controls.IDataBoundControl.SetValue
End Sub
Let's take each item one at a time:
Private _DataField As String
Public Property DataField() As String Implements IDataBoundControl.DataField
Get
Return _DataField
End Get
Set(ByVal value As String)
_DataField = value
End Set
End Property
In line 1 we're declaring a private variable to hold the DataField value. Line 4 returns that value when the property is read. Line 7 sets the variable when the property is set. This follows the standard property declaration for most controls and you'll find the other property declarations follow the same pattern:
Private _DataType As System.Data.DbType = System.Data.DbType.String
Public Property DataType() As System.Data.DbType Implements IDataBoundControl.DataType
Get
Return _DataType
End Get
Set(ByVal value As System.Data.DbType)
_DataType = value
End Set
End Property
' When True, empty Text returned as Null
Private _Nullable As Boolean = False
Public Property Nullable() As Boolean Implements IDataBoundControl.Nullable
Get
Return _Nullable
End Get
Set(ByVal Value As Boolean)
_Nullable = Value
End Set
End Property
In the above code, notice in lines 1 and 12, we've set a default value for the _DataType and _Nullable variables. This is good practice – especially for the DataType
property as it should always have a default data type. Nullable
is not as important because Boolean variables always default to False anyway.
Next, let's move on to the GetValue()
function. This is called when XMP needs to get the control's value so it can be sent to the <SubmitCommand>. How you implement this function depends on the nature of your control. For our purposes, we want to HTMLEncode the value of our textbox and send that to XMP:
Public Function GetValue() As Object Implements IDataBoundControl.GetValue
If Me.Nullable Then
If MyBase.Text.Trim.Length = 0 Then
Return System.DBNull.Value
End If
End If
Return Page.Server.HtmlEncode(MyBase.Text)
End Function
Line 2 is important for any control to implement. The control must be able to handle NULL values in some way, shape or form. So, we check to see if Nullable has been set to True (the "Me." isn't necessary but has been added for this tutorial to emphasize Nullable is a property of the control). If so, we return a DBNull value only if our control is empty (defined as no characters or only whitespace for this control, but it could be something else depending on your control). Finally, in line 8 we take our control's value and HTMLEncode it, returning that to XMP.
The SetValue()
method is called by XMP after the <SelectCommand>
has been executed. This could happen when editing a record – the most likely cause – but can also happen if the user wants to set default values for an AddForm
.
Public Sub SetValue(ByVal BoundValue As Object) Implements IDataBoundControl.SetValue
If IsDBNull(BoundValue) Then
MyBase.Text = String.Empty
Else
MyBase.Text = Page.Server.HtmlDecode(BoundValue)
End If
End Sub
XMP sends us BoundValue which is of type Object. The first things the control's code should do is test to see if that BoundValue is DBNull and set its value(s) to what it defines as empty. In the case of our control, we just set the Text property to String.Empty. If the BoundValue isn't DBNull, we can take appropriate action to set the control's property/properties. In this case, we want to display the decoded HTML in the textbox, so we set Text to HtmlDecode(BoundValue). In the real world you would probably want to also do more data type checking, bounds checking, etc.
That's it. The control is technically complete. Before compiling though, we should also account for un-expected errors.
Trapping and Reporting Errors
Since the control's properties can be dynamically bound to data at run-time, it's possible that data may violate the rules for expected values in those properties. While your control is encouraged to trap these errors wherever appropriate, there are two generic areas you'll most likely want to implement. These will help your control play nicely with XMP's system for reporting errors.
Protected Overrides Sub OnDataBinding(ByVal e As System.EventArgs)
Try
MyBase.OnDataBinding(e)
Catch ex As Exception
RaiseBubbleEvent(Me, New ControlErrorEventArgs(ex))
End Try
End Sub
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
Try
MyBase.OnPreRender(e)
Catch ex As Exception
RaiseBubbleEvent(Me, New ControlErrorEventArgs(ex))
End Try
End Sub
In each of these event Overrides, we're catching any un-expected errors from the base class event and bubbling them up the stack. It's important to pass the control as the source of the error (Me), create a new instance of the ControlErrorEventArgs class, and set that object's Exception property to the exception that was just thrown. In the example above, I'm taking advantage of ControlErrorEventArgs alternate constructor to quickly create the class and set the exception.
You can and should use the RaiseBubbleEvent method, pass your control as the source, and use the ControlErrorEventArgs when a significant error is encountered in your control that needs to be reported. This allows XMP to better handle the error and report it to the user. NOTE: The ControlErrorEventArgs may be expanded in the future to provide more granular control over reporting options.
Here's the full code:
Imports System
Imports System.Web.UI.WebControls
Imports KnowBetter.XModPro.Web.Controls
Public Class HtmlEncodedTextBox
Inherits TextBox
Implements IDataBoundControl
#Region "Properties"
Private _DataField As String
Public Property DataField() As String Implements IDataBoundControl.DataField
Get
Return _DataField
End Get
Set(ByVal value As String)
_DataField = value
End Set
End Property
Private _DataType As System.Data.DbType
Public Property DataType() As System.Data.DbType Implements IDataBoundControl.DataType
Get
Return _DataType
End Get
Set(ByVal value As System.Data.DbType)
_DataType = value
End Set
End Property
' When True, empty Text returned as Null
Private _Nullable As Boolean = False
Public Property Nullable() As Boolean Implements IDataBoundControl.Nullable
Get
Return _Nullable
End Get
Set(ByVal Value As Boolean)
_Nullable = Value
End Set
End Property
#End Region
Public Sub New()
' Default to 'string' data type
_DataType = DbType.String
End Sub
Public Function GetValue() As Object Implements IDataBoundControl.GetValue
If Me.Nullable Then
If MyBase.Text.Trim.Length = 0 Then
Return System.DBNull.Value
End If
End If
Return Page.Server.HtmlEncode(MyBase.Text)
End Function
Public Sub SetValue(ByVal BoundValue As Object) Implements IDataBoundControl.SetValue
If IsDBNull(BoundValue) Then
MyBase.Text = String.Empty
Else
MyBase.Text = Page.Server.HtmlDecode(BoundValue)
End If
End Sub
Protected Overrides Sub OnDataBinding(ByVal e As System.EventArgs)
Try
MyBase.OnDataBinding(e)
Catch ex As Exception
RaiseBubbleEvent(Me, New ControlErrorEventArgs(ex))
End Try
End Sub
Protected Overrides Sub OnPreRender(ByVal e As System.EventArgs)
Try
MyBase.OnPreRender(e)
Catch ex As Exception
RaiseBubbleEvent(Me, New ControlErrorEventArgs(ex))
End Try
End Sub
End Class
Compile and Deploy
Now, all we have to do is compile the project, take the resulting DLL, and place that in the site's /bin directory. Then, use the <Register>
tag to register the control in the form and we're ready to use it.
<Register TagPrefix="cctxt" NameSpace="MyCompany.XMPControls.Form" Assembly="MyCompany.XMPControls" />
<AddForm>
...
<cctxt:HtmlEncodeTextBox id="txtBio" TextMode="Multiline" Height="100" Width="400" DataField="Bio" />
...
</AddForm>
<EditForm>
...
<cctxt:HtmlEncodeTextBox Id="txtBio" TextMode="Multiline" Height="100" Width="400" DataField="Bio"/>
...
</EditForm>