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.

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.