03 September 2008

Writing DNN custom modules

This is the last of three blog entries that make up a full introduction to DotNetNuke (DNN) from a developer's point of view. This time I cover Writing Custom Modules to add your own functionality to a DNN web site. Previously I gave an Introduction to DNN and showed how to Write a simple DNN Skin. A concise version of the complete article first appeared on 1 July 2008 in UK programming magazine VSJ.

The phdcc.CodeModule module described in this article can be downloaded from here.

DNN5 has a new extension/package structure for custom modules and skins - however 'legacy' DNN3 and DNN4 module zips will install in DNN 5.

Overview

Custom DNN modules can be as complex as you want. I want to get started by analysing our freeware phdcc.CodeModule module which is available at dnn.phdcc.com and contains the full source. This module makes it easier to add your own functionality without having to write a whole new module. Your code has access to the database and the ASP.NET and DNN APIs.

phdcc.CodeModule simply renders your own user control. Once the module is installed, write your own code, eg in file intro.ascx. Upload the file to the DesktopModules/phdcc.CodeModule/ directory. Add an instance of the phdcc.CodeModule module to the desired DNN page and configure it to use intro.ascx. intro.ascx can contain normal HTML, ASP.NET code and DNN calls - phdcc.CodeModule will display this whenever requested.

phdcc.CodeModule's initial display:

Enter your control filename in the Settings:
Suppose intro.ascx contains this HTML and code to say hello to the current user:
<%@ Control Language="VB" ClassName="intro" %>
<%@ Import Namespace="DotNetNuke.Entities.Users" %>
Hello <% =UserController.GetCurrentUserInfo.Username%>

Then this is the output, within the SimpleContainer that we made earlier:
As stated earlier, you can switch off the container header so it simply says "Hello host". The code simply says "Hello" if no user is logged in.

Development environment

To develop for DNN, download the latest Starter kit vsi file for Visual Web Developer Express or Visual Studio 2005. Double click on this and the Visual Studio Content Installer creates a few DNN-related templates. To create a whole new DNN site, select [File][New][Web site...], "Visual Basic" and the "DotNetNuke Web Application Framework" template. After the site has been created, an instructions page is shown. You can simply compile and run if you have SQL Server 2005 Express installed. But wait...

To use the full version of SQL Server, create an empty new database, typically with a SQL Server owner login, and put the connection string in Web.Config in two places as instructed. I also set the only instance of "objectQualifier" to eg "DNN_" so that all DNN database tables etc have this prefix. I also usually increase authentication+forms-timeout from 60 (seconds) to let users stay logged in for longer, also setting slidingExpiration to "true". The last task that I might do before running is to convert from Visual Studio's built-in web server to use the local IIS - using an IIS virtual directory seems to work OK. This lets me test how the site looks in FireFox etc.

I then compile and run. With all the above already set up, you can select the "Auto" install option instead of the "Typical" wizard. If installed using "Auto", your first task will be to login as host and admin to change the passwords. Then go to [Host][Host Settings] eg to set the SMTP Server, and then set up the site/portal using [Admin][Site Settings]. Go back to Home and delete all the module instances - then off you go.

The created project has the 'code' half of DNN supplied as DLLs in the /bin/ directory. The web pages, controls, their code-behind and other resources are installed as web site source. All this lot can take a while to compile, which can be a bit frustrating when working on your own module - I did attempt once to convert DNN to a faster Web Project but I couldn't get it to work.

phdcc.CodeModule

Use [Host][Module Definitions] then "Install New Module". Browse to find phdccCodeModule_01_00_02.zip and click "Install New Module". Check the installation log for errors then press "Return". The following files will have been installed under the existing root DesktopModules directory.
DesktopModules
+ phdcc.CodeModule
+ App_LocalResources
+ Edit.ascx.resx
+ Settings.ascx.resx
+ View.ascx.resx
Edit.ascx, Edit.ascx.vb
Settings.ascx, Settings.ascx.vb
View.ascx, View.ascx.vb

The module zip also contains a manifest file phdcc.CodeModule.dnn that tells DNN where to install each file - in addition, it says that there are three controls: a View, Edit and Settings.

The View control displays the normal module output to users. The Settings control is at the bottom of the module settings page - visible to admin users. The Edit control is not used by phdcc.CodeModule yet but will typically be shown to admin users to set up the module content. You can have more user controls if need be.

View

View.ascx primarily contains the following placeholder into which your user control will be inserted. The view contains additional elements which are used to give instructions and show errors.
<asp:PlaceHolder ID="ViewControl" runat="server" />

The View.ascx.vb codebehind class is based on DotNetNuke.Entities.Modules.PortalModuleBase which is in turn derived from DotNetNuke.Framework.UserControlBase and eventually System.Web.UI.UserControl. These base classes contain many crucial DNN properties and objects, ranging from the PortalId to the current UserId. The View Page_Load uses PortalModuleBase.Settings to retrieve your control filename before adding it to the ViewControl PlaceHolder:

Dim ControlName = CStr(Settings("control"))
ViewControl.Controls.Add(LoadControl(ControlName))


If you have not specified your control yet, the code shows an asp:label and a "Settings" asp:button. If this button is clicked then you are redirected to the module's Settings - the DNN NavigateURL global method is used to create the correct URL to see the Settings. In DNN parlance, each page is called a "tab" so the TabID selects the page; there may be more than one module on a page, so the correct one is selected using the ModuleId:
Response.Redirect(DotNetNuke.Common.Globals.NavigateURL(PortalSettings.ActiveTab.TabID, "Module", "moduleid", ModuleId.ToString()), False)

phdcc.CodeModule could be improved by adding an "Edit Control" option to the module menu (to let an admin user create or edit the control in a text area edit box). The existing View implements the IActionable interface to do this job. The ModuleActions get property must return a DNN ModuleActionCollection with a new Action added using localised text. DNN sets up this action so that selecting it redirects to the Edit control.

Settings

The module settings page has a common user interface at the top of the page - our content is shown lower down, as shown above. The Settings.ascx user control primarily shows a labelled text box for you to type in your control filename.
<asp:textbox id="txtControlName" cssclass="NormalTextBox" width="200" runat="server" />

The codebehind Settings class is derived from DotNetNuke.Entities.Modules.ModuleSettingsBase. The LoadSettings method fills the text box with the current setting on first entry. ModuleSettingsBase.TabModuleSettings gets the same information that is retrieved by the view using PortalModuleBase.Settings. Please see my blog on DNN module and tab-module settings for full details of the different settings available.

If Not Page.IsPostBack Then
Dim ControlName = TabModuleSettings("control")
txtControlName.Text = ControlName
End If


When the Settings page Update button is pressed, the UpdateSettings method is called to store the new control filename and refresh the DNN in-memory cache:

Dim objModules As New DotNetNuke.Entities.Modules.ModuleController
objModules.UpdateTabModuleSetting(TabModuleId, "control", txtControlName.Text)
SynchronizeModule()


Localisation

The only remaining element of phdcc.CodeModule is the localisation files in the App_LocalResources directory. In this module, only the Settings.ascx.resx file is used. In Settings.ascx, the dnn:label control is used for the filename prompt:
<dnn:label id="lblControlName" runat="server" controlname="txtControlName" suffix=":">

This shows a help question mark followed by a label, with the actual text picked up from the resource file "lblControlName.Text" and "lblControlName.Help" strings. Within DNN you can edit these strings or create a new resource file for a different locale. DNN looks at the logged-in user's UserInfo.Profile.PreferredLocale property to determine which resource to use.

Tip 1

We recently wrote a simple phdcc.CodeModule user control that would not save its ViewState, eg:
<% If Not IsPostBack Then lblGiven.Text = "init" %>
<asp:Label ID="lblGiven" runat="server" />
If a button was pressed then lblGiven would not contain "init" on postback. The problem turned out to be that embedded code blocks <% %> are run in the PreRender phase, after the ViewState had been saved. The solution was to set the label in the Page_Load method.

Tip 2

In a phdcc.CodeModule control, it is useful to have access to the current DNN PortalModuleBase instance. The following code achieves this:

Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim ViewControl As Control = Me.TemplateControl.Parent
Dim View As Control = ViewControl.Parent
If TypeOf (View) Is PortalModuleBase Then
Dim pmb As PortalModuleBase = CType(View, PortalModuleBase)
Dim ps As PortalSettings = pmb.PortalSettings
End If
End Sub


Tip 3

Sometimes it is not clear whether to use DNN or ASP.NET functions. If in doubt, rummage around in the DNN source to see how they do it. For example, to determine if the user is logged in, I use ASP.NET Request.IsAuthenticated. In another case, I wanted to get someone's password given their UserId. After getting the Username using DNN APIs, I used the ASP.NET GetUser method to return a MembershipUser that is used to get the password.

UserInfo ui = UserController.GetUser( PortalId, UserId, true);
MembershipUser user = Membership.GetUser(ui.Username);
string pwd = user.GetPassword();


Database Modules

More substantial modules will typically want to store their own information in the database. The following example module shows a list of Text/HTML items (in an asp:datalist data bound list). There are admin options to Add Content items and define a template in Settings.

In Visual Studio, right click on the project name and select [Add New Item...], choose "DotNetNuke Dynamic Module" for VB or C#, enter a name such as "TestMod", then press OK. You will have to do two directory renames as per the shown instructions. If you chose C# then you will have to add to the main Web.Config codeSubDirectories element.

In [Host][Module Definitions] click on "Import Module Definition". In the Manifest list, select "TestMod.dnn" and click "Import Manifest". The TestMod module can now be put on a page but will show a ModuleLoadException until the database has been set up.

The new module will have code classes created in the \App_Code\TestMod\ directory. File TestModController.vb represents your business logic and provides access to the database from your user controls. The Text/HTML items are each represented by a TestModInfo.vb object, each of which is stored as a row in a database table. File DataProvider.vb describes a generic database interface, while SqlDataProvider.vb proves an actual implementation of this interface.

Controller

The generated TestModController class has a GetTestMods method that returns a list of TestModInfo objects. It finds the current data provider and calls its GetTestMods method, passing the current module instance id ModuleId. The data provider GetTestMods returns an IDataReader. The DNN Custom Business Object utility class CBO method FillCollection is used to create the TestModInfo list - it sees what fields have been returned in each IDataReader record and fills the corresponding properties in each TestModInfo instance automatically - quite a mouthful but certainly a useful tool. There is a similar method CBO.FillObject to fill just one object.

Public Function GetTestMods(ByVal ModuleId As Integer) As List(Of TestModInfo)
Return CBO.FillCollection(Of TestModInfo)(DataProvider.Instance().GetTestMods(ModuleId))
End Function

The controller also contains further methods GetTestMod, AddTestMod and UpdateTestMod which work with a single TestModInfo object, using matching functionality in the data provider.

The controller class also implements the DNN ISearchable and IPortable interfaces, to support searching and import/export of the module content into XML.

Database SQL

When a module is installed, DNN looks at the manifest version number and compares it to certain filenames in the zip. File 01.00.00.SqlDataProvider contains the SQL instructions to create any tables, stored procedures etc required to bring the database up to version 01.00.00. A script 01.01.03.SqlDataProvider would take the database to version 01.01.03. When installing as new, then both scripts will be run. If you are upgrading from say version 01.00.01 then only the second one is run.

Because you have created a module from a template, the database has not have been created. To set this up, you need to run the 01.00.00.SqlDataProvider script in SQL Server Management Studio, though you may want to rename some of the tables etc first. You should also first replace {databaseOwner} with "dbo." and {objectQualifier} with the "objectQualifier" that you used, ie nothing by default.

The generated TestMod SQL script creates a table called YourCompany_TestMod (for TestModInfo objects) and various stored procedures that correspond to the data provider GetTestMods, GetTestMod, AddTestMod and UpdateTestMod methods. The YourCompany_TestMod table contains a ModuleId field that corresponds to the unique module identifier with the same name in the DNN Modules table.

When you add a TestMod module instance to a page there is no event raised to let you populate the database. The View code knows its ModuleId, so it calls GetTestMods to obtain all TestModInfo objects for the current module instance. If it finds that there are no such objects, then it creates a default TestModInfo and adds it to the database.
Dim objTestMods As New TestModController
Dim colTestMods As List(Of TestModInfo)
colTestMods = objTestMods.GetTestMods(ModuleId)
If colTestMods.Count = 0 Then
Dim objTestMod As TestModInfo = New TestModInfo
objTestMods.AddTestMod(objTestMod)
colTestMods = objTestMods.GetTestMods(ModuleId)
End If

There is no event raised when a module is deleted from a page. So when is your per-module-instance data deleted? DNN has a Recycle Bin for pages and modules that have been deleted. When a module is finally deleted from this bin, the module instance row in the Modules tables is deleted - our YourCompany_TestMod table has a cascade delete relation on this row, thereby deleting the right rows in our table. Unfortunately, there is no programmer event that corresponds to this final delete.

Finally, the generated module also contains another SQL script file Uninstall.SqlDataProvider that is run if the module is uninstalled from the system in [Host][Module Definitions..]. The SQL in there naturally deletes all tables etc that were created in the database.

I will leave an exploration of the rest of the code as an exercise for you dear reader. There are no imposed limitations on how you use the database, so you can create whatever tables you want. Your code has access to all DNN and ASP.NET data, not to mention bog standard Session variables etc, so the world is your oyster.

And more...
If you develop a module and want to keep your source private then follow the documentation instructions. Another option is to make the App_Code section of your module into a separate DLL class library project (while keeping the web site controls with full source). Include the project DLL in your module's ZIP so that it installs on other systems in the /bin/ directory.

Module code also has full access to the web site file system. It is common to store files relating to a site in the relevant portal directory, eg /Portals/0/. It would be sensible if each module worked within a suitably named sub-directory.

There is stack of DNN functionality and namespaces which I haven't touched upon. However I hope that I have given you a flavour of what DNN is and how to extend it with your code – in a simple way using phdcc.CodeModule, or by writing a full-blown custom module.

4 comments:

Tarun said...

how to create a extension package

Chris Cant said...

In DNN 5 custom modules and skins are called extension packages. DNN4 modules, as described above, will install in DNN5 as an extension package.

I did a quick search and found a couple of pages that specifically descibe DNN 5 extension packaging:

http://www.erikvanballegoij.com/tabId/36/itemId/24/DotNetNuke-5-Extension-packaging.aspx

http://www.dotnetnuke.com/Community/Forums/tabid/795/forumid/109/threadid/276990/scope/posts/Default.aspx

Anonymous said...

I have an issue with updating settings (settings.ascx) from another desktop module control (say start.ascx). Module update works on localhost but not on network DNN install. On network - updating from settings control updates the start control settings just fine but the reverse seems to fail on network.

I notice differences exist between settings of both controls:

To get the settings in start control - I use a hashtable. In Settings control - I use Settings[].

To process updates in Settings control I use: 'this.ModuleId'.
In start control I use:
int modID = modules.GetModuleByDefinition(portalID, AppModuleSettings.AppName).ModuleID;

The portalid is found using:
ModuleController modules = new ModuleController();

int portalID = PortalSettings.ActiveTab.ParentId;

if (PortalSettings.PortalId != null)
{ portalID = PortalSettings.PortalId;
}

I update all instances using:
modules.UpdateModuleSetting(...);

ModuleController.SynchronizeModule(this.ModuleId);

Can they be updating the wrong moduleid or even portalid? Or shouldn't the code be more similar?

Chris Cant said...

Not sure if it's relevant, but have you seen this:

http://chriscant.phdcc.com/2008/05/dnn-module-and-tab-module-settings.html