(VB) DNN 5 module development tutorial - part 7: Creating the installation package

7. June 2010 22:54

To C# version

If you have followed the previous parts of this tutorial, you should now have your module and its controls installed on your local DotNetNuke site. This is a great starting point for creating an install package. When DotNetNuke installs an extension package (which is really just the extension’s files zipped together) it uses the installation manifest. The installation manifest is an XML document with a .dnn file extension in the installation package. Basically it tells DotNetNuke which files to put where and which database scripts to run on install/uninstall.
Lucky for us we have access to a great extension installation manifest generator; namely DotNetNuke itself.

Before we start creating the installation package we should finish an important part of it; the database uninstall script.
Back in part 3 we created the database install script where the table and stored procedures are created. In the uninstall script we should remove these.
Open up the file Uninstall.SqlDataProvider and replace its content with the following:

/** Drop Table **/

ALTER TABLE {databaseOwner}[{objectQualifier}SipidCode_Products] DROP CONSTRAINT [FK_{objectQualifier}SipidCode_Products_Modules]
GO

DROP TABLE {databaseOwner}[{objectQualifier}SipidCode_Products]
GO

/** Drop Stored Procedures **/

DROP PROCEDURE {databaseOwner}[{objectQualifier}SipidCode_GetProducts]
GO

DROP PROCEDURE {databaseOwner}[{objectQualifier}SipidCode_GetProduct]
GO

DROP PROCEDURE {databaseOwner}[{objectQualifier}SipidCode_AddProduct]
GO

DROP PROCEDURE databaseOwner}[{objectQualifier}SipidCode_UpdateProduct]
GO

DROP PROCEDURE databaseOwner}[{objectQualifier}SipidCode_DeleteProduct]
GO

Don’t forget to save the file.
The batch script you just created will be run when the module is uninstalled and drops the Products table and its foreign key constraint. It also drops the stored procedures we created.

At last we are ready to create the actual installation package! Log in as Host on your local DotNetNuke website and go to Host > Module Definitions. Scroll down to the Products item and click the edit icon.
Scroll down to the section Package Settings. Here you can fill in the fields if you like. Only the Friendly Name field is mandatory.
When you are done, click the Create Package link at the bottom.

Create Package link

The Create Package screen is displayed. Just click the Next link.

The Choose Files to include screen is displayed.

Choose files to include

Only the files shown in the picture above should be included in the package:

  • Database install/uninstall scripts
  • Resource files
  • Control markup files

If there are any others, delete those lines from the text box. Click Next when you are done.

The Choose Assemblies to include screen is displayed. Just click Next.

Assemblies to include

The Create Manifest screen is displayed.

Create Manifest

The text box contains the extension installation manifest for the package.
Great to have it generated for you, isn’t it? When you are done admiring the manifest, click next.

The Create Package screen (not the same one as before) is displayed.

Create Package

Remove the leading underscore character from the Manifest Field Name. A copy of the manifest file will be written into the project directory, overwriting the old and useless file we got from the template.
Finish by clicking Next.
The installation package is now created and written into the /Install/Module folder of your DotNetNuke installation.
You can now go ahead and install the package where you want. I would recommend you to set up another DotNetNuke site for testing your installations on. If you install it on your development site it will collide with your project files and possibly result in trouble.

(VB) DNN 5 module development tutorial - part 6: The settings control

6. June 2010 02:52

To C# version

The settings control is used a bit differently from the view and edit controls. On the module settings page the standard settings are always shown. If you create and register a settings control it will be appended to the end of the standard settings and show up at the bottom of the page.
While the edit page is used to edit the content of the module, the settings page handles the behavior of the module.

In our scenario we will create a settings control where you can configure if the product description will be displayed or not. And yes, I know it’s a pretty lame example but at least it doesn’t clutter up what I am trying to show: the basics for creating a settings control.
‘Nuff said, let’s get to work.

Since the configuration of whether to show the product description or not is basically an on/off selection we will use a checkbox control. Open Settings.ascx and make the contents look like this:

<%@ Control Language="vb" AutoEventWireup="false" Inherits="SipidCode.Modules.Products.Settings" Codebehind="Settings.ascx.vb" %>
<%@ Register TagPrefix="dnn" TagName="Label" Src="~/controls/LabelControl.ascx" %>
<table cellspacing="0" cellpadding="2" border="0">
    <tr>
        <td width="150"><dnn:Label ID="ShowDescriptionLabel" runat="server" ControlName="ShowDescription" Suffix=":"></dnn:Label></td>
        <td><asp:CheckBox id="ShowDescription" runat="server" /></td>
    </tr>
</table>

Now open Settings.ascx.vb. As you might remember from previous parts the view and edit controls inherit from PortalModuleBase. An important difference with the settings control is that it inherits from ModuleSettingsBase. If you are going to create your controls from scratch, don’t forget this.
ModuleSettingsBase inherits from PortalModuleBase and gives us members necessary for loading and updating settings for the module instance.
As you can see the templates have supplied us with overrides of the ModuleSettingsBase methods LoadSettings and UpdateSettings. Change them so they look like the following:

Public Overrides Sub LoadSettings()
 Try
  If (Not IsPostBack) Then
   If String.IsNullOrEmpty(CType(TabModuleSettings("ShowDescription"), String)) Then
    ShowDescription.Checked = True
   Else
    Dim show As Boolean
    If Not Boolean.TryParse(CType(TabModuleSettings("ShowDescription"), String), show) Then
     show = True ' Default to showing the description
    End If
    ShowDescription.Checked = show
   End If
  End If
 Catch ex As Exception
  ProcessModuleLoadException(Me, ex)
 End Try
End Sub

Public Overrides Sub UpdateSettings()
 Try
  Dim controller As New Entities.Modules.ModuleController
  controller.UpdateTabModuleSetting(TabModuleId, "ShowDescription", ShowDescription.Checked.ToString())
 Catch ex As Exception
  ProcessModuleLoadException(Me, ex)
 End Try
End Sub

No black magic here either. In LoadSettings we get the settings from the settings store and populate our controls (in our scenario we only have one checkbox).
In UpdateSettings we get the settings from the controls and stuff them back into the settings store.
Note that settings are only stored in string form, so you have to convert them to and from strings if they are other types (as our show description setting is a bool).

With the logic in place, let’s look at the resources for this control. Open Settings.ascx.resx in the App_LocalResources folder. Make it look something like this:

Settings control resource file

Here is what the different keys are used for:

ControlTitle_settings.Text: This is the section header text for your settings control.
ShowDescriptionLabel.Help: Tooltip text for the question mark icon by the Show description label.
ShowDescriptionLabel.Text: Text of the Show description label.
ModuleHelp.Text: When the Help link is clicked, this text is displayed.

And this is how your settings control will look in a moment:

Settings custom section

Just a couple of steps left until you can see it for yourself...
Now compile your project so we can go ahead and register the control.
In DotNetNuke, go to Host > Module Definitions. Locate Products in the list and click the edit icon. Scroll down to the Module Controls section and click Add Module Control.
Make the following settings, then click Update:
 

Settings control definition

Now go to the page where you added the Products module and click the settings icon. At the bottom of the settings page you should now see your settings control as the section with the header Product Settings.
If you play around with the Show description setting you will notice that the product list shows the description regardless of what you set. This, of course, is because we have not yet implemented any functionality in the view control that takes this setting into account.

To correct this, open up ViewProducts.ascx.vb. Locate the ProductList_ItemDataBound event handler and make it look like this (the green section of code is added):

Protected Sub ProductList_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.ListViewItemEventArgs) Handles ProductList.ItemDataBound
 If e.Item.ItemType = ListViewItemType.DataItem Then
  Dim prod As ProductsInfo
  prod = CType(CType(e.Item, ListViewDataItem).DataItem, ProductsInfo)

  If Me.PortalSettings.UserMode = DotNetNuke.Entities.Portals.PortalSettings.Mode.Edit Then
   Dim prodContainer As Panel
   Dim editLink As New HtmlAnchor

   prodContainer = CType(e.Item.FindControl("ProductContainer"), Panel)
   editLink.HRef = EditUrl("ItemId", prod.ItemId.ToString(), "EditItem")
   editLink.Style.Add("width", "16px")
   editLink.Style.Add("height", "16px")
   editLink.Style.Add("display", "inline-block")
   editLink.Style.Add("background-image", "url(" + DotNetNuke.Common.Globals.ApplicationPath + "/images/edit.gif)")
   editLink.Title = "Edit product"
   prodContainer.Controls.AddAt(0, editLink)
  End If

  ' Show description according to setting.
  Dim productDescription As Label
  Dim showDescription As Boolean

  productDescription = CType(e.Item.FindControl("ProductDescription"), Label)
  If Not Boolean.TryParse(CType(Settings("ShowDescription"), String), showDescription) Then
   showDescription = True
  End If
  productDescription.Visible = showDescription

 End If
End Sub

Compile the project again, the go and play with the setting again. The list should now display the description text only if the Show description checkbox is checked.

(VB) DNN 5 module development tutorial - part 5: The edit control

5. June 2010 15:12

To C# version

This time we are going to create ourselves an edit control. We start with the markup, so open EditProducts.ascx. Make it look like this:

<%@ Control language="vb" Inherits="SipidCode.Modules.Products.EditProducts" AutoEventWireup="false" Explicit="True" Codebehind="EditProducts.ascx.vb" %>
<%@ Register TagPrefix="dnn" TagName="TextEditor" Src="~/controls/TextEditor.ascx"%>
<table width="650" border="0" summary="Edit Product Item">
 <tr>
  <td width="75">Name</td>
  <td><asp:TextBox ID="Name" runat="server"></asp:TextBox></td>
 </tr>
 <tr>
  <td>Description</td>
  <td><dnn:TextEditor ID="Description" runat="server" height="200" width="510" /></td>
 </tr>
</table>
<p>
    <asp:linkbutton cssclass="CommandButton" id="cmdUpdate" resourcekey="cmdUpdate" runat="server" borderstyle="none" text="Update" OnClick="cmdUpdate_Click"></asp:linkbutton>&nbsp;
    <asp:linkbutton cssclass="CommandButton" id="cmdCancel" resourcekey="cmdCancel" runat="server" borderstyle="none" text="Cancel" causesvalidation="False" OnClick="cmdCancel_Click"></asp:linkbutton>&nbsp;
    <asp:linkbutton cssclass="CommandButton" id="cmdDelete" resourcekey="cmdDelete" runat="server" borderstyle="none" text="Delete" causesvalidation="False" OnClick="cmdDelete_Click"></asp:linkbutton>&nbsp;
</p>

Open EditProducts.ascx.vb. In the region “Private Members” the class variable ItemId is declared. This is where we store the ID of the product item to edit.
Insert the following code into the empty Page_Load method (we deleted the method body in the previous part, remember?):

If Not (Request.QueryString("ItemId") Is Nothing) Then
 ItemId = Integer.Parse(Request.QueryString("ItemId"))
End If

Now we have the product item ID in the ItemId variable, if supplied. If there was no ItemId parameter in the query string this means that we should create a new item instead of editing an existing one.

Add the following code:

If Not IsPostBack Then
 If Not Null.IsNull(ItemId) Then
  cmdDelete.Attributes.Add("onClick", "javascript:return confirm('" + Localization.GetString("DeleteItem") + "');")

  Dim dc As New ProductsController
  Dim itm As ProductsInfo = dc.GetProduct(ModuleId, ItemId)

  If Not itm Is Nothing Then
   Name.Text = itm.Name
   Description.Text = itm.Description
  Else
   Response.Redirect(Globals.NavigateURL(), True)
  End If
 Else
  cmdDelete.Visible = False
 End If
End If

This only has to be done the first time the page is loaded so we wrap it in a check for IsPostBack. In the case when an ItemId is supplied we add some client script to get a confirmation when deleting an item. Then we fetch the product item with the current module ID and item ID as keys and populate the controls. If there is no matching item, something must be wrong. In that case we redirect the heck out of there, back to the view page.
When an ItemId is not supplied, this means we should create a new item. Just hide the Delete button.

Error handling is always a good thing, so wrap everything in Page_Load in a try-catch. The Page_Load method should now look something like this:

Protected Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
 Try
  If Not (Request.QueryString("ItemId") Is Nothing) Then
   ItemId = Integer.Parse(Request.QueryString("ItemId"))
  End If

  If Not IsPostBack Then
   If Not Null.IsNull(ItemId) Then
    cmdDelete.Attributes.Add("onClick", "javascript:return confirm('" + Localization.GetString("DeleteItem") + "');")

    Dim dc As New ProductsController
    Dim itm As ProductsInfo = dc.GetProduct(ModuleId, ItemId)

    If Not itm Is Nothing Then
     Name.Text = itm.Name
     Description.Text = itm.Description
    Else
     Response.Redirect(Globals.NavigateURL(), True)
    End If
   Else
    cmdDelete.Visible = False
   End If
  End If
 Catch ex As Exception
  Exceptions.ProcessModuleLoadException(Me, ex)
 End Try
End Sub

The error handling code displays an error message to the user, which is enough for us right now.

Go through all three of the Click event handlers and make sure they are marked as Protected and not Private.

Insert the following as method body for cmdCancel_Click:

Response.Redirect(Globals.NavigateURL(), True)

Next in line is the implementation of cmdUpdate_Click. Basically we check if we got an ItemId or not and thereby decide if we should update an existing item or create a new one.
cmdUpdate_Click should look like this:

Protected Sub cmdUpdate_Click(ByVal sender As Object, ByVal e As EventArgs) Handles cmdUpdate.Click
 Try
  Dim controller As New ProductsController
  Dim productItem As New ProductsInfo

  productItem.ModuleId = ModuleId
  productItem.ItemId = ItemId
  productItem.Name = Name.Text
  productItem.Description = Description.RichText.Text

  If Null.IsNull(ItemId) Then
   controller.AddProduct(productItem)
  Else
   controller.UpdateProduct(productItem)
  End If
  Response.Redirect(Globals.NavigateURL(), True)
 Catch ex As Exception
  Exceptions.ProcessModuleLoadException(Me, ex)
 End Try
End Sub

One thing to note about the code above is how we pull the content from the Description TextEditor control. If you use Description.Text you will get the HTML encoded content (“<” is converted to “&lt;” and so on) which is not what you want in this case. Use Description.RichText.Text and you will get the raw markup.

The last event handler is cmdDelete_Click. If we have an ItemId then we delete the corresponding product item from the database. Simple as that. cmdDelete_Click should look like this:

Protected Sub cmdDelete_Click(ByVal sender As Object, ByVal e As EventArgs) Handles cmdDelete.Click
 Try
  If Not Null.IsNull(ItemId) Then
   Dim controller As New ProductsController
   controller.DeleteProduct(ModuleId, ItemId)
  End If
  Response.Redirect(Globals.NavigateURL(), True)
 Catch ex As Exception
  Exceptions.ProcessModuleLoadException(Me, ex)
 End Try
End Sub

 With that done, compile the project to make sure everything is ok. Then log in as host in your DotNetNuke website. Now we’re going to add our brand new edit control to the Products module. Go to Host > Module Definitions. Locate the Products row in the list and click the edit icon.

Products module definition row

Scroll down to the section Module Controls and click Add Module Control.

Product module controls

Make the following settings and click Update:

Edit control definition

Now we can at last create product items and edit them. Great! Try it out!
Go to the page where you added the Products module. Make sure you are in edit mode. Now click the Add Content link. This time it actually happens something interesting. You should now see the edit control with a text field for product name and a text editor for description. Enter some text in them and click Update. The product you just added should now appear in the product list.
Now click the edit (pencil) icon next to the product name. This will take you to the edit screen and let you edit the product item.
Play around with your nice new controls until next time, when we look at the settings control.

(VB) DNN 5 module development tutorial - part 4: The view control

4. June 2010 19:45

To C# version

A DotNetNuke module usually contains several user controls; view, edit and settings. We will start by creating our view control. As I mentioned in part 2 of this series, the view control is what is displayed to your visitors. The Products view control will be a list of product items belonging to the module instance.
Open ViewProducts.ascx. Delete all content of the file and replace it with this:

<%@ Control language="vb" Inherits="SipidCode.Modules.Products.ViewProducts" AutoEventWireup="false" Explicit="True" Codebehind="ViewProducts.ascx.vb" %>
<asp:ListView ID="ProductList" runat="server">
 <LayoutTemplate>
  <div class="sc-productlist-container">
   <asp:PlaceHolder ID="itemPlaceholder" runat="server"></asp:PlaceHolder>
  </div>
 </LayoutTemplate>
 <ItemTemplate>
  <asp:Panel ID="ProductContainer" runat="server" CssClass="sc-product-container">
   <asp:Label ID="ProductName" runat="server" CssClass="sc-product-name" Text='<%# Eval("Name") %>'></asp:Label>
   <asp:Label ID="ProductDescription" runat="server" CssClass="sc-product-description" Text='<%# Eval("Description") %>'></asp:Label>
  </asp:Panel>
 </ItemTemplate>
</asp:ListView>

Open ViewProducts.ascx.vb and replace the Page_Load method with the following:

Protected Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
 Try
  Dim controller As New ProductsController
  Dim products As List(Of ProductsInfo)
  ' Get the product list.
  products = controller.GetProducts(ModuleId)
  ' Bind the product list to the ListView control.
  ProductList.DataSource = products
  ProductList.DataBind()
 Catch exc As Exception
  ' Module failed to load.
  ProcessModuleLoadException(Me, exc)
 End Try
End Sub

Delete the method lstContent_ItemDataBound.
Add the method ProductList_ItemDataBound, looking like this:

Protected Sub ProductList_ItemDataBound(ByVal sender As Object, ByVal e As System.Web.UI.WebControls.ListViewItemEventArgs) Handles ProductList.ItemDataBound
 If e.Item.ItemType = ListViewItemType.DataItem Then
  Dim prod As ProductsInfo = DirectCast(DirectCast(e.Item, ListViewDataItem).DataItem, ProductsInfo)

  If Me.PortalSettings.UserMode = DotNetNuke.Entities.Portals.PortalSettings.Mode.Edit Then
   Dim prodContainer As Panel
   Dim editLink As New HtmlAnchor

   prodContainer = CType(e.Item.FindControl("ProductContainer"), Panel)
   editLink.HRef = EditUrl("ItemId", prod.ItemId.ToString(), "EditItem")
   editLink.Style.Add("width", "16px")
   editLink.Style.Add("height", "16px")
   editLink.Style.Add("display", "inline-block")
   editLink.Style.Add("background-image", "url(" + DotNetNuke.Common.Globals.ApplicationPath + "/images/edit.gif)")
   editLink.Title = "Edit product"
   prodContainer.Controls.AddAt(0, editLink)
  End If
 End If
End Sub

In ProductList_ItemDataBound we check if the module is viewed in edit mode. In that case we add a link to the edit page (which we have not created a control for yet). The URL to the edit page is created by calling the EditUrl method (defined in PortalModuleBase).

This particular variant of the method will return an URL with the query string parameter "ItemId" carrying the value of the curent product item. The last method parameter will tell the edit page to show the edit control registered with the key "EditItem". You will learn more about that in the next part.

By adding edit links to the items in the list we have created a way to edit existing items, but how about creating new items then?
The ViewProducts class implements the interface IActionable, which defines the property ModuleActions. Look in the region “Optional Interfaces” for the implementation. The property returns a ModuleActionCollection object containing the actions that can be performed on the module. Each action is represented by an icon (or an entry in the module title flip-out menu) when the module is viewed in edit mode.
You can return several actions for a module, but for now we are content with the “add content” action we got from the template.
We will just make one small adjustment: locate the call to EditUrl() and give it the string "EditItem" as parameter. The property implementation should then look like this:

Public ReadOnly Property ModuleActions() As Entities.Modules.Actions.ModuleActionCollection Implements Entities.Modules.IActionable.ModuleActions
 Get
  Dim Actions As New Entities.Modules.Actions.ModuleActionCollection
  Actions.Add(GetNextActionID, Localization.GetString(Entities.Modules.Actions.ModuleActionType.AddContent, LocalResourceFile), Entities.Modules.Actions.ModuleActionType.AddContent, "", "", EditUrl("EditItem"), False, Security.SecurityAccessLevel.Edit, True, False)
  Return Actions
 End Get
End Property

In part 3 we made a lot of changes in the data layer. Due to this there is a lot of broken code in the EditProducts and Settings controls. To sidestep this we just delete the broken code for now and create working code in coming posts.
In EditProducts.ascx.vb, delete the contents of methods Page_Load, cmdUpdate_Click and cmdDelete_Click. Now we should be able to successfully compile the project again and are ready to try out the view in DotNetNuke.
We are going to set up the module manually in the system so we can run it, so open your browser and point it to your local DotNetNuke installation. Log in with the Host account.

We begin with setting up the database, so select SQL from the Host menu. In Visual Studio, open the file 01.00.00.SqlDataProvider. Copy all text and paste it into DotNetNuke. Make sure Run as Script is checked and click Execute

Execute SQL

From the Host menu, select Module Definitions. Make sure Edit mode is selected.

Edit mode selection

Select Create New Module from the Module Definitions flip-out menu.

Module definition menu

In the Create New Module screen, select/enter the following settings:

Create module screen

Then click Create Module.

You should now be able to add the module Products to a page. Go ahead and try it.
There will of course not be any products in the list, since we have not defined any yet. As you can see there is an Add Content link in the module (corresponding to the module's action we looked at earlier in this post.), but there is no Edit control so it will only lead to a blank page.
In the next post we will create that Edit control so we can put some products into the list.

(VB) DNN 5 module development tutorial - part 3: The data layer

3. June 2010 22:41

To C# version

In this part we are going to take a look at how the data layer is organized, set up the database objects and create the functionality needed to access the data.

Of course you are free to access your data any way you want. You might not even have the need to access the database. Anyway, the project template provides us with a data layer structure that I found quite okay to work with.
Your module controls call methods in the ProductsController class (our BLL), which calls methods in the SqlDataProvider class (part of our DAL, which inherits from the abstract DataProvider class). SqlDataProvider then access the database by calling stored procedures (also part of our DAL). The stored procedure calls are made using the Microsoft Enterprise Library.

So, where do we start? Let's build from the ground up and start with the SQL scripts that set up your database objects.
Open the file 01.00.00.SqlDataProvider. As you can see the file is really just an SQL batch script with placeholders for database owner and object prefix ({databaseOwner} and {objectQualifier}). On module installation the placeholders are replaced with the current DotNetNuke installation's values for database owner and object prefix.
I suggest we begin with a clean slate, so delete everything in the file. Insert the following instead:

/** Create Table **/
if not exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_Products]') and OBJECTPROPERTY(id, N'IsTable') = 1)
    BEGIN
        CREATE TABLE {databaseOwner}[{objectQualifier}SipidCode_Products]
        (
            [ModuleID] [int] NOT NULL,
            [ItemID] [int] NOT NULL IDENTITY(1, 1),
            [Name] [nvarchar](200) NOT NULL,
            [Description] [nvarchar](2000) NOT NULL
        )

        ALTER TABLE {databaseOwner}[{objectQualifier}SipidCode_Products] ADD CONSTRAINT [PK_{objectQualifier}SipidCode_Products] PRIMARY KEY NONCLUSTERED  ([ItemID])
        CREATE CLUSTERED INDEX [IX_{objectQualifier}SipidCode_Products] ON {databaseOwner}[{objectQualifier}SipidCode_Products] ([ModuleID])

        ALTER TABLE {databaseOwner}[{objectQualifier}SipidCode_Products] WITH NOCHECK ADD CONSTRAINT [FK_{objectQualifier}SipidCode_Products_{objectQualifier}Modules] FOREIGN KEY ([ModuleID]) REFERENCES {databaseOwner}[{objectQualifier}Modules] ([ModuleID]) ON DELETE CASCADE NOT FOR REPLICATION
    END
GO

To not loose focus we keep the data structure fairly simple... nothing too weird here. The ModuleID column is used to associate the product items to a specific module instance. The rest of the columns belong to the actual product item.
Now that we have a table we need some stored procedures to access its data. But before we create the stored procedures it's a good idea to drop them if the already exist. So add the following:

/** Drop Existing Stored Procedures **/

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_GetProducts]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_GetProducts
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_GetProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_GetProduct
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_AddProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_AddProduct
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_UpdateProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_UpdateProduct
GO

if exists (select * from dbo.sysobjects where id = object_id(N'{databaseOwner}[{objectQualifier}SipidCode_DeleteProduct]') and OBJECTPROPERTY(id, N'IsProcedure') = 1)
    drop procedure {databaseOwner}{objectQualifier}SipidCode_DeleteProduct
GO

And lastly, creation of the stored procedures:

/** Create Stored Procedures **/

create procedure {databaseOwner}{objectQualifier}SipidCode_GetProducts
    @ModuleId int
as

select ModuleId,
       ItemId,
       [Name],
       Description
from {objectQualifier}SipidCode_Products
where  ModuleId = @ModuleId
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_GetProduct
     @ModuleId int,
     @ItemId    int
as

select ModuleId,
       ItemId,
       [Name],
       Description
from {objectQualifier}SipidCode_Products
where  ModuleId = @ModuleId
and ItemId = @ItemId
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_AddProduct
    @ModuleId    int,
    @Name         nvarchar(200),
    @Description  nvarchar(2000)
as

insert into {objectQualifier}SipidCode_Products (
       ModuleId,
       [Name],
       Description
)
values (
    @ModuleId,
    @Name,
    @Description
)
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_UpdateProduct
    @ModuleId     int,
    @ItemId        int,
    @Name          nvarchar(200),
    @Description   nvarchar(2000)
as

update {objectQualifier}SipidCode_Products
set    Name       = @Name,
        Description = @Description
where ModuleId = @ModuleId
and ItemId = @ItemId
GO

create procedure {databaseOwner}{objectQualifier}SipidCode_DeleteProduct
    @ModuleId       int,
    @ItemId          int
as

delete
from   {objectQualifier}SipidCode_Products
where  ModuleId = @ModuleId
and    ItemId = @ItemId
GO

To store the data from the database and use it the application code, we use a class as data holder. This class only members are public properties corresponding to the database table columns we are interested in. Open the ProductsInfo.vb file in the Components folder.
As you see there is just an empty constructor and some public properties. Delete all class members and make the class look like this:

Public Class ProductsInfo

 Private _ModuleId As Integer
 Private _ItemId As Integer
 Private _Name As String
 Private _Description As String

 Public Property ModuleId() As Integer
  Get
   Return _ModuleId
  End Get
  Set(ByVal Value As Integer)
   _ModuleId = Value
  End Set
 End Property

 Public Property ItemId() As Integer
  Get
   Return _ItemId
  End Get
  Set(ByVal Value As Integer)
   _ItemId = Value
  End Set
 End Property

 Public Property Name() As String
  Get
   Return _Name
  End Get
  Set(ByVal Value As String)
   _Name = Value
  End Set
 End Property

 Public Property Description() As String
  Get
   Return _Description
  End Get
  Set(ByVal Value As String)
   _Description = Value
  End Set
 End Property

End Class

Personally, I would have used the shorthand auto properties introduced in VB10. But since I'd like this tutorial to work for VB9 users without too much hassle we stay old school for now :)

With the data objects prepaired we need to get our data provider in the data access layer in order.
Open the file DataProvider.vb in the Components folder. The interesting stuff is found in the "Abstract methods" region. We want to remove the last "s" from the method names and adjust the parameters of AddProduct and UpdateProduct so they look like this:

Public MustOverride Function GetProducts(ByVal ModuleId As Integer) As IDataReader
Public MustOverride Function GetProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer) As IDataReader
Public MustOverride Sub AddProduct(ByVal ModuleId As Integer, ByVal Name As String, ByVal Description As String)
Public MustOverride Sub UpdateProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer, ByVal Name As String, ByVal Description As String)
Public MustOverride Sub DeleteProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer)

Now open SqlDataProvider.vb so we can make matching changes there. Expand the "Public Methods" region. We need to change these methods so they match the abstract methods in DataProvider. Besides changing the method signatures we also have to adjust the implementations. The string parameter in the calls to GetFullyQualifiedName() should match the name of the stored procedure we want to call. We also have to change the parameters sent to the stored procedures. The methods in the "Public Methods" region should look like this:

Public Overrides Function GetProducts(ByVal ModuleId As Integer) As IDataReader
 Return CType(SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetProducts"), ModuleId), IDataReader)
End Function

Public Overrides Function GetProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer) As IDataReader
 Return CType(SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetProduct"), ModuleId, ItemId), IDataReader)
End Function

Public Overrides Sub AddProduct(ByVal ModuleId As Integer, ByVal Name As String, ByVal Description As String)
 SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("AddProduct"), ModuleId, Name, Description)
End Sub

Public Overrides Sub UpdateProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer, ByVal Name As String, ByVal Description As String)
 SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("UpdateProduct"), ModuleId, ItemId, Name, Description)
End Sub

Public Overrides Sub DeleteProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer)
 SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("DeleteProduct"), ModuleId, ItemId)
End Sub

Then we need to make matching changes in the ProductsController class. As I said before ProductsController calls the methods in SqlDataProvider to get the data and returns it to the controls. In our simple implementation there is not very much interesting going on in ProductsController. In a more advanced scenario there could be business specific logic in there, thereby earning ProductsController the name "business logic layer".
We want the methods in the "Public methods" region in ProductsController.vb to look like this:

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

Public Function GetProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer) As ProductsInfo
 Return CType(CBO.FillObject(DataProvider.Instance().GetProduct(ModuleId, ItemId), GetType(ProductsInfo)), ProductsInfo)
End Function

Public Sub AddProduct(ByVal prod As ProductsInfo)
 If prod.Name.Trim <> "" Then
  DataProvider.Instance().AddProduct(prod.ModuleId, prod.Name, prod.Description)
 End If
End Sub

Public Sub UpdateProduct(ByVal prod As ProductsInfo)
 If prod.Name.Trim <> "" Then
  DataProvider.Instance().UpdateProduct(prod.ModuleId, prod.ItemId, prod.Name, prod.Description)
 End If
End Sub

Public Sub DeleteProduct(ByVal ModuleId As Integer, ByVal ItemId As Integer)
 DataProvider.Instance().DeleteProduct(ModuleId, ItemId)
End Sub

 

Remove the inheritance from interfaces ISearchable and IPortable, changing the class declaration from

Public Class ProductsController
 Implements Entities.Modules.ISearchable, Entities.Modules.IPortable

to

Public Class ProductsController

Delete the methods GetSearchItems, ExportModule and ImportModule. We will revisit these in later posts.

With that done we now have the functionality we need to access the database and get/set/update our data. Next time we will put it to use in the control for the module's edit view.

(VB) DNN 5 module development tutorial - part 2: Setting up the project

2. June 2010 22:38

To C# version

During the next parts of this tutorial we will create a module displaying a simple listing of products. We will call it... Products! In this part we will set up the project and look at what is in the templates.

In Visual Studio, create a new project. In the New Project dialog, under Installed Templates, expand Other Languages > Visual Basic and select the Web templates node. Then select "DotNetNuke Compiled Module" from the templates list. Enter "Products" as the name of your module. Then point the location to the "DesktopModules" folder of your DNN installation. Make sure "Create directory for solution" is not checked. This is important. Click OK.

Visual Studio 2010 New Project dialog


Now when we have the project started, there are a couple of things you should know.
First: the project is kind of a cross-breed between a library project and a web project. This is so we can work with the user controls (the ascx files and their associated code-behind and designer files) pretty much like in a standard web project. If you just start an ordinary library project, add the files needed for a user control and try to work with them in the Visual Studio environment, you are neck deep in additional work.

Second: since the project is of the mixed type described above, it will try to run if you start debugging it. This will not work. I will cover dubugging of modules in some other post.

Third: If a web.config file for some reason is created in your module project; delete it. Since your project folder is a subdirectory in the DotNetNuke web application that hosts you module, a web.config file there will most probably lead to problems later on when you are running your web site.

With those things straightened out, go into the properties settings of your project by double-clicking the My Project node in solution explorer. On the Application tab to the left, set the Assembly name to "SipidCode.Modules.Products". Make sure the Root namespace field is empty.

Project settings screen

Nowadays DNN is compiled against version 3.5 of the .NET framework. This means we will have to set at least that as target version for our project as well.
Select the Compile tab. Click the Advanced Compile Options... button at the bottom of the screen. At the bottom of the Advanced Compiler Settings dialog, select ".NET Framework 3.5" as Target framework.

Advanced Compiler settings

Click OK to close the dialog.
You will now be informed that the project has to be closed and reopened, and asked if you want to continue. Click Yes.

Open up the project properties again. Then select the References tab to the left. As you can see the topmost reference (DotNetNuke.Library) cannot be found. Select it in the list and click the Remove button below the list to remove it.

Project references

Now we need a reference to the DotNetNuke assembly for things to work correctly. Click the Add... button below the list. The Add Reference dialog will open. Select the Browse tab. The contents of the Products project folder will be displayed. Go up two levels in the folder structure and from there go into the bin folder. Select the file "DotNetNuke.dll" and click OK.

To make the designer work correctly with DotNetNuke controls we will have to help it find them. Select the Web tab to the left.
Check the radio button Use Local IIS Web server. Enter the URL to the folder where your module code is in your local DotNetNuke installation.
Make sure Override applicationroot URL checkbox is checked and enter the URL to your local DotNetNuke website under it.

Project web settings

You can now close the Products tab.
From the Edit menu, select Find and Replace > Replace in Files. Enter "YourCompany" in the Find what field, and "SipidCode" in Replace with. Expand Find options and make sure Look at these file types is set to "*.*". Click Replace All.

Visual Studio 2008 Find Replace window

You can now compile the project to make sure it works as it should.
Let's take a peek at what the project template has given us.

User controls: ViewProducts, EditProducts and Settings. These correspond to different modes of your module. Their names give us a hint of what they are for. The View control is used when the module is displayed to the visitors of your website. The Edit control is used when an administrator goes into edit mode. This is usually where the content of the module is handled. Then we have the Settings control. That one is displayed in bottom of the settings screen of the control. Used to handle settings special to your module.

Database setup and uninstall scripts: the files with the extension SqlDataProvider. As you see there is one with the version number "01.00.00". That one is run on installation of the module, setting up tables, stored procedures and so on. It's really just a SQL batch script. There can be multiple installation scripts, each corresponding to a version of your module. We will go into the details of that in some future post.
There is also the Uninstall script. That one is run on (yeah! you guessed right!) uninstallation of the module, removing the database objects created by the installation script.

Extension installation manifest: Products.dnn. This is the instruction file for the installation. It tells DotNetNuke stuff like which scripts to run, which files to copy where and so on. We will look closer at that one later on. We've got a lot of other stuff to cover before we get to the installation part.
Components folder. This is where the files involved in the data access layer are located.
The Documentation folder contains only some info concerning the project template. It is pretty obsolete and is of no interest to us.

Next time we are going to take a look into the data layer. Then we will write some code, at last.

(VB) DNN 5 module development tutorial - part 1: The tools

1. June 2010 22:31

This is a re-work of the C# module tutorial series to suit VB developers. It uses newer versions of Visual Studio and DNN, but otherwise it is pretty much the same as its C# sibling.
Credits for the VB code goes to my friend Kai Joussen who made this possible.
But now, to the Visual Basic module action...

So, you want to create your own DNN modules, but don't know how? Started programming with the Visual Studio Starter Kit but got stuck because it seems so... complicated? Fear not. There is no über advanced programming neccessary. Once you get a grip on how it all fits together you will wonder what all the fuss was about.
In this tutorial series I will assume that you have at least basic skills in ASP.NET development. I will also frequently skip over info and procedures you easily can google yourself.
So, since this is meant to be a tutorial, let's start from the beginning and look at what you need to get some serious module dev action.
Of course there are several setups and practices regarding the development of DotNetNuke modules, but this tutorial will use the following:

  • DotNetNuke 5 installed and running locally on IIS
  • Visual Studio 2010 Professional
  • SQL Server (SQL Server Express will work just as well)
  • DotNetNuke Visual Studio Starter Kit

In this tutorial series I use version 5.4.2 of both DotNetNuke web application and Visual Studio Starter Kit.
If you still use Visual Studio 2008, don't get too worried. The differences aren't that big and I tried to use procedures that should work fairly well in VS 2008 too.

DotNetNuke installation

Make sure you have IIS installed, configured and running on your machine. This tutorial won't cover setting that up since there is tons if guides for that out there.
Next, download the new install version of DotNetNuke. You'll find the latest version under "Recommended Download" in the downloads section of DNN's project page on CodePlex. To simplify things i suggest you do not use the source code version since this will make things slower and possibly confuse you, since all the source files for DotNetNuke will be in your installation.
I have DotNetNuke installed in the web root of my local web server, but installation in a virtual directory works fine too.
To install, simply unzip the contents of the installation package from CodePlex into your web root or virtual folder. Now set up an empty database in SQL Server or SQL Server Express. Point your web browser to your DotNetNuke installation, e.g. "http://localhost" or "http://localhost/MyDnnInstallation". This will start the installation wizard, which I won't go through in detail either. Read the info on the screens carefully when you go through it and you should be fine.

Visual Studio Starter Kit

The Visual Studio Starter Kit will help you with templates for our new module. It can also be downloaded from CodePlex under "Other Available Downloads". The installation is very simple so I won't go into that at all. The starter kit contains templates for both C# and Visual Basic and installs them for both Visual Studio 2008 and 2010 (probably also for 2005, but I have not verified this myself).

Now we have the tools ready, so the next part will be about starting our first module project and take a look at what is in the templates.

DotNetNuke 5 module development tutorial - part 7: Creating the installation package

9. March 2010 21:48

To Visual Basic version

If you have followed the previous parts of this tutorial, you should now have your module and its controls installed on your local DotNetNuke site. This is a great starting point for creating an install package. When DotNetNuke installs an extension package (which is really just the extension’s files zipped together) it uses the installation manifest. The installation manifest is an XML document with a .dnn file extension in the installation package. Basically it tells DotNetNuke which files to put where and which database scripts to run on install/uninstall.
Lucky for us we have access to a great extension installation manifest generator; namely DotNetNuke itself.

Before we start creating the installation package we should finish an important part of it; the database uninstall script.
Back in part 3 we created the database install script where the table and stored procedures are created. In the uninstall script we should remove these.
Open up the file Uninstall.SqlDataProvider and replace its content with the following:

/** Drop Table **/

ALTER TABLE {databaseOwner}[{objectQualifier}SipidCode_Products] DROP CONSTRAINT [FK_{objectQualifier}SipidCode_Products_Modules]
GO

DROP TABLE {databaseOwner}[{objectQualifier}SipidCode_Products]
GO

/** Drop Stored Procedures **/

DROP PROCEDURE {databaseOwner}[{objectQualifier}SipidCode_GetProducts]
GO

DROP PROCEDURE {databaseOwner}[{objectQualifier}SipidCode_GetProduct]
GO

DROP PROCEDURE {databaseOwner}[{objectQualifier}SipidCode_AddProduct]
GO

DROP PROCEDURE databaseOwner}[{objectQualifier}SipidCode_UpdateProduct]
GO

DROP PROCEDURE databaseOwner}[{objectQualifier}SipidCode_DeleteProduct]
GO

Don’t forget to save the file.
The batch script you just created will be run when the module is uninstalled and drops the Products table and its foreign key constraint. It also drops the stored procedures we created.

At last we are ready to create the actual installation package! Log in as Host on your local DotNetNuke website and go to Host > Module Definitions. Scroll down to the Products item and click the edit icon.
Scroll down to the section Package Settings. Here you can fill in the fields if you like. Only the Friendly Name field is mandatory.
When you are done, click the Create Package link at the bottom.

Create Package link

The Create Package screen is displayed. Just click the Next link.

The Choose Files to include screen is displayed.

Choose files to include

Only the files shown in the picture above should be included in the package:

  • Database install/uninstall scripts
  • Resource files
  • Control markup files

If there are any others, delete those lines from the text box. Click Next when you are done.

The Choose Assemblies to include screen is displayed. Just click Next.

Assemblies to include

The Create Manifest screen is displayed.

Create Manifest

The text box contains the extension installation manifest for the package.
Great to have it generated for you, isn’t it? When you are done admiring the manifest, click next.

The Create Package screen (not the same one as before) is displayed.

Create Package

Remove the leading underscore character from the Manifest Field Name. A copy of the manifest file will be written into the project directory, overwriting the old and useless file we got from the template.
Finish by clicking Next.
The installation package is now created and written into the /Install/Module folder of your DotNetNuke installation.
You can now go ahead and install the package where you want. I would recommend you to set up another DotNetNuke site for testing your installations on. If you install it on your development site it will collide with your project files and possibly result in trouble.

DotNetNuke 5 module development tutorial - part 6: The settings control

25. February 2010 23:49

To Visual Basic version

The settings control is used a bit differently from the view and edit controls. On the module settings page the standard settings are always shown. If you create and register a settings control it will be appended to the end of the standard settings and show up at the bottom of the page.
While the edit page is used to edit the content of the module, the settings page handles the behavior of the module.

In our scenario we will create a settings control where you can configure if the product description will be displayed or not. And yes, I know it’s a pretty lame example but at least it doesn’t clutter up what I am trying to show: the basics for creating a settings control.
‘Nuff said, let’s get to work.

Since the configuration of whether to show the product description or not is basically an on/off selection we will use a checkbox control. Open Settings.ascx and make the contents look like this:

<%@ Control Language="C#" AutoEventWireup="false" Inherits="SipidCode.Modules.Products.Settings" Codebehind="Settings.ascx.cs" %>
<%@ Register TagPrefix="dnn" TagName="Label" Src="~/controls/LabelControl.ascx" %>
<table cellspacing="0" cellpadding="2" border="0">
    <tr>
        <td width="150"><dnn:Label ID="ShowDescriptionLabel" runat="server" ControlName="ShowDescription" Suffix=":"></dnn:Label></td>
        <td><asp:CheckBox id="ShowDescription" runat="server" /></td>
    </tr>
</table>

Now open Settings.ascx.cs. As you might remember from previous parts the view and edit controls inherit from PortalModuleBase. An important difference with the settings control is that it inherits from ModuleSettingsBase. If you are going to create your controls from scratch, don’t forget this.
ModuleSettingsBase inherits from PortalModuleBase and gives us members necessary for loading and updating settings for the module instance.
As you can see the templates have supplied us with overrides of the ModuleSettingsBase methods LoadSettings and UpdateSettings. Change them so they look like the following:

public override void LoadSettings()
{
 try
 {
  if (!IsPostBack)
  {
   if ((string)TabModuleSettings["ShowDescription"] != string.Empty)
   {
    bool show;
    if (!bool.TryParse((string)TabModuleSettings["ShowDescription"], out show))
    {
     show = true; // Default to showing the description.
    }
    ShowDescription.Checked = show;
   }
  }
 }
 catch (Exception ex)
 {
  Exceptions.ProcessModuleLoadException(this, ex);
 }
}

public override void UpdateSettings()
{
 try
 {
  ModuleController controller = new ModuleController();
  controller.UpdateTabModuleSetting(TabModuleId, "ShowDescription", ShowDescription.Checked.ToString());
 }
 catch (Exception ex)
 {
  Exceptions.ProcessModuleLoadException(this, ex);
 }
}

No black magic here either. In LoadSettings we get the settings from the settings store and populate our controls (in our scenario we only have one checkbox).
In UpdateSettings we get the settings from the controls and stuff them back into the settings store.
Note that settings are only stored in string form, so you have to convert them to and from strings if they are other types (as our show description setting is a bool).

With the logic in place, let’s look at the resources for this control. Open Settings.ascx.resx in the App_LocalResources folder. Make it look something like this:

Settings control resource file

Here is what the different keys are used for:

ControlTitle_settings.Text: This is the section header text for your settings control.
ShowDescriptionLabel.Help: Tooltip text for the question mark icon by the Show description label.
ShowDescriptionLabel.Text: Text of the Show description label.
ModuleHelp.Text: When the Help link is clicked, this text is displayed.

And this is how your settings control will look in a moment:

Settings custom section

Just a couple of steps left until you can see it for yourself...
Now compile your project so we can go ahead and register the control.
In DotNetNuke, go to Host > Module Definitions. Locate Products in the list and click the edit icon. Scroll down to the Module Controls section and click Add Module Control.
Make the following settings, then click Update:
 

Settings control definition

Now go to the page where you added the Products module and click the settings icon. At the bottom of the settings page you should now see your settings control as the section with the header Product Settings.
If you play around with the Show description setting you will notice that the product list shows the description regardless of what you set. This, of course, is because we have not yet implemented any functionality in the view control that takes this setting into account.

To correct this, open up ViewProducts.ascx.cs. Locate the ProductList_ItemDataBound event handler and make it look like this (the green section of code is added):

protected void ProductList_ItemDataBound(object sender, ListViewItemEventArgs e)
{
 if (e.Item.ItemType == ListViewItemType.DataItem)
 {
  ProductsInfo prod = (ProductsInfo)((ListViewDataItem)e.Item).DataItem;
  if (this.PortalSettings.UserMode == DotNetNuke.Entities.Portals.PortalSettings.Mode.Edit)
  {
   Panel prodContainer = (Panel)e.Item.FindControl("ProductContainer");
   System.Web.UI.HtmlControls.HtmlAnchor editLink = new System.Web.UI.HtmlControls.HtmlAnchor();
   editLink.HRef = EditUrl("ItemId", prod.ItemId.ToString(), "EditItem");
   editLink.Style.Add("width", "16px");
   editLink.Style.Add("height", "16px");
   editLink.Style.Add("display", "inline-block");
   editLink.Style.Add("background-image", "url(/images/edit.gif)");
   editLink.Title = "Edit product";
   prodContainer.Controls.AddAt(0, editLink);
  }

  // Show description according to setting.
  Label productDescription = (Label)e.Item.FindControl("ProductDescription");
  bool showDescription;
  if(!bool.TryParse(Settings["ShowDescription"] as string, out showDescription))
  {
   showDescription = true;
  }
  productDescription.Visible = showDescription;

 }
}

Compile the project again, the go and play with the setting again. The list should now display the description text only if the Show description checkbox is checked.

DotNetNuke 5 module development tutorial - part 5: The edit control

17. February 2010 22:24

To Visual Basic version

This time we are going to create ourselves an edit control. We start with the markup, so open EditProducts.ascx. Make it look like this:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="EditProducts.ascx.cs" Inherits="SipidCode.Modules.Products.EditProducts" %>
<%@ Register TagPrefix="dnn" TagName="TextEditor" Src="~/controls/TextEditor.ascx"%>
<table width="650" border="0" summary="Edit Product Item">
 <tr>
  <td width="75">Name</td>
  <td><asp:TextBox ID="Name" runat="server"></asp:TextBox></td>
 </tr>
 <tr>
  <td>Description</td>
  <td><dnn:TextEditor ID="Description" runat="server" height="200" width="510" /></td>
 </tr>
</table>
<p>
    <asp:linkbutton cssclass="CommandButton" id="cmdUpdate" resourcekey="cmdUpdate" runat="server" borderstyle="none" text="Update" OnClick="cmdUpdate_Click"></asp:linkbutton>&nbsp;
    <asp:linkbutton cssclass="CommandButton" id="cmdCancel" resourcekey="cmdCancel" runat="server" borderstyle="none" text="Cancel" causesvalidation="False" OnClick="cmdCancel_Click"></asp:linkbutton>&nbsp;
    <asp:linkbutton cssclass="CommandButton" id="cmdDelete" resourcekey="cmdDelete" runat="server" borderstyle="none" text="Delete" causesvalidation="False" OnClick="cmdDelete_Click"></asp:linkbutton>&nbsp;
</p>

Open EditProducts.ascx.cs. In the region “Private Members” the class variable ItemId is declared. This is where we store the ID of the product item to edit.
Insert the following code into the empty Page_Load method (we deleted the method body in the previous part, remember?):

if ((Request.QueryString["ItemId"] != null))
{
 ItemId = Int32.Parse(Request.QueryString["ItemId"]);
}

Now we have the product item ID in the ItemId variable, if supplied. If there was no ItemId parameter in the query string this means that we should create a new item instead of editing an existing one.

Add the following code:

if (!IsPostBack)
{
 if (!Null.IsNull(ItemId))
 {
  cmdDelete.Attributes.Add("onClick", "javascript:return confirm('" + Localization.GetString("DeleteItem") + "');");
  ProductsController dc = new ProductsController();
  ProductsInfo itm = dc.GetProduct(ModuleId, ItemId);
  if (itm != null)
  {
   Name.Text = itm.Name;
   Description.Text = itm.Description;
  }
  else
  {
   Response.Redirect(Globals.NavigateURL(), true);
  }
 }
 else
 {
  cmdDelete.Visible = false;
 }
}

This only has to be done the first time the page is loaded so we wrap it in a check for IsPostBack. In the case when an ItemId is supplied we add some client script to get a confirmation when deleting an item. Then we fetch the product item with the current module ID and item ID as keys and populate the controls. If there is no matching item, something must be wrong. In that case we redirect the heck out of there, back to the view page.
When an ItemId is not supplied, this means we should create a new item. Just hide the Delete button.

Error handling is always a good thing, so wrap everything in Page_Load in a try-catch. The Page_Load method should now look something like this:

protected void Page_Load(object sender, System.EventArgs e)
{
 try
 {
  if ((Request.QueryString["ItemId"] != null))
  {
   ItemId = Int32.Parse(Request.QueryString["ItemId"]);
  }
  if (!IsPostBack)
  {
   if (!Null.IsNull(ItemId))
   {
    cmdDelete.Attributes.Add("onClick", "javascript:return confirm('" + Localization.GetString("DeleteItem") + "');");
    ProductsController dc = new ProductsController();
    ProductsInfo itm = dc.GetProduct(ModuleId, ItemId);
    if (itm != null)
    {
     Name.Text = itm.Name;
     Description.Text = itm.Description;
    }
    else
    {
     Response.Redirect(Globals.NavigateURL(), true);
    }
   }
   else
   {
    cmdDelete.Visible = false;
   }
  }
 }
 catch (Exception ex)
 {
  Exceptions.ProcessModuleLoadException(this, ex);
 }
}

The error handling code displays an error message to the user, which is enough for us right now.

Insert the following as method body for cmdCancel_Click:

Response.Redirect(Globals.NavigateURL(), true);

Next in line is the implementation of cmdUpdate_Click. Basically we check if we got an ItemId or not and thereby decide if we should update an existing item or create a new one.
cmdUpdate_Click should look like this:

protected void cmdUpdate_Click(object sender, EventArgs e)
{
 try
 {
  ProductsController controller = new ProductsController();
  ProductsInfo productItem = new ProductsInfo()
  {
   ModuleId = ModuleId,
   ItemId = ItemId,
   Name = Name.Text,
   Description = Description.RichText.Text
  };
  if (Null.IsNull(ItemId))
  {
   controller.AddProduct(productItem);
  }
  else
  {
   controller.UpdateProduct(productItem);
  }
  Response.Redirect(Globals.NavigateURL(), true);
 }
 catch (Exception exc)
 {
  Exceptions.ProcessModuleLoadException(this, exc);
 }
}

One thing to note about the code above is how we pull the content from the Description TextEditor control. If you use Description.Text you will get the HTML encoded content (“<” is converted to “&lt;” and so on) which is not what you want in this case. Use Description.RichText.Text and you will get the raw markup.

The last event handler is cmdDelete_Click. If we have an ItemId then we delete the corresponding product item from the database. Simple as that. cmdDelete_Click should look like this:

protected void cmdDelete_Click(object sender, EventArgs e)
{
 try
 {
  if (!Null.IsNull(ItemId))
  {
   ProductsController controller = new ProductsController();
   controller.DeleteProduct(ModuleId, ItemId);
  }
  Response.Redirect(Globals.NavigateURL(), true);
 }
 catch (Exception exc)
 {
  Exceptions.ProcessModuleLoadException(this, exc);
 }
}

 With that done, compile the project to make sure everything is ok. Then log in as host in your DotNetNuke website. Now we’re going to add our brand new edit control to the Products module. Go to Host > Module Definitions. Locate the Products row in the list and click the edit icon.

Products module definition row

Scroll down to the section Module Controls and click Add Module Control.

Product module controls

Make the following settings and click Update:

Edit control definition

Now we can at last create product items and edit them. Great! Try it out!
Go to the page where you added the Products module. Make sure you are in edit mode. Now click the Add Content link. This time it actually happens something interesting. You should now see the edit control with a text field for product name and a text editor for description. Enter some text in them and click Update. The product you just added should now appear in the product list.
Now click the edit (pencil) icon next to the product name. This will take you to the edit screen and let you edit the product item.
Play around with your nice new controls until next time, when we look at the settings control.

About the addict

Johan Seppäläinen lives in Uppsala, Sweden. He spends most of his days working as a systems architect/developer, specialized in solutions built on Microsoft platforms.
Occasionally there is time for some recreational coding, when he pursues optimal solutions and code zen, mainly in C#. When he is not writing in this blog, that is.