Click here to Skip to main content
Click here to Skip to main content

Building Websites with VB.NET - Chapter 7: Creating Custom Modules

, 3 May 2006
Rate this:
Please Sign up or sign in to vote.
In this chapter, we are going to walk you through creating a custom module for the CoffeeConnections portal.

Title Building Websites with VB.NET and DotNetNuke 3.0
Author Daniel N. Egan
Publisher Packt Publishing Ltd.
Published March 2005
ISBN 1904811272
Price USD 35.99
Pages 299

Introduction

In this chapter, we are going to walk you through creating a custom module for the CoffeeConnections portal. A custom module can consist of one or more custom web controls. The areas we will cover are:

  • Creating a private assembly project to build and debug your module
  • Creating View and Edit controls
  • Adding additional options to the module settings page
  • Implementing the IActionable, ISearchable, and IPortable interfaces
  • Using the Dual List Control
  • Creating a SQLDataProvider
  • Packaging your module
  • Uploading your module

Coffee Shop Listing Module Overview

One of the main attractions for the CoffeeConnections portal is that users will be able to search, by zip code, for coffee shops in their area. After searching, the users will be presented with the shops in their area. To allow the focus of this chapter to be on module development, we will present a simplified version of this control. We will not spend time on the ASP.NET controls used or validation of these controls, instead we will focus only on what is necessary to create your own custom modules.

Setting Up Your Project (Private Assembly)

The design environment we will be using is Visual Studio .NET 2003. The files used in DotNetNuke come pre-packaged as a VS.NET solution and it is the best way to create custom modules for DotNetNuke. Visual Studio will allow us to create private assemblies (PA) which will keep our custom module code separate from the DotNetNuke framework code.

A private assembly is an assembly (.dll or .exe) that will be deployed along with an application to be used in conjunction with that application. In our case, the main application is the DotNetNuke core framework. The private assembly will be a project that is added to the DotNetNuke solution (.sln). This will keep our module architecture separate from the DotNetNuke core architecture but will allow us to use Visual Studio to debug the module within the framework. Since building our modules in a PA allows us to have separation from the DotNetNuke core framework, upgrading to newer versions of DotNetNuke is a simple process.

Even though the DotNetNuke framework is built using VB.NET, you can create your module private assemblies using any .NET language. Since your module logic will be compiled to a .dll, you can code in the language you like.

The DotNetNuke project is divided into many different solutions enabling you to work on different parts of the project. We have already seen the HTTP Module solution and the Providers solutions. Since we want to look at the default modules that have been packaged with DotNetNuke we will be using the DotNetNuke.DesktopModules solution.

You can even create a new solution and add the DotNetNuke project to the new solution. You would then need to create a build support project to support your modules. We are using the DotNetNuke.DesktopModules solution so that you are able to look at the default modules for help in design process.

To set up your private assembly as part of the DotNetNuke.DesktopModules solution, take the following steps:

  1. Open up the DotNetNuke Visual Studio.NET solution file (C:\DotNetNuke\Solutions\DotNetNuke.DesktopModules\ DotNetNuke.DesktopModules.sln).
  2. In the Solution Explorer, right-click on the DotNetNuke solution (not the project) and select Add | New Project:

  3. In Project Types, make sure that Visual Basic Projects is highlighted and select Class Library as your project type. Our controls are going to run in the DotNetNuke virtual directory, so we do not want to create a web project. This would create an additional virtual directory that we do not need.
  4. Your project should reside under the C:\DotNetNuke\DesktopModules folder. Make sure to change the location to this folder.
  5. The name of your project should follow the following convention: CompanyName.ModuleName. This will help avoid name conflicts with other module developers. Ours is named EganEnterprises.CoffeeShopListing. You should end up with a new project added to the DotNetNuke solution.

    If you have installed URLScan, which is part of Microsoft's IIS Lockdown Tool, you will have problems with folders that contain a period (.). If this is the case, you can create your project using an underscore instead of a period. Refer to Microsoft for more information on the IIS Lockdown Tool.

  6. You need to modify a few properties to allow you to debug our project within the DotNetNuke solution:
    • In the Common Properties folder, under the General section remove the Root namespace. Our module will be running under the DotNetNuke namespace, so we do not want this to default to the name of our assembly.

    • Delete the Class1.vb file that was created with the project.
    • Right-click on our private assembly project and select Properties.
  7. In the Common Properties folder, under the Imports subsection, we want to add imports that will help us as we create our custom module. Enter each of the namespaces below into the namespace box and click on Add Import.
    • DotNetNuke
    • DotNetNuke.Common
    • DotNetNuke.Common.Utilities
    • DotNetNuke.Data
    • DotNetNuke.Entities.Users
    • DotNetNuke.Framework
    • DotNetNuke.Services.Exceptions
    • DotNetNuke.Services.Localization
    • DotNetNuke.UI

  8. Click OK to save your settings.

When we run a project as a private assembly in DotNetNuke, the DLL for the module will build into the DotNetNuke bin directory. This is where DotNetNuke will look for the assembly when it tries to load your module. To accomplish this, there is a project called BuildSupport inside each of the solutions. The BuildSupport project is responsible for taking the DLL that is created by your project and adding it to the DotNetNuke solution's bin folder.

To allow the BuildSupport project to add our DLL, we need to add a reference to our custom module project.

  1. Right-click on the reference folder located below the BuildSupport project and select Add Reference.

  2. Select the Projects tab.
  3. Double-click on the EganEnterprises.CoffeeShopListing project to place it in the Selected Components box.
  4. Click OK to add the reference.

Finally, we want to be able to use all of the objects available to us in DotNetNuke within our private assembly, so we need to add a reference to DotNetNuke in our project.

  1. Right-click on the reference folder located below the EganEnterprises.CoffeeShopListing private assembly project we just created and select Add Reference.
  2. Select the Projects tab.
  3. Double-click on the DotNetNuke project to place it in the Selected Components box.
  4. Click OK to add the reference.

Before moving on, we want to make sure that we can build the solution without any errors. We will be doing this at different stages in development to help us pinpoint any mistakes we make along the way.

After building the solution, you should see something similar to the following in your output window:

---------------------- Done ----------------------
    Build: 35 succeeded, 0 failed, 0 skipped

The number you have in succeeded may be different but make sure that there is a zero in failed. If there are any errors fix them before moving on.

Creating Controls Manually in Visual Studio

When using a Class Library project as a starting point for your private assembly, you cannot add a Web User Control to your project by selecting Add | New Item from the project menu. Because of this we will have to add our controls manually.

An optional way to create the user controls needed is to create a Web User Control inside the DotNetNuke project and then drag the control to your PA project to make modifications.

Creating the View Control

The View control is what a non-administrator sees when you add the module to your portal. In other words, this is the public interface for your module.

Let's walk through the steps needed to create this control.

  1. Making sure that your private assembly project is highlighted, select Add New Item from the Project menu.
  2. Select Text File from the list of available templates and change the name to ShopList.ascx.
  3. Click Open to create the file.

  4. Click on the HTML tab and add the following directive to the top of the page:
    <%@ Control language="vb" AutoEventWireup="false" 
                Inherits="EganEnterprises.CoffeeShopListing.ShopList" 
                CodeBehind="ShopList.ascx.vb"%>

    Directives can be located anywhere within the file, but it is standard practice to place them at the beginning of the file. This directive sets the language to VB.NET and specifies the class and code-behind file that we will inherit from.

  5. Click the save icon on the toolbar to save the page.
  6. In the Solution Explorer right-click on the ShopList.ascx file and select View Code.

This will create a code-behind file for the Web User Control that we just created. The code-behind file follows the format of a normal Web User Control that inherits from System.Web.UserControl. This control, though based on Web.UserControl, will instead inherit from a class in DotNetNuke. Change the code-behind file to look like the code that follows. Here is the code-behind page in its entirety minus the Web Form Designer Generated Code:

Imports DotNetNuke
Imports DotNetNuke.Security.Roles
Namespace EganEnterprises.CoffeeShopListing
    Public MustInherit Class ShopList
       Inherits Entities.Modules.PortalModuleBase
       Implements Entities.Modules.IActionable
       Implements Entities.Modules.IPortable
       Implements Entities.Modules.ISearchable

      Private Sub Page_Load(ByVal sender As System.Object, _ 
        ByVal e As System.EventArgs) Handles MyBase.Load
          'Put user code to initialize the page here
      End Sub
      
      Public ReadOnly Property ModuleActions() As _
       DotNetNuke.Entities.Modules.Actions.ModuleActionCollection _
       Implements DotNetNuke.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(), _
            False, _
            Security.SecurityAccessLevel.Edit, _
            True, _
            False)
          Return Actions
         End Get
      End Property

      Public Function ExportModule(ByVal ModuleID As Integer) _
             As String Implements _
         DotNetNuke.Entities.Modules.IPortable.ExportModule
         ' included as a stub only so that the core 
         'knows this module Implements Entities.Modules.IPortable
      End Function

      Public Sub ImportModule(ByVal ModuleID As Integer, _
             ByVal Content As String, _
             ByVal Version As String, _
             ByVal UserID As Integer) _
             Implements _
             DotNetNuke.Entities.Modules.IPortable.ImportModule
        ' included as a stub only so that the core 
        'knows this module Implements Entities.Modules.IPortable
      End Sub

      Public Function GetSearchItems( _
        ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
        As DotNetNuke.Services.Search.SearchItemInfoCollection _
        Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
          ' included as a stub only so that the core 
          'knows this module Implements Entities.Modules.IPortable
      End Function
    End Class
End Namespace

Let's break up the code listing above so that we can better understand what is happening in this section. The first thing that we do is add an Imports statement for DotNetNuke and DotNetNuke.Security.Roles so that we may access their methods without using the fully qualified names.

Imports DotNetNuke
Imports DotNetNuke.Security.Roles
Namespace EganEnterprises.CoffeeShopListing

Next, we add the namespace to the class and set it to inherit from Entities.Modules.PortalModuleBase. This is the base class for all module controls in DotNetNuke. Using the base class is what gives our controls consistency and implements the basic module behavior like the module menu and header. This class also gives us access to useful items such as User ID, Portal ID, and Module ID among others.

This section then finishes up by implementing three different interfaces. These interfaces allow us to add enhanced functionality to our module. We will only be implementing the IActionable interface in this file. The others will only be placed in this file to allow the framework to see, using reflection, whether the module implements the interfaces. The actual implementation for the other interfaces occurs in the controller class that we will create later.

Public MustInherit Class ShopList
       Inherits Entities.Modules.PortalModuleBase
       Implements Entities.Modules.IActionable
       Implements Entities.Modules.IPortable
       Implements Entities.Modules.ISearchable

Since we will be implementing the IActionable interface in this file, we will now look at the IActionable ModuleActions properties that need to be implemented.

The core framework creates certain menu items automatically. These include the movement, module settings, and so on. You can manually add functionality to the menu by implementing this interface.

To add an action menu item to the module actions menu, we need to create an instance of a ModuleActionCollection. This is done in the ModuleActions property declaration.

Public ReadOnly Property ModuleActions() As _
 DotNetNuke.Entities.Modules.Actions.ModuleActionCollection _
 Implements DotNetNuke.Entities.Modules.IActionable.ModuleActions
    Get
      Dim Actions As New _
          Entities.Modules.Actions.ModuleActionCollection

We then use the Add method of this object to add and item to the menu.

    Actions.Add(GetNextActionID, _
        Localization.GetString( _
        Entities.Modules.Actions.ModuleActionType.AddContent, _
        LocalResourceFile), _
        Entities.Modules.Actions.ModuleActionType.AddContent, _
        "", _
        "", _
        EditUrl(), _
        False, _
        Security.SecurityAccessLevel.Edit, _
        True, _
        False)
        Return Actions
  End Get
End Property

The parameters of the Actions.Add method are:

Parameter

Type

Description

ID

Integer

The GetNextActionID function (found in the ActionsBase.vb file) will retrieve the next available ID for your ModuleActionCollection. This works like an auto-increment field, adding one to the previous action ID.

Title

String

The title is what is displayed in the context menu form your module.

CmdName

String

If you want your menu item to call client-side code (JavaScript), then this is where you will place the name of the command. This is used for the delete action on the context menu. When the delete item is selected, a message asks you to confirm your choice before executing the command. For the menu items we are adding we will leave this blank.

CmdArg

String

This allows you to add additional arguments for the command.

Icon

String

This allows you to set a custom icon to appear next to your menu option.

URL

String

This is where the browser will be redirected to when your menu item is clicked. You can use a standard URL or use the EditURL function to direct it to another module. The EditURL function finds the module associated with your view module by looking at the key passed in. You will notice that the first example below passes in "Options" and the second one passes nothing. This is because the default key is "Edit". These keys are entered in the Module Definition. We will learn how to add these manually later.

ClientScript

String

As the name implies, this is where you would add the client-side script to be run when this item is selected. This is paired with the CmdName attribute above. We are leaving this blank for your actions.

UseActionEvent

Boolean

This determines if the user will receive notification when a script is being executed.

Secure

SecurityAccessLevel

This is an Enum that determines the access level for this menu item.

Visible

Boolean

Determines whether this item will be visible.

NewWindow

Boolean

Determines whether information will be presented in a new window.

You will notice that the second parameter of the Add method asks for a title. This is the text that will show up on the menu item you create. In our code you will notice that instead of using a string, we use the Localization.GetString method to get the text from a local resource file.

Actions.Add(GetNextActionID, _
    Localization.GetString( _
    Entities.Modules.Actions.ModuleActionType.AddContent, _
    LocalResourceFile), _
    Entities.Modules.Actions.ModuleActionType.AddContent, _
    "", _
    "", _
    EditUrl(), _
    False, _
    Security.SecurityAccessLevel.Edit, _
    True, _
    False)

Localization is one of the many things that DotNetNuke 3.0 has brought us. This allows you to set the language seen on most sections of your portal to the language of your choice. Localization is somewhat beyond the scope of this chapter, but we will at least implement it for the actions menu.

To add a localization file, we first need to create a folder to place it in. Right-click on the EganEnterprises.CoffeeShopListing project in the Solution Explorer and select Add | New Folder. Name the folder App_LocalResources. This is where we will place our localization file. To add the file, right-click on the App_LocalResources folder and select Add | Add New Item from the menu. Select Assembly Resource File from the options and name it ShopList.ascx.resx. Click on Open when you are done.

Under the name section add the resource key AddContent.Action and give it a value of Add Coffee Shop. The action menu we implemented using the IActionable interface earlier uses this key to place Add Coffee Shop on the context menu.

To learn more about how to implement localization in your DotNetNuke modules, please see the DotNetNuke Localization white paper (\DotNetNuke\Documentation\Public\DotNetNuke Localization.doc).

Now we can move on to the other interfaces. As we stated earlier, these interfaces only need us to add the shell of the implemented functions into this file. These will only be placed in this file to allow the framework to see, using reflection, if the module implements the interfaces. We will write the code to implement these interfaces in the CoffeeShopListingController class later.

Public Function ExportModule(ByVal ModuleID As Integer) _
 As String Implements _
 DotNetNuke.Entities.Modules.IPortable.ExportModule
    ' included as a stub only so that the core 
    'knows this module Implements Entities.Modules.IPortable
End Function

Public Sub ImportModule(ByVal ModuleID As Integer, _
 ByVal Content As String, _
 ByVal Version As String, _
 ByVal UserID As Integer) _
 Implements DotNetNuke.Entities.Modules.IPortable.ImportModule
    ' included as a stub only so that the core 
    'knows this module Implements Entities.Modules.IPortable
End Sub

Public Function GetSearchItems( _
 ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
 As DotNetNuke.Services.Search.SearchItemInfoCollection _
 Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
    ' included as a stub only so that the core 
    'knows this module Implements Entities.Modules.IPortable
End Function

That is all the code we need at this time to set up our view module. Open up the display portion of the control in Visual Studio, and by using Table | Insert | Table on Visual Studio's main menu, add an HTML table to the form. Add the following text to the table:

We add the table and text because we will be testing our modules to make sure that everything is in order before moving on the more advanced coding. Again, setting test points in your development allows you to pinpoint errors that may have been introduced into your code. Once we finish the setup for the Edit and Settings controls we will test the module to make sure we have not missed anything.

Module Edit Control

The Edit control is used by administrators to modify or change how your module functions. To set up the Edit control follow the steps we took to create the View control with the following exceptions:

  • Do not implement the IPortable, IActionable, and ISearchable interfaces. The context menu only works with the View control.
  • The control menu is used to navigate to the Edit control.
  • Change the text in the table to say EditShopList RowOne and EditShopList RowTwo.
  • Save the file as EditShopList.ascx.

Add the following in the HTML section:

<%@ Control language="vb" AutoEventWireup="false" 
            Inherits="EganEnterprises.CoffeeShopListing.EditShopList" 
            CodeBehind="EditShopList.ascx.vb"%>

and this to the code-behind page:

Imports DotNetNuke
Namespace EganEnterprises.CoffeeShopListing
    Public MustInherit Class EditShopList
        Inherits Entities.Modules.PortalModuleBase
        Private Sub Page_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
            'Put user code to initialize the page here
        End Sub
    End Class
End Namespace

Again, add an HTML table to your control. When viewing your control in design mode it should look like the figure below:

Module Settings Control

The DotNetNuke framework allows you to add customized settings to the Module Settings Page. To do this you need to implement a Settings control.

To set up the Settings control follow the steps we took to create the View control with the following exceptions.

  • Do not implement the IPortable, IActionable, and ISearchable interfaces.
  • Change the text in the table to say OptionModule RowOne and OptionModule RowTwo.
  • Save the file as Settings.ascx.

Add the following to the HTML section:

<%@ Control language="vb" AutoEventWireup="false" 
  Inherits="EganEnterprises.CoffeeShopListing.Settings" 
  CodeBehind="Settings.ascx.vb"%>

In the code-behind section it gets a little tricky. As opposed to the other two controls, this control inherits from ModuleSettingsBase instead of PortalModuleBase. This causes a problem in the Visual Studio designer when you attempt to view your form in design mode. The Visual Studio designer will show the following error.

This is because the ModuleSettingsBase has two abstract methods that we will need to implement: LoadSettings and UpdateSettings. So unless you want to design your control using only HTML, you will need to use the following workaround.

When you need to see this control in the designer, just comment out the Inherits ModuleSettingsBase declaration and both the public overrides methods (LoadSettings and UpdateSettings), and instead inherit from the PortalModuleBase. You can then drag and drop all the controls you would like to use from the toolbox and adjust them on your form. When you are happy with how it looks in the designer, simply switch over the Inherits statements. For now, the only code we need in the code-behind file for this control is the one below. We will add to this code once we have created the DAL (Data Access Layer).

Imports DotNetNuke
 Namespace EganEnterprises.CoffeeShopListing
    Public Class Settings
        Inherits Entities.Modules.ModuleSettingsBase
        'Inherits Entities.Modules.PortalModuleBase
        Private Sub Page_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load
            'Put user code to initialize the page here
        End Sub
        Public Overrides Sub LoadSettings()
        End Sub
        Public Overrides Sub UpdateSettings()
        End Sub
    End Class
End Namespace

Just like the other controls, add an HTML table to the control so we can test our modules to this point.

With all your controls complete, build your project and verify that it builds successfully. At this point, the module still cannot be viewed in a browser within the DotNetNuke framework. To do this, you will first need to add module definitions to the portal.

Adding Module Definitions

When you upload a free or purchased module to your portal by using the host's file manager, the module definitions are added for you automatically. When developing modules, you will want to be able to debug them in the DotNetNuke environment using Visual Studio. This requires you to add module definitions manually.

Adding module definitions makes the module appear in the control panel module dropdown when you are signed on as host or admin. It connects your controls to the portal framework. To add the module definitions needed for our project:

  1. Hit F5 to run the DotNetNuke solution, log in as host, and click on the Module Definitions option on the Host menu.
  2. Under the Module Definition menu, select Add New Module Definition:

  3. Enter the name for your module and a short description of what it does. When you are finished, click on the Update link:

  4. This will bring up a new section that allows you to add the definitions for the module. Enter the New Definition name and click on Add Definition. This will add the definition to the Definitions dropdown and will bring up a third section that will allow you to add the controls created in the previous section:

First, we will add the View control for the module.

  1. Click on the Add Control link to start.

  2. Enter the Title for the control. This is the default title when the control is added to a tab.
  3. Select the Source for the control from the drop-down list. You will be selecting the file name of our control. This is the View control we created in the last section. Select the control from the dropdown.
  4. Select the Type of control. This is the control that non-administrators will see when they view your module on the portal. Select View from the dropdown.
  5. Click Update when done.

Next we want to add our Edit control.

  1. Enter Edit for the Key field. This is the key that the Actions Menu we created earlier will use to navigate to this control.
  2. Enter a Title for the control.
  3. Select the ShopListEdit.ascx control from the Source drop-down list.
  4. Select Edit as in the Type dropdown.
  5. Click Update when complete.

Finally we need to add our Settings control.

  1. Click on Add Control to add the third control for this module.

  2. Enter Settings for the key field.
  3. Enter a Title for the control.
  4. Select the Settings.ascx control from the Source drop-down list.
  5. Select Edit as in the Type dropdown.
  6. Click Update when complete.

This will complete the module definition. Your control page will look like the following:

Click on the Home page menu item to exit the module definition section.

Adding Your Module to a Page

The last step before adding the real functionality to our module is to add the module to a page. I prefer to add a Testing Tab to the portal to test out my new modules. We add the modules to the site before adding any functionality to them to verify that we have set them up correctly. We'll do this in stages so that you can easily determine any errors you encountered, by ensuring each stage of development was completed successfully.

Create a tab called Testing Tab and select EganEnterprises ShopList (or the name you used) from the Module drop-down list on the control panel and click on the Add link to add it to a pane on the page.

If all goes well you should see the module we created on the page. Verify that you can access the custom menu items from the context menu. When selected, they should bring you to the Edit and Settings controls that we created earlier.

For your Module Settings section to appear correctly in the module settings page, make sure that you have it inheriting from ModuleSettingsBase, and not PortalModuleBase.

We now have a basic template for creating our module. Before we can give our controls the functionality they need we need to construct our data layers.

The Datastore Layer

The datastore layer consists of the table(s) needed to store our records and the stored procedures required to access them. We begin by creating our tables and stored procedures for SQL Server.

SQL Server

First, we need to create the tables needed to hold our coffee shop information. When naming your tables and stored procedures it is a good idea to prefix them with the name of your company (CompanyName_). This is done for two reasons:

  1. It helps to avoid your module overriding a table of the same name. Simple table names like options or tasks turn into EganEnterprises_options or EganEnterprises_tasks. The chances of another developer creating a table with the same name are low.
  2. Inside SQL Server Enterprise Manager, all of your tables and stored procedures are grouped together, making them easy to locate and work with.

Since we will be using Microsoft SQL Server, we will be displaying our table and stored procedure information in script format. The first thing we need to do is to create the table that will hold our coffee shop information. This is the specific information we want to collect about each coffee shop that we will store.

CREATE TABLE [EganEnterprises_CoffeeShopInfo] (
   [coffeeShopID] [int] IDENTITY (1, 1) NOT NULL ,
   [moduleID] [int] NOT NULL ,
   [coffeeShopName] [varchar] (100)  NOT NULL ,
   [coffeeShopAddress1] [varchar] (150)  NULL ,
   [coffeeShopAddress2] [varchar] (150)  NULL ,
   [coffeeShopCity] [varchar] (50)  NOT NULL ,
   [coffeeShopState] [char] (2)  NOT NULL ,
   [coffeeShopZip] [char] (11)  NOT NULL ,
   [coffeeShopWiFi] [smallint] NOT NULL ,
   [coffeeShopDetails] [varchar] (250)  NOT NULL 
) ON [PRIMARY]
GO

Next, we need to create a table that will hold our module option information. This simple table has only two fields, moduleID and AuthorizedRoles, and will be used to handle the customized security we will be using with our module. This information will be accessed through the Settings control we created and will be seen on the module settings page.

CREATE TABLE [EganEnterprises_CoffeeShopModuleOptions] (
   [moduleID] [int] NOT NULL ,
   [AuthorizedRoles] [varchar] (200) NOT NULL 
) ON [PRIMARY]
GO

When we create scripts that will be used to create the database tables automatically when the PA is uploaded to a site, we will be prefixing the scripts with the databaseOwner and objectQualifier variables as follows:

CREATE TABLE databaseOwer {databaseOwner}
       {objectQualifier} [EganEnterprises_CoffeeShopInfo]

In this chapter, we are assuming that you use the SQL Server tools to create your database objects. If you are running these scripts from the SQL option on the host menu, you can add these variables to the script before you run them. Make sure that you have the Run As Script option checked.

We then need to create the stored procedures necessary to access our tables. Even though we can create stored procedures that combine functions like adding and updating records in the same stored procedure, we separate these out to make them easier to read and understand.

The following procedure adds new entries to our coffee shop listings:

CREATE PROCEDURE dbo.EganEnterprises_AddCoffeeShopInfo
@moduleID int,
@coffeeShopName                varchar(100)  ,
@coffeeShopAddress1            varchar(150),
@coffeeShopAddress2            varchar(150),
@coffeeShopCity                varchar(50) ,
@coffeeShopState               char(2),
@coffeeShopZip                 char(11),
@coffeeShopWiFi                int,
@coffeeShopDetails             varchar(250)         
AS
INSERT INTO EganEnterprises_CoffeeShopInfo (
  moduleID, 
  coffeeShopName,
  coffeeShopAddress1,
  coffeeShopAddress2,
  coffeeShopCity,
  coffeeShopState,
  coffeeShopZip,
  coffeeShopWiFi,
  coffeeShopDetails
)
VALUES (
  @moduleID,
  @coffeeShopName,
  @coffeeShopAddress1,
  @coffeeShopAddress2,
  @coffeeShopCity,
  @coffeeShopState,
  @coffeeShopZip,
  @coffeeShopWiFi,
  @coffeeShopDetails
)

The following procedure adds roles to the CoffeeShopModuleOptions table:

CREATE PROCEDURE dbo.EganEnterprises_AddCoffeeShopModuleOptions
@moduleID                  int,
@authorizedRoles varchar(250)


AS
INSERT INTO  EganEnterprises_CoffeeShopModuleOptions
  (moduleId, AuthorizedRoles) 
VALUES
  (@moduleID, @authorizedRoles)

The following procedure deletes a shop listing:

CREATE PROCEDURE dbo.EganEnterprises_DeleteCoffeeShop
@coffeeShopID  int 
AS
DELETE 
FROM   EganEnterprises_CoffeeShopInfo
WHERE   coffeeShopID = @coffeeShopID

The following procedure retrieves the users authorized to add shops:

CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShopModuleOptions
@moduleId int
AS
SELECT  *
FROM    EganEnterprises_CoffeeShopModuleOptions
WHERE
       moduleID = @moduleID

The following procedure retrieves all coffee shops:

CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShops
@moduleId int
AS
SELECT coffeeShopID,
       coffeeShopName,
       coffeeShopAddress1,
       coffeeShopAddress2,
       coffeeShopCity,
       coffeeShopState,
       coffeeShopZip,
       coffeeShopWiFi,
       coffeeShopDetails
FROM   EganEnterprises_CoffeeShopInfo
WHERE
       moduleID = @moduleID

The following procedure retrieves one shop for editing:

CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShopsByID
@coffeeShopID int
AS
SELECT coffeeShopID,
       coffeeShopName,
       coffeeShopAddress1,
       coffeeShopAddress2,
       coffeeShopCity,
       coffeeShopState,
       coffeeShopZip,
       coffeeShopWiFi,
       coffeeShopDetails
FROM   EganEnterprises_CoffeeShopInfo
WHERE  
      coffeeShopID = @coffeeShopID

The following procedure retrieves shops by zip code:

CREATE PROCEDURE dbo.EganEnterprises_GetCoffeeShopsByZip
@moduleID int,
@coffeeShopZip         char(11)
AS
SELECT coffeeShopID,
       coffeeShopName,
       coffeeShopAddress1,
       coffeeShopAddress2,
       coffeeShopCity,
       coffeeShopState,
       coffeeShopZip,
       coffeeShopWiFi,
       coffeeShopDetails
FROM   EganEnterprises_CoffeeShopInfo
WHERE
      coffeeShopZip = @coffeeShopZip AND moduleID = @moduleID

The following procedure updates a coffee shop listing:

CREATE PROCEDURE dbo.EganEnterprises_UpdateCoffeeShopInfo
@coffeeShopID                  int,   
@coffeeShopName                varchar(100),
@coffeeShopAddress1            varchar(150),
@coffeeShopAddress2            varchar(150),
@coffeeShopCity                varchar(50),
@coffeeShopState               char(2),
@coffeeShopZip                 char(11),
@coffeeShopWiFi                int ,
@coffeeShopDetails             varchar(250)
AS
UPDATE EganEnterprises_CoffeeShopInfo
SET    coffeeShopName = isnull(@coffeeShopName,coffeeShopName),
       coffeeShopAddress1 = isnull(@coffeeShopAddress1,
       coffeeShopAddress1),
       coffeeShopAddress2 = isnull(@coffeeShopAddress2,
       coffeeShopAddress2),
       coffeeShopCity = isnull(@coffeeShopCity,coffeeShopCity),
       coffeeShopState = isnull(@coffeeShopState,coffeeShopState),
       coffeeShopZip = isnull(@coffeeShopZip,coffeeShopZip),
       coffeeShopWiFi = isnull(@coffeeShopWiFi,coffeeShopWiFi),
       coffeeShopDetails = isnull(@coffeeShopDetails,
       coffeeShopDetails)
WHERE  coffeeShopID = @coffeeShopID

The following procedure updates who can add coffee shop listings:

CREATE PROCEDURE dbo.EganEnterprises_UpdateCoffeeShopModuleOptions
@moduleID        int,
@authorizedRoles varchar(250)
AS
UPDATE EganEnterprises_CoffeeShopModuleOptions
SET    AuthorizedRoles = @AuthorizedRoles
WHERE  moduleID = @moduleID

The Data Access Layer (DAL)

The provider model that DotNetNuke uses allows you to connect to the database of your choice. It is designed so that switching the datastore used by both the core and the modules can be done by simply changing the default provider. The DAL is where we place the code necessary for each provider we wish to support.

Before building our DAL, we need to create a few folders to organize our project. Right-click on your PA project and select Add Folder. Create two new folders in addition to the App_LocalResources folder created earlier: Providers and Installation.

The Providers folder will be used to hold the provider that we are going to create, and the Installation folder will be used to organize our installation files when we get to that section.

To begin building the DAL for our module, right-click on the EganEnterprises.CoffeeShopListing project and select Add Class. Name the class DataProvider.vb. This is the base provider class that will be used for the module. We will walk through and discuss each section of this file.

The first thing we need to do is to add a few import statements that we need for our class. We will be using both caching and reflection in our provider:

Imports System
Imports DotNetNuke

Just as we did for our controls, we want to place this class inside our CompanyName.ModuleName namespace:

Namespace EganEnterprises.CoffeeShopListing

This class will be used as the base class for our provider so we declare this as MustInherit. This means we will not be able to instantiate this class; it can only be used as the base class for our provider:

Public MustInherit Class DataProvider

Next, we need to declare the object that will serve as the singleton object for this class:

Private Shared objProvider As DataProvider = Nothing

We use a singleton object to ensure that only one instance of the data provider is created at any given time. The constructor is used to instantiate the object. In the constructor, we call the CreateProvider method to ensure that only one instance is created.

Shared Sub New()
    CreateProvider()
End Sub

The CreateProvider method uses reflection to create an instance of the data provider being created. We pass it the provider type, the namespace, and the assembly name.

Private Shared Sub CreateProvider()
    objProvider = _
     CType(Framework.Reflection.CreateObject _
     ("data", "EganEnterprises.CoffeeShopListing", _
     "EganEnterprises.CoffeeShopListing"), DataProvider)
End Sub

Finally, the Instance method is used to actually create the instance of our data provider.

Public Shared Shadows Function Instance() As DataProvider
    Return objProvider
End Function

At the bottom of the DataProvider class, we need to define all the abstract methods that will correspond to the stored procedures we have already created. The methods are created as MustOverride because we will need to implement them in our provider object.

Since the provider module allows any datastore to be used, the implementation of these methods will reside in the provider. Here we will only create the signature of the methods. The parameter names match those in our stored procedures (minus the @). As you can see, when implemented, these methods will be responsible for all the inserts, updates, and deletions for or module tables.

' all core methods defined below
Public MustOverride Function EganEnterprises_GetCoffeeShops _ 
   (ByVal ModuleId As Integer) As IDataReader
Public MustOverride Function EganEnterprises_GetCoffeeShopsByZip _ 
   (ByVal ModuleId As Integer, ByVal coffeeShopZip As String) _ 
    As IDataReader
Public MustOverride Function EganEnterprises_GetCoffeeShopsByID _ 
   (ByVal coffeeShopID As Integer) As IDataReader
Public MustOverride Function EganEnterprises_AddCoffeeShopInfo _ 
   (ByVal ModuleId As Integer, _ 
    ByVal coffeeShopName As String, _ 
    ByVal coffeeShopAddress1 As String, _ 
    ByVal coffeeShopAddress2 As String, _
    ByVal coffeeShopCity As String, _
    ByVal coffeeShopState As String, _
    ByVal coffeeShopZip As String, _
    ByVal coffeeShopWiFi As System.Int16, _ 
    ByVal coffeeShopDetails As String) As Integer
Public MustOverride Sub EganEnterprises_UpdateCoffeeShopInfo _ 
   (ByVal coffeeShopID As Integer, _ 
    ByVal coffeeShopName As String, _
    ByVal coffeeShopAddress1 As String, _
    ByVal coffeeShopAddress2 As String, _
    ByVal coffeeShopCity As String, _ 
    ByVal coffeeShopState As String, _ 
    ByVal coffeeShopZip As String, _
    ByVal coffeeShopWiFi As System.Int16, _ 
    ByVal coffeeShopDetails As String)
Public MustOverride Sub EganEnterprises_DeleteCoffeeShop _
   (ByVal coffeeShopID As Integer)
Public MustOverride Function _ 
    EganEnterprises_AddCoffeeShopModuleOptions _ 
   (ByVal ModuleID As Integer, _ 
    ByVal authorizedRoles As String) As Integer

We have a separate table that will hold the options for our module. The definitions for the options table are placed here.

'Options info
Public MustOverride Function _ 
  EganEnterprises_GetCoffeeShopModuleOptions _ 
  (ByVal ModuleID As Integer) As IDataReader

Public MustOverride Function _
 EganEnterprises_UpdateCoffeeShopModuleOptions _ 
 (ByVal ModuleID As Integer, _
  ByVal authorizedRoles As String) _
 As Integer

Public MustOverride Function _ 
  EganEnterprises_AddCoffeeShopModuleOptions _ 
  (ByVal ModuleID As Integer, _
   ByVal authorizedRoles As String) _
  As Integer
    
End Class
End Namespace

After this class is created, we need to create the SqlDataProvider project that our module will use.

The SQLDataProvider Project

The SqlDataProvider project is built as a separate private assembly. We will again be creating a Class Library type project. Name the project with a CompanyName.ModuleName.SqlProvider syntax (its location should be the Providers folder). In our case, the project will be called EganEnterprises.CoffeeShopListing.SqlProvider, and will be created in the C:\DotNetNuke\DesktopModules\EganEnterprises.CoffeeshopListing\Providers folder.

Just as we did for your module project, we will need to modify a few properties for the project. Right-click on the new project and select Properties. This will bring up the property pages.

Under the General section of the Common Properties folder, clear out the Root namespace.

We also want the project to build into the bin directory of the DotNetNuke project. This is where DotNetNuke will look for the assembly when it tries to load the provider. To allow the BuildSupport project to add our DLL, we need to add a reference to the SqlDataProvider project.

  1. Right-click on the reference folder located below the BuildSupport project and select Add Reference.
  2. Select the Projects tab.
  3. Double-click on the EganEnterprises.CoffeeShopListing.SqlDataProvider project to place it in the Selected Components box.
  4. Click OK to add the reference.

Finally, we want to be able to use all of the objects available to us in DotNetNuke in our private assembly, so we need to add a reference to the DotNetNuke project.

  1. Right-click on the reference folder located below the EganEnterprises.CoffeeShopListing.SqlDataProvider private assembly project we just created and select Add Reference.
  2. Select the Projects tab.
  3. Double-click on the DotNetNuke project to place it in the Selected Components box.
  4. Click OK to add the reference.

The Provider File

After you are finished setting up the project, it is time to create the SqlDataProvider class. First delete the Class1.vb file that was created with the project, then right-click on the project and select Add Class. Name the file SqlDataProvider.vb and click OK. This will provide you with the shell needed to create the provider. We will walk through the modifications needed to create the provider.

The first thing you need to do is to pull in a few imports. Most of these you should be quite used to seeing but the one that stands out is Microsoft.ApplicationBlocks.Data. This is a class created by Microsoft to help with the connections and commands needed to work with SQL Server. It is used to facilitate calls to the database without having to create all of the ADO.NET code manually. You will find this class in the C:\DotNetNuke\Providers\DataProviders\SqlDataProvider\SQLHelper folder of the DotNetNuke project. Take time to look it over; its methods are quite easy to understand. We will be using methods from this class in our data provider. To start, we add the imports we need for our class.

Imports System
Imports System.Data
Imports System.Data.SqlClient
Imports Microsoft.ApplicationBlocks.Data
Imports DotNetNuke
Imports DotNetNuke.Common.Utilities
Imports DotNetNuke.Framework.Providers

After adding the import statements, we need to wrap our class in the namespace for our module. As you can see, we will also be inheriting from the DataProvider base class created earlier. We also need to declare a constant variable that will hold the type of the provider. There are many different providers used in DotNetNuke so we need to specify the type. This is done by assigning it the simple lowercase string data.

Namespace EganEnterprises.CoffeeShopListing
    Public Class SqlDataProvider
        Inherits EganEnterprises.CoffeeShopListing.DataProvider
        Private Const ProviderType As String = "data"

We then use this type to instantiate a data provider configuration:

Private _providerConfiguration As _
        ProviderConfiguration = _ 
        ProviderConfiguration.GetProviderConfiguration _ 
        (ProviderType)

Then, we declare a few variables that will hold the information necessary for us to connect to the database:

        Private _connectionString As String
        Private _providerPath As String
        Private _objectQualifier As String
        Private _databaseOwner As String

In the constructor for the class we read the attributes that we set in the web.config file to fill the database specific information like connection string, database owner, etc.

Public Sub New()
  
   Dim objProvider As Provider = _ 
   CType(_providerConfiguration.Providers _ 
   (_providerConfiguration.DefaultProvider), _
      Provider)

   If objProvider.Attributes("connectionStringName") <> "" AndAlso _
    System.Configuration.ConfigurationSettings.AppSettings _
    (objProvider.Attributes("connectionStringName")) <> "" Then
      _connectionString = _
       System.Configuration.ConfigurationSettings.AppSettings _
       (objProvider.Attributes("connectionStringName"))
   Else
    _connectionString = _
    objProvider.Attributes("connectionString")
   End If
   
   _providerPath = objProvider.Attributes("providerPath")
   _objectQualifier = _ 
    objProvider.Attributes("objectQualifier")
   If _objectQualifier <> "" And _ 
       _objectQualifier.EndsWith("_") = False Then
       _objectQualifier += "_"
   End If

   _databaseOwner = objProvider.Attributes("databaseOwner")
   If _databaseOwner <> "" And _ 
     _databaseOwner.EndsWith(".") = False Then
        _databaseOwner += "."
   End If
End Sub

Public ReadOnly Property ConnectionString() As String
    Get
        Return _connectionString
    End Get
End Property

Public ReadOnly Property ProviderPath() As String
    Get
        Return _providerPath
    End Get
End Property

Public ReadOnly Property ObjectQualifier() As String
    Get
        Return _objectQualifier
    End Get
End Property
Public ReadOnly Property DatabaseOwner() As String
    Get
        Return _databaseOwner
    End Get
End Property

As you recall, in the base provider class we declared our methods as MustOverride. In this section, we are doing just that. We override the methods from the base class and use the Microsoft.ApplicationBlocks.Data class to make the calls to the database.

The GetNull function is used to convert an application-encoded null value to a database null value that is defined for the datatype expected. We will be utilizing this throughout the rest of this section.

' general
Private Function GetNull(ByVal Field As Object) As Object
    Return Null.GetNull(Field, DBNull.Value)
End Function

Public Overrides Function EganEnterprises_GetCoffeeShops( _
   ByVal ModuleId As Integer) _
   As IDataReader
    Return CType(SqlHelper.ExecuteReader(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_GetCoffeeShops", _
       ModuleId), _
       IDataReader)
End Function

Public Overrides Function EganEnterprises_GetCoffeeShopsByZip( _
   ByVal ModuleId As Integer, _
   ByVal coffeeShopZip As String) _
   As IDataReader
    Return CType(SqlHelper.ExecuteReader(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_GetCoffeeShopsByZip", _
       ModuleId, _
       coffeeShopZip), _
       IDataReader)
End Function

Public Overrides Function EganEnterprises_GetCoffeeShopsByID( _
   ByVal coffeeShopID As Integer) _
   As IDataReader
    Return CType(SqlHelper.ExecuteReader(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_GetCoffeeShopsByID", _
       coffeeShopID), _
       IDataReader)
End Function

Public Overrides Function EganEnterprises_AddCoffeeShopInfo( _
   ByVal ModuleId As Integer, _
   ByVal coffeeShopName As String, _
   ByVal coffeeShopAddress1 As String, _
   ByVal coffeeShopAddress2 As String, _
   ByVal coffeeShopCity As String, _
   ByVal coffeeShopState As String, _
   ByVal coffeeShopZip As String, _
   ByVal coffeeShopWiFi As System.Int16, _
   ByVal coffeeShopDetails As String) _
   As Integer
    Return CType(SqlHelper.ExecuteScalar(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_AddCoffeeShopInfo", _
       ModuleId, _
       coffeeShopName, _
       GetNull(coffeeShopAddress1), _
       GetNull(coffeeShopAddress2), _
       coffeeShopCity, _
       coffeeShopState, _
       coffeeShopZip, _
       coffeeShopWiFi, _
       coffeeShopDetails), _
       Integer)
End Function

Public Overrides Sub EganEnterprises_UpdateCoffeeShopInfo( _
   ByVal coffeeShopID As Integer, _
   ByVal coffeeShopName As String, _
   ByVal coffeeShopAddress1 As String, _
   ByVal coffeeShopAddress2 As String, _
   ByVal coffeeShopCity As String, _
   ByVal coffeeShopState As String, _
   ByVal coffeeShopZip As String, _ 
   ByVal coffeeShopWiFi As System.Int16, _
   ByVal coffeeShopDetails As String)
    SqlHelper.ExecuteNonQuery(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_UpdateCoffeeShopInfo", _
       coffeeShopID, _
       coffeeShopName, _
       GetNull(coffeeShopAddress1), _
       GetNull(coffeeShopAddress2), _
       coffeeShopCity, _
       coffeeShopState, _
       coffeeShopZip, _
       coffeeShopWiFi, _
       coffeeShopDetails)
End Sub

Public Overrides Sub EganEnterprises_DeleteCoffeeShop( _
   ByVal coffeeShopID As Integer)
    SqlHelper.ExecuteNonQuery(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_DeleteCoffeeShop", _
       coffeeShopID)
End Sub

Public Overrides Function EganEnterprises_GetCoffeeShopModuleOptions( _
   ByVal ModuleId As Integer) _
   As IDataReader
    Return CType(SqlHelper.ExecuteReader(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_GetCoffeeShopModuleOptions", _
       ModuleId), _
       IDataReader)
End Function

Public Overrides Function EganEnterprises_UpdateCoffeeShopModuleOptions( _
   ByVal ModuleID As Integer, _
   ByVal AuthorizedRoles As String) _
   As Integer
    Return CType(SqlHelper.ExecuteNonQuery(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_UpdateCoffeeShopModuleOptions", _
       ModuleID, _
       AuthorizedRoles), _
       Integer)
End Function

Public Overrides Function EganEnterprises_AddCoffeeShopModuleOptions( _
   ByVal ModuleID As Integer, _
   ByVal AuthorizedRoles As String) _
   As Integer
    Return CType(SqlHelper.ExecuteNonQuery(ConnectionString, _
       DatabaseOwner & _
       ObjectQualifier & _
       "EganEnterprises_AddCoffeeShopModuleOptions", _

 
       ModuleID, _
       AuthorizedRoles), _
       Integer)
End Function

End Class
End Namespace

The Business Logic Layer (BLL)

The third piece in this provider puzzle is the Business Logic Layer (BLL). The BLL connects the data-access sections we just completed with the presentation layer. Since we will have a Settings control, we will need to create four different classes:

  • CoffeeShopListingInfo
  • CoffeeShopListingController
  • CoffeeShopListingOptionsInfo
  • CoffeeShopListingOptionsController

CoffeeShopListingInfo and CoffeeShopListingOptionsInfo

The CoffeeShopListingInfo and CoffeeShopListingOptionsInfo classes are very simple classes that hold the information we need to pass to our database layer. These are used to pass hydrated objects instead of individual pieces of information. Each class will hold all the information associated with each object.

We start by adding our Imports statements and Namespace declarations.

Imports System
Imports System.Configuration
Imports System.Data
Namespace EganEnterprises.CoffeeShopListing

The next region of the code consists of private variables to hold the data and public properties to allow the setting and getting of the variables. Both classes are shown below:

Public Class CoffeeShopListingInfo
#Region "Private Members"
        Private m_moduleID As Integer
        Private m_coffeeShopID As Integer
        Private m_coffeeShopName As String
        Private m_coffeeShopAddress1 As String
        Private m_coffeeShopAddress2 As String
        Private m_coffeeShopCity As String
        Private m_coffeeShopState As String
        Private m_coffeeShopZip As String
        Private m_coffeeShopWiFi As System.Int16
        Private m_coffeeShopDetails As String
#End Region
#Region "Constructors"
    Public Sub New()
    End Sub
#End Region

#Region "Public Properties"
        Public Property moduleID() As Integer
            Get
                Return m_moduleID
            End Get
            Set(ByVal Value As Integer)
                m_moduleID = Value
            End Set
        End Property
        Public Property coffeeShopID() As Integer
            Get
                Return m_coffeeShopID
            End Get
            Set(ByVal Value As Integer)
                m_coffeeShopID = Value
            End Set
        End Property
        Public Property coffeeShopName() As String
            Get
                Return m_coffeeShopName
            End Get
            Set(ByVal Value As String)
                m_coffeeShopName = Value
            End Set
        End Property
        Public Property coffeeShopAddress1() As String
            Get
                Return m_coffeeShopAddress1
            End Get
            Set(ByVal Value As String)
                m_coffeeShopAddress1 = Value
            End Set
        End Property
        Public Property coffeeShopAddress2() As String
            Get
                Return m_coffeeShopAddress2
            End Get
            Set(ByVal Value As String)
                m_coffeeShopAddress2 = Value
            End Set
        End Property
        Public Property coffeeShopCity() As String
            Get
                Return m_coffeeShopCity
            End Get
            Set(ByVal Value As String)
                m_coffeeShopCity = Value
            End Set
        End Property
        Public Property coffeeShopState() As String
            Get
                Return m_coffeeShopState
            End Get
            Set(ByVal Value As String)
                m_coffeeShopState = Value
            End Set
        End Property
        Public Property coffeeShopZip() As String
            Get
                Return m_coffeeShopZip
            End Get
            Set(ByVal Value As String)
                m_coffeeShopZip = Value
            End Set
        End Property
        Public Property coffeeShopWiFi() As System.Int16
            Get
                Return m_coffeeShopWiFi
            End Get
            Set(ByVal Value As System.Int16)
                m_coffeeShopWiFi = Value
            End Set
        End Property
        Public Property coffeeShopDetails() As String
            Get
                Return m_coffeeShopDetails
            End Get
            Set(ByVal Value As String)
                m_coffeeShopDetails = Value
            End Set
        End Property
#End Region
End Class

Namespace EganEnterprises.CoffeeShopListing

Public Class CoffeeShopListingOptionsInfo
        Private m_moduleID As Integer
        Private m_AuthorizedRoles As String
        Public Property moduleID() As Integer
            Get
                Return m_moduleID
            End Get
            Set(ByVal Value As Integer)
                m_moduleID = Value
            End Set
        End Property
        Public Property AuthorizedRoles() As String
            Get
                Return m_AuthorizedRoles
            End Get
            Set(ByVal Value As String)
                m_AuthorizedRoles = Value
            End Set
        End Property
End Class
End Namespace

Once these classes have been completed, we then create the controller classes. As the name suggests, these are in charge of controlling the data flow to our module.

CoffeeShopListingController and CoffeeShopListingOptionsController

The CoffeeShopListingController class is paired with the CoffeeShopListingInfo class and is used to pass the CoffeeShopListingInfo objects to the dataprovider. To help minimize the task of populating custom business objects from the data layer, the DotNetNuke core team has created a generic utility class to help hydrate your business objects, the CBO class. This class contains two public functions—one for hydrating a single object instance and one for hydrating a collection of objects.

For more information on custom business objects refer to DotNetNuke Data Access.doc under C:\DotNetNuke\Documentation\Public.

When looking at the classes CoffeeShopListingController and CoffeeShopListingOptionsController, there are a few things you'll notice:

  • For functions like EganEnterprises_AddCoffeeShopInfo, the parameters for module-specific information are not passed individually but as a CoffeeShopListingInfo object.
  • The functions used to hydrate your data are found in the CBO class. This class uses database-neutral objects to fill your data so that it can be passed to the database of your choice.
  • We will be implementing the ISearchable and IPortable interfaces.

First, we will look at the CoffeeShopListingController class. We begin by adding our namespace to the class.

Namespace EganEnterprises.CoffeeShopListing

This is followed by the actual class declaration and the declarations for the interfaces.

Public Class CoffeeShopListingController
       Implements Entities.Modules.ISearchable
       Implements Entities.Modules.IPortable

We will break our code up into two different regions. In the Public Methods region, we create the functions that will make our calls to the database. We use the CBO object that calls the implemented DataProvider methods. Notice that we pass the detailed information about the coffee shop in a CoffeeShopListInfo object and then the function breaks out all of the individual items needed to call the DataProvider methods.

#Region "Public Methods"
Public Function EganEnterprises_GetCoffeeShops( _
       ByVal ModuleId As Integer) As ArrayList
    Return CBO.FillCollection _
            (DataProvider.Instance(). _
            EganEnterprises_GetCoffeeShops _
            (ModuleId), GetType(CoffeeShopListingInfo))
End Function

Public Function EganEnterprises_GetCoffeeShopsByZip( _
       ByVal ModuleId As Integer, _
       ByVal coffeeShopZip As String) _
       As ArrayList

    Return CBO.FillCollection _
           (DataProvider.Instance(). _
           EganEnterprises_GetCoffeeShopsByZip _
           (ModuleId, coffeeShopZip), _
           GetType(CoffeeShopListingInfo))
End Function

Public Function EganEnterprises_GetCoffeeShopsByID( _
       ByVal coffeeShopID As Integer) As CoffeeShopListingInfo
    Return CType(CBO.FillObject _
          (EganEnterprises.CoffeeShopListing. _
           DataProvider.Instance(). _
           EganEnterprises_GetCoffeeShopsByID( _
           coffeeShopID), GetType(CoffeeShopListingInfo)), _
           CoffeeShopListingInfo)
End Function

Public Function EganEnterprises_AddCoffeeShopInfo( _
       ByVal objShopList As _
       EganEnterprises.CoffeeShopListing.CoffeeShopListingInfo) _
       As Integer
    Return CType(EganEnterprises.CoffeeShopListing. _
           DataProvider.Instance(). _
           EganEnterprises_AddCoffeeShopInfo( _
           objShopList.moduleID, _
           objShopList.coffeeShopName, _
           objShopList.coffeeShopAddress1, _
           objShopList.coffeeShopAddress2, _
           objShopList.coffeeShopCity, _
           objShopList.coffeeShopState, _
           objShopList.coffeeShopZip, _
           objShopList.coffeeShopWiFi, _
           objShopList.coffeeShopDetails), Integer)
End Function

Public Sub EganEnterprises_UpdateCoffeeShopInfo( _
       ByVal objShopList As _
       EganEnterprises.CoffeeShopListing.CoffeeShopListingInfo)
    EganEnterprises.CoffeeShopListing. _
            DataProvider.Instance(). _
            EganEnterprises_UpdateCoffeeShopInfo( _
            objShopList.coffeeShopID, _
            objShopList.coffeeShopName, _
            objShopList.coffeeShopAddress1, _
            objShopList.coffeeShopAddress2, _
            objShopList.coffeeShopCity, _
            objShopList.coffeeShopState, _
            objShopList.coffeeShopZip, _
            objShopList.coffeeShopWiFi, _
            objShopList.coffeeShopDetails)
End Sub

Public Sub EganEnterprises_DeleteCoffeeShop( _
       ByVal coffeeShopID As Integer)
    EganEnterprises.CoffeeShopListing. _
            DataProvider.Instance(). _
            EganEnterprises_DeleteCoffeeShop(coffeeShopID)
End Sub
#End Region

Remember that when we created our ShopList.ascx.vb file we only created the shells needed for our interfaces. In our controller class, we will be coding the implementation of these interfaces.

Implementing IPortable

The IPortable interface can be implemented to allow a user to transfer data from one module instance to another. This is accessed on the context menu of the module.

To use this interface, you will need to implement two different methods, ExportModule and ImportModule. The implementation of these methods will be slightly different depending on the data that is stored in the module. Since we will be holding information about certain coffee shops in our module this is the information we need to import and export. This is accomplished using the System.XML namespace built into .NET.

The ExportModule method uses our EganEnterprises_GetCoffeeShops stored procedure to build an ArrayList of CoffeeShopListingInfo objects. The objects are then converted to XML nodes and returned to the caller. We don't need to call the ExportModule function ourselves; the DotNetNuke framework takes this when the Export link is clicked, and the data is exported to a physical file.

Public Function ExportModule(ByVal ModuleID As Integer) _
       As String Implements _
       DotNetNuke.Entities.Modules.IPortable.ExportModule
  Dim strXML As String
  Dim arrCoffeeShops As ArrayList = _
  EganEnterprises_GetCoffeeShops(ModuleID)

 
  If arrCoffeeShops.Count <> 0 Then
    strXML += "<coffeeshops>"
    Dim objCoffeeShop As CoffeeShopListingInfo
    For Each objCoffeeShop In arrCoffeeShops
      strXML += "<coffeeshop>"
      strXML += "<name>" & _
      XMLEncode(objCoffeeShop.coffeeShopName) & "</name>"
      strXML += "<address1>" & _
      XMLEncode(objCoffeeShop.coffeeShopAddress1) & "</address1>"
      strXML += "<address2>" & _
      XMLEncode(objCoffeeShop.coffeeShopAddress2) & "</address2>"
      strXML += "<city>" & _
      XMLEncode(objCoffeeShop.coffeeShopCity) & "</city>"
      strXML += "<state>" & _
      XMLEncode(objCoffeeShop.coffeeShopState) & "</state>"
      strXML += "<zip>" & _
      XMLEncode(objCoffeeShop.coffeeShopZip.ToString) & "</zip>"
      strXML += "<wifi>" & _
      XMLEncode(objCoffeeShop.coffeeShopWiFi.ToString) & "</wifi>"
      strXML += "<details>" & _
      XMLEncode(objCoffeeShop.coffeeShopDetails) & "</details>"
      strXML += "</coffeeshop>"
    Next
    strXML += "</coffeeshops>"
  End If
  Return strXML
End Sub

The ImportModule method does just the opposite; it takes the XML file created by the ExportModule method and creates CoffeeShopListingInfo items. Then it uses the EganEnterprises_AddCoffeeShopInfo method to add them to the database, thus filling the module with transferred data.

Public Sub ImportModule(ByVal ModuleID As Integer, _
       ByVal Content As String, ByVal Version As String, _
       ByVal UserID As Integer) _
       Implements DotNetNuke.Entities.Modules.IPortable.ImportModule
  Dim xmlCoffeeShop As XmlNode
  Dim xmlCoffeeShops As XmlNode = _
  GetContent(Content, "coffeeshops")
  For Each xmlCoffeeShop In xmlCoffeeShops
    Dim objCoffeeShop As New CoffeeShopListingInfo
    objCoffeeShop.moduleID = ModuleID
    objCoffeeShop.coffeeShopName = _
    xmlCoffeeShop.Item("name").InnerText
    objCoffeeShop.coffeeShopAddress1 = _
    xmlCoffeeShop.Item("address1").InnerText
    objCoffeeShop.coffeeShopAddress2 = _
    xmlCoffeeShop.Item("address2").InnerText
    objCoffeeShop.coffeeShopCity = _
    xmlCoffeeShop.Item("city").InnerText
    objCoffeeShop.coffeeShopState = _
    xmlCoffeeShop.Item("state").InnerText
    objCoffeeShop.coffeeShopZip = _
    xmlCoffeeShop.Item("zip").InnerText
    objCoffeeShop.coffeeShopWiFi = _
    xmlCoffeeShop.Item("wifi").InnerText
    objCoffeeShop.coffeeShopDetails = _
    xmlCoffeeShop.Item("details").InnerText
    EganEnterprises_AddCoffeeShopInfo(objCoffeeShop)
  Next
End Sub

Implementing ISearchable

With DotNetNuke 3.0 came the ability to search the portal for content. To allow your modules to be searched, you need to implement the ISearchable interface. This interface has only one method you need to implement: GetSearchItems.

This method uses a SearchItemCollection, which can be found in the DotNetNuke.Services.Search namespace, to hold a list of the items available in the search. In our implementation, we use the EganEnterprises_GetCoffeeShops method to fill an ArrayList with the coffee shops in our database. We then use the objects returned to the ArrayList to add to a SearchItemInfo object. The constructor for this object is overloaded and it holds items like Title, Descrption, Author, and SearchKey. What you place in these properties depends on your data. For our coffee shop items we will be using coffeeShopName, coffeeShopID, and coffeeShopCity to fill the object.

Public Function GetSearchItems _
      (ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
       As DotNetNuke.Services.Search.SearchItemInfoCollection _
       Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
    Dim SearchItemCollection As New SearchItemInfoCollection
    Dim CoffeeShops As ArrayList = _
    EganEnterprises_GetCoffeeShops(ModInfo.ModuleID)
    Dim objCoffeeShop As Object
    For Each objCoffeeShop In CoffeeShops
        Dim SearchItem As SearchItemInfo
        With CType(objCoffeeShop, CoffeeShopListingInfo)
            SearchItem = New SearchItemInfo _
            (ModInfo.ModuleTitle & " - " & .coffeeShopName, _
            .coffeeShopName, _
            Convert.ToInt32(10), _
            DateTime.Now, ModInfo.ModuleID, _
            .coffeeShopID.ToString, _
            .coffeeShopName & " - " & .coffeeShopCity)
            SearchItemCollection.Add(SearchItem)
        End With
    Next
    Return SearchItemCollection
End Function

Each time it loops through the arraylist, it will add a search item to the SearchItemCollection. The core framework takes care of all the other things needed to implement this on your portal.

Since we only need to implement the interfaces for the CoffeeShopListingController class, the code for the CoffeeShopListingOptionsController class is much simpler.

Imports System
Imports System.Data
Imports DotNetNuke
Namespace EganEnterprises.CoffeeShopListing
    Public Class CoffeeShopListingOptionsController
       Public Function EganEnterprises_GetCoffeeShopModuleOptions( _
           ByVal ModuleId As Integer) _
           As ArrayList
            Return CBO.FillCollection(DataProvider.Instance(). _
            EganEnterprises_GetCoffeeShopModuleOptions(ModuleId), _
               GetType(CoffeeShopListingOptionsInfo))
       End Function
       Public Function EganEnterprises_UpdateCoffeeShopModuleOptions( _
           ByVal objShopListOptions As EganEnterprises. _
           CoffeeShopListing.CoffeeShopListingOptionsInfo) _
           As Integer
            Return CType(DataProvider.Instance(). _
            EganEnterprises_UpdateCoffeeShopModuleOptions( _
            objShopListOptions.moduleID, _
               objShopListOptions.AuthorizedRoles), _
               Integer)
       End Function
       Public Function EganEnterprises_AddCoffeeShopModuleOptions( _
           ByVal objShopListOptions As EganEnterprises. _
           CoffeeShopListing.CoffeeShopListingOptionsInfo) _
           As Integer
            Return CType(DataProvider.Instance(). _
            EganEnterprises_AddCoffeeShopModuleOptions( _
            objShopListOptions.moduleID, _
            objShopListOptions.AuthorizedRoles), Integer)
       End Function
    End Class
End Namespace

The Presentation Layer

We can now get back to the View, Edit, and Settings controls we created in our private assembly project. Now we can write code to interact with the data store.

ShopList.aspx

Our View control will consist of two panels, of which only one will be shown at any given moment. The first panel will be used to search and view coffee shops by zip code.

The second panel will allow users to add coffee shops to the database.

Since leaving fields blank when submitting this form could cause various runtime errors, in a real-world application it would be necessary to add validation to all user input. You could use ASP.NET validation controls to accomplish this task.

<%@ Control language="vb" AutoEventWireup="false" 
    Inherits="EganEnterprises.CoffeeShopListing.ShopList"
    CodeBehind="ShopList.ascx.vb"%>
<asp:Panel id="pnlGrid" runat="server">
    <TABLE id="Table1" cellSpacing="1" cellPadding="1" width="100%" 
     border="1">
       <TR>
          <TD>
              <P align="center">Enter Zip code
               <asp:TextBox id="txtZipSearch" runat="server">
               </asp:TextBox>
               <asp:LinkButton id="lbSearch" runat="server">Search 
                By Zip</asp:LinkButton></P>
          </TD>
       </TR>
       <TR>
          <TD>
              <P align="center">
               <asp:linkbutton id="lbAddNewShop" runat="server">
               Add New Shop</asp:linkbutton></P>
          </TD>
       </TR>
    </TABLE>
    <asp:datagrid id="dgShopLists" runat="server" Width="100%" 
    BorderWidth="2px" BorderColor="Blue" AutoGenerateColumns="False">
       <AlternatingItemStyle BackColor="Lavender">
       </AlternatingItemStyle>
       <HeaderStyle BackColor="Silver"></HeaderStyle>
       <Columns>
          <asp:TemplateColumn>
              <ItemTemplate>
                 <asp:HyperLink id=hlcoffeeShopID runat="server" 
                    Visible="<%# IsEditable %>" 
                    NavigateUrl='<%# EditURL("coffeeShopID",
                        DataBinder.Eval(Container.DataItem, 
                        "coffeeShopID")) %>'
                    ImageUrl="~/images/edit.gif">
                 </asp:HyperLink>
              </ItemTemplate>
          </asp:TemplateColumn>
          <asp:BoundColumn DataField="coffeeShopName" ReadOnly="True" 
            HeaderText="Coffee Shop Name"></asp:BoundColumn>
          <asp:BoundColumn DataField="coffeeShopAddress1" 
            ReadOnly="True" HeaderText="Address"></asp:BoundColumn>
          <asp:BoundColumn DataField="coffeeShopCity" ReadOnly="True" 
            HeaderText="City"></asp:BoundColumn>
          <asp:BoundColumn DataField="coffeeShopZip" ReadOnly="True" 
            HeaderText="Zip Code"></asp:BoundColumn>
       </Columns>
   </asp:datagrid>
</asp:Panel>


<asp:Panel id="pnlAdd" runat="server">
   <TABLE id="Table2" cellSpacing="1" cellPadding="1" 
     width="100%" border="1">
       <TR>
         <TD align="center" bgColor="lavender" colSpan="2">
           <STRONG><FONT color="#000000">Enter A New Coffee Shop
           </FONT></STRONG></TD>
       </TR>
       <TR>
          <TD>
            <P align="center">ShopName</P>
          </TD>
          <TD>
            <asp:textbox id="txtcoffeeShopName" runat="server">
            </asp:textbox></TD>
       </TR>
       <TR>
          <TD>
              <P align="center">Address1</P>
          </TD>
          <TD>
            <asp:textbox id="txtCoffeeShopAddress1" runat="server">
            </asp:textbox></TD>
       </TR>
       <TR>
          <TD>
              <P align="center">Address2</P>
          </TD>
          <TD>
            <asp:textbox id="txtCoffeeShopAddress2" runat="server">
            </asp:textbox></TD>
       </TR>
       <TR>
          <TD>
              <P align="center">City</P>
          </TD>
          <TD>
            <asp:textbox id="txtcoffeeShopCity" runat="server">
            </asp:textbox></TD>
       </TR>
       <TR>
          <TD>
            <P align="center">State</P>
          </TD>
          <TD>
            <asp:textbox id="txtcoffeeShopState" runat="server">
            </asp:textbox></TD>
       </TR>
       <TR>
          <TD>
            <P align="center">zip</P>
          </TD>
          <TD>
            <asp:textbox id="txtcoffeeShopZip" runat="server">
            </asp:textbox></TD>
       </TR>
       <TR>
          <TD height="31">


            <P align="center">WiFi Yes or No</P>
          </TD>
          <TD height="31">
            <asp:RadioButtonList id="rblWiFi" runat="server" 
                              RepeatDirection="Horizontal">
              <asp:ListItem Value="1">Yes</asp:ListItem>
              <asp:ListItem Value="0">No</asp:ListItem>
            </asp:RadioButtonList></TD>
       </TR>
       <TR>
          <TD>
            <P align="center">Extra Details</P>
          </TD>
          <TD>
            <asp:TextBox id="txtcoffeeShopDetails" runat="server">
            </asp:TextBox></TD>
       </TR>
       <TR>
          <TD>
            <P align="center">&nbsp;</P>
          </TD>
          <TD>
            <P>
              <asp:LinkButton id="cmdAdd" runat="server" 
                CssClass="CommandButton" BorderStyle="none" 
                Text="Update">Add</asp:LinkButton>
              <asp:LinkButton id="cmdCancel" runat="server" 
                CssClass="CommandButton" BorderStyle="none" 
                Text="Cancel" CausesValidation="False">
                </asp:LinkButton>
            </P>
          </TD>
       </TR>
   </TABLE>
</asp:Panel>

We are going to start our look into the code-behind file by looking at the code that is fired when the search button is clicked. This event, of course, expects a zip code to be placed in the textbox before it is executed.

The first line instantiates a CoffeeShopListingController object. This is the class we created in the last section that handles the interface to our data provider. Next we create an ArrayList to hold the data that is returned when we call the EganEnterprises_GetCoffeeShopsByZip function. This function takes only ModuleID and Zipcode as parameters. You notice that we just typed in ModuleID without ever declaring the variable. ModuleID is a variable that we inherit from the PortalModuleControl class. It will hold the unique ModuleID for this module. This will, of course fill our ArrayList, which we then bind to our DataGrid.

Private Sub lbSearch_Click( _
   ByVal sender As System.Object, _
   ByVal e As System.EventArgs)

    Dim objCoffeeShops As New CoffeeShopListingController
    Dim myList As ArrayList
 
    myList = _
    objCoffeeShops.EganEnterprises_GetCoffeeShopsByZip _
    (ModuleId, txtZipSearch.Text)
    Me.dgShopLists.DataSource = myList
    Me.dgShopLists.DataBind()
End Sub

The next method we will look at is the AddNewShop link button's Click event-handler. As we will see when we look at the Page_Load event, this button is only available to certain security roles. This button click simply redirects the page back to itself and adds a querystring to the end. Then NavigateURL function is used to work in conjunction with the URL rewriting.

Private Sub lbAddNewShop_Click( _
   ByVal sender As System.Object, _
   ByVal e As System.EventArgs)
    Response.Redirect(NavigateURL(TabId, "", "Add=YES"), True)
End Sub

Now we will look at the Page_Load method. The first thing the event looks for is whether or not the Add querystring exists. Based on this, the control will show either the panel with the DataGrid, or the panel with the form to allow users to add coffee shops to the list. If we are showing the grid, we will fill it using the same technique as we used in the search method.

Private Sub Page_Load(ByVal sender As System.Object, _
        ByVal e As System.EventArgs) Handles MyBase.Load

    'If we are not adding show the grid
    If (Request.Params("Add") Is Nothing) Then
        'Grid panel is visible
        pnlAdd.Visible = False
        pnlGrid.Visible = True
        'Then fill the grid
        If Not Page.IsPostBack Then
            Dim objCoffeeShops As New CoffeeShopListingController
            Dim myList As ArrayList
            myList = objCoffeeShops.EganEnterprises_GetCoffeeShops _
            (ModuleId)
            Me.dgShopLists.DataSource = myList
            Me.dgShopLists.DataBind()
        End If

We will now be looking at the security roles set up for the portal. We wanted to be able to tie into security roles to allow only certain users the ability to add a new coffee shop. We did not want to use the module settings because that would give the role the ability to modify more of the module than we want. We will be saving the security roles that can add a coffee shop into the options table we created earlier. This will be done on the ShopListOptions control. In this section we will be reading that table.

'Check roles to see if the user can add items to the listing
'String of roles for shoplist
Dim objShopRoles As New CoffeeShopListingOptionsController
Dim objShopRole As CoffeeShopListingOptionsInfo
Dim arrShopRoles As ArrayList = _
objShopRoles.EganEnterprises_GetCoffeeShopModuleOptions _
(ModuleId)
'Put roles into a string 
Dim shopRoles As String = ""
For Each objShopRole In arrShopRoles
    shopRoles = objShopRole.AuthorizedRoles.ToString
Next

We use the CoffeeShopListingOptionsController class we created to put roles that are authorized to add a coffee shop into a delimited string.

We then use the portal settings and the role controller to find the security roles the user possesses. These are placed in an array and compared against the roles allowed to add a coffee shop.

The RoleController class works similarly to the controller classes we created for our module.

Dim bAuth = False
If UserInfo.UserID <> -1 Then
    If UserInfo.IsSuperUser = True Then
        bAuth = True
    Else
        Dim objRoles As New RoleController
        Dim Roles As String() = objRoles.GetPortalRolesByUser _
        (UserInfo.UserID, PortalSettings.PortalId)
        Dim maxRows As Integer = UBound(Roles)
        Dim i As Integer
        For i = 0 To maxRows
            Dim objRoleInfo As RoleInfo
            objRoleInfo = objRoles.GetRoleByName(PortalId, Roles(i))
            If shopRoles.IndexOf(objRoleInfo.RoleID & ";") <> -1 Then
                bAuth = True
                Exit For
            End If
        Next
    End If
End If

If the user is authorized, the Add New Shop link button is visible.

If bAuth Then
    lbAddNewShop.Visible = True
Else
    lbAddNewShop.Visible = False
End If

If the Add querystring exists then we want to show the Add panel and hide the grid panel.

    Else ' If we are adding...
        'Add panel is visible
        pnlAdd.Visible = True
        pnlGrid.Visible = False
    End If
End Sub

The next section we will look at is the Add button click event. Remember that this is only visible if the user has the authority to add a coffee shop. This is what adds the information typed into the add coffee shop form.

The first thing we do is create an instance of our CoffeeShopListingInfo class and fill it with the information filled out in the textboxes:

Private Sub cmdAdd_Click( _
   ByVal sender As System.Object, _
   ByVal e As System.EventArgs)
    Dim objShopList As New CoffeeShopListingInfo
    With objShopList
        .moduleID = ModuleId
        .coffeeShopID = coffeeShopID
        .coffeeShopName = txtcoffeeShopName.Text
        .coffeeShopAddress1 = txtCoffeeShopAddress1.Text
        .coffeeShopAddress2 = txtCoffeeShopAddress2.Text
        .coffeeShopCity = txtcoffeeShopCity.Text
        .coffeeShopState = txtcoffeeShopState.Text
        .coffeeShopZip = txtcoffeeShopZip.Text
        .coffeeShopDetails = txtcoffeeShopDetails.Text
        .coffeeShopWiFi = rblWiFi.SelectedValue
    End With

We then create an instance of our controller class and pass the objShopList to the EganEnterprises_AddCoffeeShopInfo function. When complete we are redirected back to the grid view of the control.

      Dim objShopLists As New CoffeeShopListingController
      coffeeShopID = _ 
      objShopLists.EganEnterprises_AddCoffeeShopInfo(objShopList)
      ' Redirect back to the portal
      Response.Redirect(NavigateURL())
End Sub

We then finish up by adding redirect code to the Cancel button-click event.

Private Sub cmdCancel_Click( _
   ByVal sender As System.Object, _
   ByVal e As System.EventArgs)
    ' Redirect back to the portal 
    Response.Redirect(NavigateURL())
End Sub

EditShopList.ascx

The EditShopList control is designed similarly to the Add Coffee Shops form on the View control. The only difference is that administrators of the module are able to not only add new shops but also modify and delete them. The first thing we need to do is to build the form that the administrators will be working with.

Since leaving fields blank when submitting this form could cause various runtime errors, it would be necessary in a real-world application to add validation to all user input. You could use ASP.NET validation controls to accomplish this task.

<%@ Control language="vb" AutoEventWireup="false" 
    Inherits="EganEnterprises.CoffeeShopListing.EditShopList" 
    CodeBehind="EditShopList.ascx.vb"%>
<TABLE id="Table1" cellSpacing="1" cellPadding="1" width="100%" border="1">
   <TR>
       <TD>
          <P align="center">ShopName</P>
       </TD>
       <TD><asp:textbox id="txtcoffeeShopName" runat="server">
           </asp:textbox></TD>
   </TR>
   <TR>
       <TD>
          <P align="center">Address1</P>
       </TD>
       <TD><asp:textbox id="txtCoffeeShopAddress1" runat="server">
           </asp:textbox></TD>
   </TR>
   <TR>
       <TD>
          <P align="center">Address2</P>
       </TD>
       <TD><asp:textbox id="txtCoffeeShopAddress2" runat="server">
           </asp:textbox></TD>
   </TR>
   <TR>
       <TD>
          <P align="center">City</P>
       </TD>
       <TD><asp:textbox id="txtcoffeeShopCity" runat="server">
           </asp:textbox></TD>
   </TR>
   <TR>
       <TD>
          <P align="center">State</P>
       </TD>
       <TD><asp:textbox id="txtcoffeeShopState" runat="server">
           </asp:textbox></TD>
   </TR>
   <TR>
       <TD>
          <P align="center">zip</P>
       </TD>
       <TD><asp:textbox id="txtcoffeeShopZip" runat="server">
           </asp:textbox></TD>
   </TR>
   <TR>
       <TD height="31">
          <P align="center">WiFi Yes or No</P>
       </TD>
       <TD height="31">
          <asp:RadioButtonList id="rblWiFi" runat="server" 
                           RepeatDirection="Horizontal">
              <asp:ListItem Value="1">Yes</asp:ListItem>
              <asp:ListItem Value="0">No</asp:ListItem>
          </asp:RadioButtonList></TD>
   </TR>
   <TR>
       <TD>
          <P align="center">Extra Details</P>
       </TD>
       <TD>
          <asp:TextBox id="txtcoffeeShopDetails" runat="server">
           </asp:TextBox></TD>
   </TR>
   <TR>
       <TD>
          <P align="center">&mp;nbsp;</P>
       </TD>
       <TD>
          <P>
              <asp:LinkButton id="cmdUpdate" runat="server" 
                  Text="Update" BorderStyle="none" 
                  CssClass="CommandButton"></asp:LinkButton>


              <asp:LinkButton id="cmdCancel" runat="server" 
                  Text="Cancel" BorderStyle="none" 
                  CssClass="CommandButton"
                  CausesValidation="False"></asp:LinkButton>
              <asp:LinkButton id="cmdDelete" runat="server" 
                  Text="Delete" BorderStyle="none"
                  CssClass="CommandButton"
                  CausesValidation="False"></asp:LinkButton>
          </P>
       </TD>
   </TR>
</TABLE>

We are going to start our look into the code-behind file by seeing the code executed when the Page_Load event is fired. The first thing we do is check to see if there is a coffeeShopID in the querystring. This will be used to determine whether this is a update or a new record.

Imports DotNetNuke
Namespace EganEnterprises.CoffeeShopListing

Public MustInherit Class EditShopList
        Inherits Entities.Modules.PortalModuleBase

    Dim coffeeShopID As Integer = -1
    Private Sub Page_Load(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles MyBase.Load
        ' get parameter
        If Not (Request.Params("coffeeShopID") Is Nothing) Then
            coffeeShopID = _
                Integer.Parse(Request.Params("coffeeShopID"))
        Else
            coffeeShopID = Null.NullInteger
        End If

Then, if this is not a post back to the page, we add some JavaScript to the cmdDelete button that will make them confirm their action before a deletion takes place. Although this code is shown on the server-side, this action will be used client-side.

If Page.IsPostBack = False Then
    cmdDelete.Attributes.Add("onClick", _
       "javascript:return confirm('Are You" & _ 
       " Sure You Wish To Delete This Item ?');")

Next, we check the coffeeShopID value to determine whether it is an update or a new record. If coffeeShopID is not Null then it is an existing record.

If Not DotNetNuke.Common.Utilities.Null.IsNull(coffeeShopID) Then

Since the record exists, we need to create a CoffeeShopListingController and use it to obtain the information from the database. This information is loaded into a CoffeeShopListingInfo object and used to populate the textboxes located on the form.

If Not objCoffeeShop Is Nothing Then
    txtcoffeeShopName.Text = objCoffeeShop.coffeeShopName
    txtCoffeeShopAddress1.Text = objCoffeeShop.coffeeShopAddress1
    txtCoffeeShopAddress2.Text = objCoffeeShop.coffeeShopAddress2
    txtcoffeeShopCity.Text = objCoffeeShop.coffeeShopCity
    txtcoffeeShopState.Text = objCoffeeShop.coffeeShopState
    txtcoffeeShopZip.Text = objCoffeeShop.coffeeShopZip

    If objCoffeeShop.coffeeShopWiFi Then
        rblWiFi.Items(0).Selected = True
    Else
        rblWiFi.Items(1).Selected = True
    End If

    txtcoffeeShopDetails.Text = objCoffeeShop.coffeeShopDetails

Else
    ' If object has no data we want to go back
    Response.Redirect(NavigateURL())
End If

If this is a new record, then all we need to do is remove the Delete link from the form.

Else
    ' This is new item
    cmdDelete.Visible = False
End If

Once we have determined that it is a new record, we want to look at the code that is called when the Update button is clicked. Again, we will make use of both the CoffeeShopListingInfo object and the CoffeeShopListingController object. We fill the first one with the data found in the form, and use the last one to call the update or insert code.

Private Sub cmdUpdate_Click( _
   ByVal sender As System.Object, _
   ByVal e As System.EventArgs)

    Try
        Dim objShopList As New CoffeeShopListingInfo
        objShopList.moduleID = ModuleId
        objShopList.coffeeShopID = coffeeShopID
        objShopList.coffeeShopName = txtcoffeeShopName.Text
        objShopList.coffeeShopAddress1 = txtCoffeeShopAddress1.Text
        objShopList.coffeeShopAddress2 = txtCoffeeShopAddress2.Text
        objShopList.coffeeShopCity = txtcoffeeShopCity.Text
        objShopList.coffeeShopState = txtcoffeeShopState.Text
        objShopList.coffeeShopZip = txtcoffeeShopZip.Text
        objShopList.coffeeShopDetails = txtcoffeeShopDetails.Text
        objShopList.coffeeShopWiFi = rblWiFi.SelectedValue

        Dim objShopLists As New CoffeeShopListingController
        If Null.IsNull(coffeeShopID) Then
            coffeeShopID = _
       objShopLists.EganEnterprises_AddCoffeeShopInfo(objShopList)
        Else
       objShopLists.EganEnterprises_UpdateCoffeeShopInfo(objShopList)
        End If
        ' Redirect back to the portal
        Response.Redirect(NavigateURL())
    Catch ex As Exception
        ProcessModuleLoadException(Me, ex)
    End Try
End Sub

The final section that we will look at is called when the Delete button is clicked. This code uses the coffeeShopID and calls the EganEnterprises_DeleteCoffeeShop stored procedure.

Private Sub cmdDelete_Click( _
   ByVal sender As System.Object, _
   ByVal e As System.EventArgs)
    If Not Null.IsNull(coffeeShopID) Then
        Dim objShopLists As New CoffeeShopListingController
        objShopLists.EganEnterprises_DeleteCoffeeShop(coffeeShopID)
    End If
    ' Redirect back to the portal 
    Response.Redirect(NavigateURL())
End Sub

This completes our Edit control and leaves us with our Settings control.

Settings.ascx

The Settings control allows you to set additional properties for your module that will appear in the module settings page. We are currently only saving one property but it is a unique one. We want to be able to tie into the built-in security roles in DotNetNuke and use them to decide what users can add items with out giving them access to the context menu. To accomplish this we use the DualList control that is found in the DotNetNuke controls folder.

To be able to work with this control in design mode, you will first need to change the class to inherit from PortalModuleBase instead of ModuleSettingsBase. Make sure you changes this back when you are done or it will not work properly.

We will be adding a dual list control to our HTML textbox. Here is the code for the Settings control:

<%@ Register TagPrefix="Portal" 
             TagName="DualList" 
             Src="~/controls/DualListControl.ascx" %>
<%@ Control language="vb" AutoEventWireup="false" 
    Inherits="EganEnterprises.CoffeeShopListing.Settings" 
    CodeBehind="Settings.ascx.vb"%>
<TABLE id="Table1" cellSpacing="1" cellPadding="1" width="100%" 
       border="1">
   <TR>
       <TD>
          <P align="center">ShopListOptions RowOne</P>
       </TD>
   </TR>
   <TR>
       <TD><portal:duallist id="ctlAuthRoles" runat="server" 
              ListBoxWidth="130" ListBoxHeight="130" 
              DataValueField="Value" DataTextField="Text" /></TD>
   </TR>
</TABLE>
<asp:LinkButton id="lbUpdate" runat="server">Update</asp:LinkButton>

In order for this control to integrate into the module settings page, we need to override two methods in our base class: LoadSettings and UpdateSettings. LoadSettings is called when the module settings page is accessed, and UpdateSettings is called when the update button is clicked on the module settings page.

We will be using the options section of the module settings page to hold security settings for this module that are outside the normal module security settings. We want to give users the ability to add a coffee shop without giving them access to the context menu and we also want to read and store the Assigned Roles in our EganEnterprises_ShopListOptions table.

We will start with the LoadSettings method by declaring ArrayList objects to hold both our available roles and our authorized roles.

' declare roles
Dim arrAvailableAuthRoles As New ArrayList
Dim arrAssignedAuthRoles As New ArrayList

The available roles are retrieved from the portal and are tied into the portal security.

' Get list of possible roles
Dim objRoles As New RoleController
Dim objRole As RoleInfo
Dim arrRoles As ArrayList = _
objRoles.GetPortalRoles(PortalId)

The authorized roles are obtained from our EganEnterprises_ShopListOptions table.

'String of roles for shoplist
Dim objShopRoles As New CoffeeShopListingOptionsController
Dim objShopRole As CoffeeShopListingOptionsInfo
Dim arrShopRoles As ArrayList = _
objShopRoles.EganEnterprises_GetCoffeeShopModuleOptions _
(ModuleId)

This passes back a single semicolon-delimited string corresponding to this module only.

'Put roles into a string 
Dim shopRoles As String = ""
For Each objShopRole In arrShopRoles
    'If it makes it here then we will be updating 
    shopRoles = objShopRole.AuthorizedRoles.ToString
Next

We then loop through all roles available in the portal and place them in the correct list.

'Now loop through all available roles in portal
For Each objRole In arrRoles
    Dim objListItem As New ListItem
    objListItem.Value = objRole.RoleID.ToString
    objListItem.Text = objRole.RoleName
    'If it matches a role in the ShopRoles string put
    'it in the assigned box
    If shopRoles.IndexOf(objRole.RoleID & ";") _
    <> -1 Or objRole.RoleID = _
    PortalSettings.AdministratorRoleId Then
        arrAssignedAuthRoles.Add(objListItem)

 
    Else ' put it in the available box
        arrAvailableAuthRoles.Add(objListItem)
    End If
Next
' assign to duallist controls
ctlAuthRoles.Available = arrAvailableAuthRoles
ctlAuthRoles.Assigned = arrAssignedAuthRoles

The dual lists' built-in functionality allows you to move roles between the lists to give or remove the rights of your users.

The UpdateSettings method will save the authorized list to our table. We build a semicolon-delimited list from the listbox and use our CoffeeShopListingOptionsInfo and CoffeeShopListingOptionsController classes to add it to the table.

Dim objShopRoles As New CoffeeShopListingOptionsController
Dim objShopRole As New CoffeeShopListingOptionsInfo
Dim item As ListItem
Dim strAuthorizedRoles As String = ""
For Each item In ctlAuthRoles.Assigned
    strAuthorizedRoles += item.Value & ";"
Next item
objShopRole.AuthorizedRoles = strAuthorizedRoles
objShopRole.moduleID = ModuleId
Dim intExists As Integer
intExists = objShopRoles. _
EganEnterprises_UpdateCoffeeShopModuleOptions(objShopRole)
If intExists = 0 Then
'New record
    objShopRoles.EganEnterprises_AddCoffeeShopModuleOptions _
    (objShopRole)
End If

This completes all three of the controls needed for our module. All that's left for us to do is to test our work.

Testing Your Module

Throughout the development process you should use all of Visual Studio's debugging capabilities to make sure that your code is working correctly. Since we set up our module as a private assembly within the DotNetNuke solution, you will be able to set breakpoints and view your code in the various watch windows. Make sure that your project is set up to allow debugging.

Your project's Properties configuration should be set to Active (Debug) and your ASP.NET debugger should be enabled. You will also need to make sure that debug is set to true in your web.config file. When you have finished debugging your module, you are ready to package it and get it ready for distribution.

Creating Your Installation Scripts

The first step in preparing your module for distribution is to  create the installation scripts needed to create the tables and procedures required by your module. There should be two files: an installation script and an un-installation script. You should name your scripts in the following manner.

Type of Script

Description

Example

Uninstallation Script

Concatenate the word uninstall with the type of provider the script represents.

  • Uninstall.AccessDataProvider
  • Uninstall.SqlDataProvider

Installation Script

Concatenate the version number of your module with the type of provider the script represents.

01.00.00.SqlDataProvider

01.00.00.AccessDataProvider

These scripts are similar to the code run for creating your tables in the beginning of this chapter. The scripts for your PA installation should use the databaseOwner and objectQualifier variables as well as including code to check if the database objects you are creating already exist in the database. This will help to ensure that uploading your module will not overwrite previous data. The full scripts can be found in the code download for this chapter.

The version number for your scripts is very important. If a version of the module is already installed on your portal, the framework checks the version number on the script file to determine whether to run the script. If the number on the file matches the number in the database then the script will not be run. In this way, you can have one package work as an installation and upgrade package.

Packaging Your Module for Distribution

To get our module package  ready for the masses, we will first need to create a manifest for our module. DotNetNuke uses a XML-based file with a .dnn extension to accomplish this. Since this is an XML file, it is important to note that it needs to be well formed. This means that all opening tags <mytag> need to have associated closing tags </mytag>.

To begin setting up our manifest, right-click on the Installation folder we created earlier and select Add | Add New Item. Select XML File from the list and name the file CoffeeShopListing.dnn. The .dnn extension is used by DotNetNuke to designate this file as a module installation file. Below you will see the file itself.

The outside tags <dotnetnuke> and </dotnetnuke> are used to tell the uploader the version and type of item that is being uploaded. The <folder> element then starts to map out where it is going to place all the files for our module.

<?xml version="1.0" encoding="utf-8" ?> 
<dotnetnuke version="3.0" type="Module">
  <folders>
    <folder>

The <name> element is the name of the folder that will be created  for your module. This folder will be created under the DotNetNuke\DesktopModules folder. It is important that you follow the CompanyName.ModuleName format when creating your modules to avoid naming collisions with other module developers. The <version> element then determines the version of your module that is being uploaded. This is followed by the <businesscontrollerclass> element. If you implement any of the interfaces we discussed earlier in your module, you will need this element to allow the import, export, and search to work. This element holds the full class name of the module (including the namespace), followed by your module's assembly name.

Since our controller class is called CoffeeShopListingController, that's what we will use inside this node.

<name>EganEnterprises.CoffeeShopListing</name>
<description>Listing of Coffee Shops</description>
<version>01.00.00</version>
<businesscontrollerclass>EganEnterprises.CoffeeShopListing.
   CoffeeShopListingController,EganEnterprises.
   CoffeeShopListing
</businesscontrollerclass>

We then start describing the controls themselves. We follow the same process when we created controls manually when building our private assembly. The <key> is how your context menu is connected to your control. This is left out for the view control. The <title> is what will show up in the module definition form. The <src> is the physical file name for the control, and the <type> determines whether this is a view control or an edit control. Both our Edit and Options controls use this element.

<modules>
  <module>
    <friendlyname>Coffee Shop Listing</friendlyname>
    <controls>
      <control>
            <title>View Coffee Shops</title>
            <src>ShopList.ascx</src>
            <type>View</type>
      </control>
      <control>
            <key>Edit</key>
            <title>Edit CoffeeShop Listing</title>
            <src>EditShopList.ascx</src>
            <type>Edit</type>
      </control>
      <control>
            <key>Settings</key>
            <title>Shop List Settings</title>
            <src>Settings.ascx</src>
            <type>Edit</type>
      </control>
    </controls>
  </module>
</modules>

After creating the <module> tags, we need to declare the physical files for our module. Be sure to include the controls and DLLs, as well as the installation and uninstall scripts for your module.

    <files>
      <file>
        <name>ShopList.ascx</name>
      </file>
      <file>
        <name>EditShopList.ascx</name>
      </file>
      <file>
        <name>Settings.ascx</name>
      </file>
      <file>
        <name>01.00.00.SqlDataProvider</name>
      </file>
      <file>
        <name>Uninstall.SqlDataProvider</name>
      </file>
      <file>
        <name>EganEnterprises.CoffeeShopListing.dll</name>
      </file>
      <file>
        <name>EganEnterprises.CoffeeShopListing
                .SqlDataProvider.dll</name>
      </file>
    </files>
   </folder>
 </folders>
</dotnetnuke>

The Install ZIP file

Now it is time to package all of the files into a ZIP file to enable them to be uploaded and installed on your portal. Do not just drag the folder containing these files into a ZIP file. Make sure that they all in the main ZIP folder. The DNN framework will take care of placing the files in the correct folders.

The files you need to place in your ZIP file are:

  • EganEnterprises.CoffeeShopListing.dll
  • EganEnterprises.CoffeeShopListing.SqlDataProvider.dll
  • ShopList.ascx
  • EditShopList.ascx
  • Settings.ascx
  • CoffeeShopListing.dnn
  • 01.00.00.SqlDataProvider
  • uninstall.SqlDataProvider

Testing Your Installation

This is the final step. At this stage, all of your coding should work fine because you have tested it in your Visual Studio .NET environment. Now you need to test if uploading your module will work for you. You have a couple of options. Since you have already set up this module manually in your visual studio environment you would have to remove the private assembly and delete the tables in your database to fully test whether your upload file works. I don't like this method. I like my PA to stay just the way it is to make it easy to do further development. What I do for testing is to set up a separate instance of DotNetNuke on my development computer that is only used for testing uploads of modules. You can decide what works best for you.

Uploading the module is simple. Sign in as Host and navigate to the module definitions item on the host menu. Hover the cursor over the context menu and select Upload New Module. Browse to your ZIP file and add it to the file download box. Click on Upload New File to load your module.

This will create a file-upload log, which will only be displayed on the screen below the upload box.

Search the log for any errors that may have occurred during the upload process and fix the errors. Since we have done our testing in Visual Studio, any errors encountered here should be related to the ZIP or DNN file.

Add your module to a tab and put it through its paces. Make sure to try out all the features. It is a good idea to have others try to break it. You will be surprised at the things that users will do with your module. When all features have been tested, you are ready to distribute it to the DotNetNuke world.

Summary

We have covered a lot of code in this chapter. Starting from creating a private assembly and setting up our project, to creating the controls, business logic layer, and the data access layer. We then saw how to package the module so it can be shared.

Since this code was very extensive, we broke it into sections and advised you to build your project at regular intervals. Doing this should give the ability to solve any issues you come across while building your module. We then took things a bit further and showed you how to use a few extra items like the settings page, the dual-list box, and the optional interfaces. This should give you a sound understanding of how all the different parts work together.

It is important to note that once you are familiar with how all of the different parts work together, there are a few tools available that can help to automate the processes of building your modules. Visual Studio DNN Project Templates can be found at DNNJungle and CodeSmith templates to help you design you data architecture can be found at Smcculloch.Net. These two tools can help speed up you module development tremendously. As with any code wizard, if you don't understand the underlying code then you will spend more time trying to figure out what the wizard created and will lose any advantage gained. Hopefully this chapter has given you a rich foundation on which to build your knowledge.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Mohan Raphel
Web Developer
India India
No Biography provided

Comments and Discussions

 
Generalbutton click does not work in view mode PinmemberDinesh Jethva4-Jan-08 2:02 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web04 | 2.8.140721.1 | Last Updated 3 May 2006
Article Copyright 2006 by Mohan Raphel
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid