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.

DotNetNuke 5 module development tutorial - part 4: The view control

14. February 2010 20:46

To Visual Basic 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="C#" Inherits="SipidCode.Modules.Products.ViewProducts" AutoEventWireup="true" CodeBehind="ViewProducts.ascx.cs" %>
<asp:ListView ID="ProductList" runat="server" OnItemDataBound="ProductList_ItemDataBound">
 <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>

You might notice that the ListView tag has a green squiggly under it and that the tooltip tells us that it is not a known element. This is because the project defaults to target .NET 2.0. The ListView control appeared in version 3.5 of the .NET framework, so we need to set that for the project.

Open the project properties by right-clicking it in solution explorer and selecting Properties from the context menu. In the Application tab, select “.NET Framework 3.5” in the Target Framework drop-down. When selected, a messagebox appears telling us that the project has to be closed and reopened. Click “Yes” in the message box.
Once the project is reopened you will notice that you now have a web.config file in there. If you paid any attention at all in part 2, you know you don’t want that. Delete it now.

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

protected void Page_Load(object sender, System.EventArgs e)
{
 try
 {
  ProductsController controller = new ProductsController();
  List<ProductsInfo> products;
  // Get the product list.
  products = controller.GetProducts(ModuleId);
  // Bind the product list to the ListView control.
  ProductList.DataSource = products;
  ProductList.DataBind();
 }
 catch (Exception ex)
 {
  // Module failed to load
  Exceptions.ProcessModuleLoadException(this, ex);
 }
}

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

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);
  }
 }
}

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 ModuleActionCollection ModuleActions
{
 get
 {
  ModuleActionCollection Actions = new ModuleActionCollection();
  Actions.Add(GetNextActionID(), Localization.GetString(ModuleActionType.AddContent, this.LocalResourceFile),
     ModuleActionType.AddContent, "", "add.gif", EditUrl("EditItem"), false, DotNetNuke.Security.SecurityAccessLevel.Edit,
   true, false);
  return Actions;
 }
}

In Page_Load you might see a squiggly under the references to ProductList.

ProductList variable not defined

This is because the designer has not updated itself since we pasted the contents into ViewProducts.ascx. To force an update, open ViewProducts.ascx, make some small change like adding or deleting a space or something and save the file. This usually does the trick.

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.cs, 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.

DotNetNuke 5 module development tutorial - part 3: The data layer

5. February 2010 17:06

To Visual Basic 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.cs file in the Components folder.
As you see there is just an empty constructor and some old-style public properties. Delete all class members and make the class look like this:

public class ProductsInfo
{
    public int ModuleId { get; set; }
    public int ItemId { get; set; }
    public string Name { get; set; }
    public string Description { get; set; }
}

Clear and concise. We love auto-implemented properties, don't we?
With the data objects prepaired we need to get our data provider in the data access layer in order.
Open the file DataProvider.cs 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 abstract IDataReader GetProducts(int ModuleId);
public abstract IDataReader GetProduct(int ModuleId, int ItemId);
public abstract void AddProduct(int ModuleId, string Name, string Description);
public abstract void UpdateProduct(int ModuleId, int ItemId, string Name, string Description);
public abstract void DeleteProduct(int ModuleId, int ItemId);

Now open SqlDataProvider.cs 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 override IDataReader GetProducts(int ModuleId)
{
    return (IDataReader)SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetProducts"), ModuleId);
}

public override IDataReader GetProduct(int ModuleId, int ItemId)
{
    return (IDataReader)SqlHelper.ExecuteReader(ConnectionString, GetFullyQualifiedName("GetProduct"), ModuleId, ItemId);
}

public override void AddProduct(int ModuleId, string Name, string Description)
{
    SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("AddProduct"), ModuleId, Name, Description);
}

public override void UpdateProduct(int ModuleId, int ItemId, string Name, string Description)
{
    SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("UpdateProduct"), ModuleId, ItemId, Name, Description);
}

public override void DeleteProduct(int ModuleId, int ItemId)
{
    SqlHelper.ExecuteNonQuery(ConnectionString, GetFullyQualifiedName("DeleteProduct"), ModuleId, ItemId);
}

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.cs to look like this:

public List<ProductsInfo> GetProducts(int ModuleId)
{
    return CBO.FillCollection<ProductsInfo>(DataProvider.Instance().GetProducts(ModuleId));
}

public ProductsInfo GetProduct(int ModuleId, int ItemId)
{
    return (ProductsInfo)CBO.FillObject(DataProvider.Instance().GetProduct(ModuleId, ItemId), typeof(ProductsInfo));
}

public void AddProduct(ProductsInfo prod)
{
    if (prod.Name.Trim() != "")
    {
        DataProvider.Instance().AddProduct(prod.ModuleId, prod.Name, prod.Description);
    }
}

public void UpdateProduct(ProductsInfo prod)
{
    if (prod.Name.Trim() != "")
    {
        DataProvider.Instance().UpdateProduct(prod.ModuleId, prod.ItemId, prod.Name, prod.Description);
    }
}

public void DeleteProduct(int ModuleId, int ItemId)
{
    DataProvider.Instance().DeleteProduct(ModuleId, ItemId);
}

 

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

public class ProductsController : ISearchable, 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.

DotNetNuke 5 module development tutorial - part 2: Setting up the project

31. January 2010 23:05

To Visual Basic 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, select the Web project type. Then select "DotNetNuke Compiled Module" under "My Templates". 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 2008 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 a later 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 right-clicking the project node in solution explorer and selecting Properties in the context menu. On the Application tab to the left, set the Assembly name to "SipidCode.Modules.Products". Do the same with the Default namespace field.

Project settings screen

If you use Visual Studio 2010, you will get compilation errors:
"The type or namespace name 'DotNetNuke' could not be found (are you missing a using directive or an assembly reference?)". Thanks to Tim Haynes for pointing this out.
To solve this, set the project's target framework to 3.5 (instead of 2.0 as in the image above).

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 properties 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.

DotNetNuke 5 module development tutorial - part 1: The tools

28. January 2010 21:04

To Visual Basic version

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 I usually work with the following setup for my development machine:

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

In this tutorial series I will use version 5.2 of both DotNetNuke web application and Visual Studio Starter Kit.

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 it 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, but I will only use C# in this tutorial series. Sorry all VB lovers.

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 development, and the need for black magic

26. January 2010 22:02

I first started to work with DotNetNuke at the end of its 4th edition era. We had tried a couple of other systems after putting our in-house developed platform to rest at last. That was a really great system, which we (of course) were very fond of, but maintenence and development of new functionality simply was not cost effective enough. What finally landed us on DotNetNuke was its pretty large developer community and the number of modules available.
When the 5th version was released it brought a couple of welcome changes. Unfortunately the documentation part of the new version kind of slipped behind. There is pretty much docs describing the workings of the 4th version and older, but for the 5th there is really not much. In the forums you find people that really know what they're talking about, but it gets kind of scattered when you want to read up on something.

Tutorials for older versions of DNN are plentiful, but for some reason there doesn't seem to be very much for the 5th there either. I admit that the differences in how you do a lot of things in version 4 and 5 aren't very big. But if you're in need of a tutorial you are probably not very familiar with what you are doing, so even small differences can easily put you off track.

This documentation shortage makes it kind of slow to get started. To get to know the system you're left to the dark arts of code crawling, which, as we know, is only for us true code freaks...
I thought I might write up some tutorials in coming posts to see if there is anyone interested in reading them. Think I'll start off with modules, but who knows, I might change my mind...

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.