In part one of this series I mentioned that the Microsoft AJAX Framework along with the ClientAPI are different than a lot of other javascript frameworks since they allow for an end-to-end integration directly with .NET. This entry will discuss how the this integration makes life easier for a DotNetNuke module developer by allowing communication to and from the server to be simple.
The Microsoft AJAX Framework allows for server-side code to add a reference to the js file, initialize the client-side object, and pass in property values by having any control implement the IScriptControl interface. For a DotNetNuke module this means a usercontrol typically inheriting from PortalModuleBase. Since we plan on having both our Edit control and View controls implement the interface we will create a base class to encapsulate the common logic. Perhaps this base class will make it into the core some day, but until then the CodeEndeavors templates will include it.
Public Class AjaxPortalModuleBase
Inherits Entities.Modules.PortalModuleBase : Implements IScriptControl
Public Event AddScriptComponentDescriptors(ByVal Descriptor As ScriptComponentDescriptor)
Public Event AddScriptReferences(ByVal References As List(Of ScriptReference))
Public Event AddLocalizedMessages(ByVal Messages As Dictionary(Of String, String))
#Region "Event Handlers"
'Enable Control Callbacks for this module
'We are passing in the second argument to ensure our callbacks reach this instance of our object
Private Sub Page_Init(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Init
ClientAPI.RegisterControlMethods(Me, Me.ClientID)
End Sub
Private Sub Page_PreRender(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.PreRender
'Tell DotNetNuke we need to use the ScriptManager
DotNetNuke.Framework.AJAX.RegisterScriptManager()
'Register our client-side Script Control
ScriptManager.GetCurrent(Me.Page).RegisterScriptControl(Me)
ScriptManager.GetCurrent(Me.Page).RegisterScriptDescriptors(Me)
End Sub
#End Region
As you can see we have a class that inherits from PortalModuleBase and implements the IScriptControl interface. We also are defining some important events that our usercontrols will trap and provide the necessary information (more on this in a bit). In Part I we looked at how the client-side can invoke a server-side method using the ClientAPI's Control Methods. Here we are getting our first look at how the methods are hooked up. The earliest possible event needs to register that the module supports Control Methods. Just prior to rendering we need to inform the DotNetNuke Framework that we will require the ScriptManager to be present. This code is really not necessary anymore as DNN5 now requires MSAJAX to be present and thus the ScriptManager. Finally, we need to tell the script manager that our control will supply ScriptControl goodness.
It is not time to review our base class's implementation of the IScriptControl interface.
#Region "IScriptControl"
Private Function GetScriptReferences() As IEnumerable(Of ScriptReference) Implements IScriptControl.GetScriptReferences
Dim refs As List(Of ScriptReference) = New List(Of ScriptReference)
RaiseEvent AddScriptReferences(refs)
Return refs
End Function
Private Function GetScriptDescriptors() As IEnumerable(Of ScriptDescriptor) Implements IScriptControl.GetScriptDescriptors
Dim descs As List(Of ScriptDescriptor) = New List(Of ScriptDescriptor)
Dim desc As ScriptComponentDescriptor = New ScriptComponentDescriptor("ClientNamespaceHere")
Dim jss As JavaScriptSerializer = New JavaScriptSerializer()
Dim msgs As Dictionary(Of String, String) = New Dictionary(Of String, String)
desc.AddScriptProperty("id", String.Format("'{0}'", Me.ClientID))
RaiseEvent AddLocalizedMessages(msgs)
desc.AddScriptProperty("msgs", String.Format("'{0}'", jss.Serialize(msgs)))
RaiseEvent AddScriptComponentDescriptors(desc)
descs.Add(desc)
Return descs
End Function
#End Region
The GetScriptReferences method simply creates a generic list of ScriptReferences and delegates the responsibility of filling the list to the control that inherits from it by passing it as an argument for the AddScriptReferences event. This is how our module's external javascript references will get added to our rendered page. We will cover the consumption of this event when we review the module's code.
The GetScriptDescriptors method has a bit more meat to it. The purpose of this method is twofold: to provide the MSAJAX Framework the information necessary to create our client-side object and to initialize its properties with values on the server. The only piece of information the framework needs to create our object is the client-side object name, which gets assigned by our control inheriting from our base class via the Descriptor's Type property. Properties are initialized by calling the AddScriptProperty method passing in the non-prefixed property names. If you refer back to Part I you will remember that we prefixed our properties with get_ and set_. If you have reviewed the code you may be asking why the javascript did not have a set_id method. This method is unnecessary since the Sys.Component method already defines it, and in fact the component requires it. Since it needs to be unique we will use an ID that is guaranteed by the ASP.NET runtime to be unique the ClientID.
There are two events left to discuss in our base class. The AddLocalizedMessages simply passes a Dictionary of string/string to our module for additional messages to be sent down. It then uses the JSON serializer included in the MSAJAX Framework to serialize the dictionary into a single property called msgs. The AddScriptComponentDescriptors event allows for custom properties to be added.
With the common base class defined, lets now turn our attention to the DotNetNuke module inheriting from it.
'-------------------------------------------------------------------------------------------------------------
'- The ControlMethodClass attribute identifies the class as supporting Control Methods
'- The Friendly name parameter is usually named after server-side class namespace, but could be shortened if you like
'- If you change this make sure you also change its client calling code found in EditDNNAjaxModule1.ascx.js
'- dnn.xmlhttp.callControlMethod('YourCompanyHere.Modules.DNNAjaxModule1.EditDNNAjaxModule1.' + this._ns,
'-------------------------------------------------------------------------------------------------------------
<ControlMethodClass("YourCompanyHere.Modules.DNNAjaxModule1.EditDNNAjaxModule1")> _
Partial Class EditDNNAjaxModule1
Inherits AjaxPortalModuleBase
The first thing that should stick out is we are decorating our class with the ControlMethodClass attribute which as the comment states identifies the class as supporting control methods. Obviously we are inheriting from the base class we just covered.
#Region "AjaxPortalModuleBase Events"
'-------------------------------------------------------------------------------------------------------------
'- This is where your client-side javascript that uses the MS AJAX framework needs to be registered
'- Adding the reference here ensures that the MS AJAX script is run before our script which uses things like Type.registerNamespace run
'-------------------------------------------------------------------------------------------------------------
Protected Sub AjaxPortalModuleBase_AddScriptReferences(ByVal References As List(Of ScriptReference)) Handles MyBase.AddScriptReferences
References.Add(New System.Web.UI.ScriptReference(Me.ModulePath & "EditDNNAjaxModule1.ascx.js"))
End Sub
'-------------------------------------------------------------------------------------------------------------
'- Add any localized text needed on the client
'-------------------------------------------------------------------------------------------------------------
Protected Sub AjaxPortalModuleBase_AddLocalizedMessages(ByVal Messages As Dictionary(Of String, String)) Handles MyBase.AddLocalizedMessages
Messages("SettingsSaved") = Localization.GetString("SettingsSaved.Client", LocalResourceFile)
End Sub
'-------------------------------------------------------------------------------------------------------------
'- This is where you add your custom properties that are to be sent down to the client-side object
'- By default we are passing the naming container ID (ns) and localized messages (msgs)
'- If you add your own properties make sure you update your client code with those properties found in EditDNNAjaxModule1.ascx.js
'- get_ImagePath: function() {return this._imagePath;},
'- set_ImagePath: function(value) {this._imagePath = value;},
'-------------------------------------------------------------------------------------------------------------
Protected Sub AjaxPortalModuleBase_AddScriptComponentDescriptors(ByVal Descriptor As ScriptComponentDescriptor) Handles MyBase.AddScriptComponentDescriptors
'IMPORTANT! Enter Client Namespace + ObjectName as Type
Descriptor.Type = "YourCompanyHere.EditDNNAjaxModule1"
'---------------------------------------------------------------------------------------------------------
'Add custom properties here
' Descriptor.AddScriptProperty("ImagePath", String.Format("'{0}'", Me.ModulePath & "images/"))
If Not Settings Is Nothing Then
'This sample uses the Module Settings collection as a custom property serialized to JSON
Dim jss As JavaScriptSerializer = New JavaScriptSerializer()
Descriptor.AddScriptProperty("settings", String.Format("'{0}'", jss.Serialize(Settings)))
End If
'---------------------------------------------------------------------------------------------------------
End Sub
#End Region
The first event, AddScriptReferences, is rather straight forward. We simply create a new ScriptReference passing in the javascript file location. The AddLocalizedMessages event handler simply adds our localized message to the dictionary. However, I do believe it worth spending a little time discussing this approach. The Microsoft AJAX Framework defines one method for localization of messages in javascript by creating multiple scripts, one for each locale. I find this method a bit of a nuisance. The method I am suggesting is to simply have a property called msgs on the client that gets a serialized dictionary of name value pairs. Then in script use the dictionary to obtain each message.
getMessage: function(key)
{
return this._msgs[key];
},
The final event is the AddScriptComponentDescriptors. As mentioned earlier, we need to set the Type to the exact object defined in our javascript. The next portion of code is where we can add any custom properties that we need on the client. In this case we are serializing the entire Settings hashtable to json to read on the client. If you remember we had client-side code in the setter that deserialized this value.
set_settings: function(value) {this._settings = Sys.Serialization.JavaScriptSerializer.deserialize(value);},
The final portion of code for our EditModule is to write our ControlMethod that is exposed to the client.
#Region "Control Methods"
'-------------------------------------------------------------------------------------------------------------
'- Exposing methods to the client code is as simple as adding the ControlMethod attribute
'- IMPORTANT! ALWAYS SECURE YOUR CONTROL METHODS IF INTERACTING WITH SENSITIVE DATA
'-------------------------------------------------------------------------------------------------------------
<ControlMethod()> _
Public Function UpdateSettings(ByVal Settings As Dictionary(Of String, Object)) As Boolean
If Me.EditMode Then 'Making sure only those users with Edit Mode rights can call this method
Dim modController As DotNetNuke.Entities.Modules.ModuleController = New DotNetNuke.Entities.Modules.ModuleController()
For Each key As String In Settings.Keys
modController.UpdateModuleSetting(Me.ModuleId, key, Settings(key))
Next
End If
Return True
End Function
#End Region
Unlike earlier versions of client-callbacks exposed in the ClientAPI, this one can accept any number of parameters and a variety of argument types. In this case we will accept the updated settings object we sent down during the initial rendering of the page. The method needs to be decorated with the ControlMethod attribute and if it exposes or updates sensitive information it needs to be secured. You may be wondering why did the ClientAPI invent such a thing as ControlMethods when the Microsoft AJAX Framework already has the ability to make AJAX calls into either a webservice or a PageMethod. The answer is simple. Neither WebServices nor PageMethods allow you to call into a specific instance of a module since a PageMethod must be defined as static/Shared and a webservice obviously has no context of a module as it is a different class entirely.
That covers the basics of writing our server-side module that supports the creation of a client-side object. Before I sign off I feel it necessary to contrast this form of AJAX development with the kind that the UpdatePanel offers. The major difference is in what is sent between the client / server exchange. For ControlMethods only the serialized settings object plus minimal identifiers (ClientID) is posted. For UpdatePanel the entire form is posted. On the server side, only the first few events in the ASP.NET event lifecycle are fired whereas the UpdatePanel needs nearly all events to fire. Finally the response text of the ControlMethod contains a simple boolean, whereas the UpdatePanel contains a large portion of HTML including ViewState. This is not to say that I do not think that the UpdatePanel has its place in the AJAX space. I am only saying that its place should be used more with retro-fitting existing applications to AJAX, not new modules being developed from scratch. The next entry in this series will showcase yet another advantage this approach has over the UpdatePanel: Client-side Inter-Module Communication.
Hopefully, this entry sheds some light on why the Microsoft AJAX Framework has some additional features than other client-side only frameworks. If you still wish to learn more of the differences, especially relating to the client-side features compared to JQuery, which recently got integrated into DNN5, I suggest listening to this podcast by Scott Hanselman.