|
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:
- Open up the DotNetNuke Visual Studio.NET solution file (C:\DotNetNuke\Solutions\DotNetNuke.DesktopModules\ DotNetNuke.DesktopModules.sln).
- In the Solution Explorer, right-click on the DotNetNuke solution (not the project) and select Add | New Project:
- 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.
- Your project should reside under the C:\DotNetNuke\DesktopModules folder. Make sure to change the location to this folder.
- 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. |
- 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.
- 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
- 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.
- Right-click on the reference folder located below the BuildSupport project and select Add Reference.
- Select the Projects tab.
- Double-click on the EganEnterprises.CoffeeShopListing project to place it in the Selected Components box.
- 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.
- Right-click on the reference folder located below the EganEnterprises.CoffeeShopListing private assembly project we just created and select Add Reference.
- Select the Projects tab.
- Double-click on the DotNetNuke project to place it in the Selected Components box.
- 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.
- Making sure that your private assembly project is highlighted, select Add New Item from the Project menu.
- Select Text File from the list of available templates and change the name to ShopList.ascx.
- Click Open to create the file.
- 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.
- Click the save icon on the toolbar to save the page.
- 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
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
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
End Sub
Public Function GetSearchItems( _
ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
As DotNetNuke.Services.Search.SearchItemInfoCollection _
Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
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
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
End Sub
Public Function GetSearchItems( _
ByVal ModInfo As DotNetNuke.Entities.Modules.ModuleInfo) _
As DotNetNuke.Services.Search.SearchItemInfoCollection _
Implements DotNetNuke.Entities.Modules.ISearchable.GetSearchItems
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
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
Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles MyBase.Load
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:
- Hit F5 to run the DotNetNuke solution, log in as host, and click on the Module Definitions option on the Host menu.
- Under the Module Definition menu, select Add New Module Definition:
- Enter the name for your module and a short description of what it does. When you are finished, click on the Update link:
- 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.
- Click on the Add Control link to start.
- Enter the Title for the control. This is the default title when the control is added to a tab.
- 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.
- 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.
- Click Update when done.
Next we want to add our Edit control.
- Enter Edit for the Key field. This is the key that the Actions Menu we created earlier will use to navigate to this control.
- Enter a Title for the control.
- Select the ShopListEdit.ascx control from the Source drop-down list.
- Select Edit as in the Type dropdown.
- Click Update when complete.
Finally we need to add our Settings control.
- Click on Add Control to add the third control for this module.
- Enter Settings for the key field.
- Enter a Title for the control.
- Select the Settings.ascx control from the Source drop-down list.
- Select Edit as in the Type dropdown.
- 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:
- 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.
- 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.
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.
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.
- Right-click on the reference folder located below the BuildSupport project and select Add Reference.
- Select the Projects tab.
- Double-click on the EganEnterprises.CoffeeShopListing.SqlDataProvider project to place it in the Selected Components box.
- 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.
- Right-click on the reference folder located below the
EganEnterprises.CoffeeShopListing.SqlDataProvider
private assembly project we just created and select Add Reference.
- Select the Projects tab.
- Double-click on the DotNetNuke project to place it in the Selected Components box.
- 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.
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"
ublic 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"> </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 (Request.Params("Add") Is Nothing) Then
pnlAdd.Visible = False
pnlGrid.Visible = True
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.
Dim objShopRoles As New CoffeeShopListingOptionsController
Dim objShopRole As CoffeeShopListingOptionsInfo
Dim arrShopRoles As ArrayList = _
objShopRoles.EganEnterprises_GetCoffeeShopModuleOptions _
(ModuleId)
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
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)
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)
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">∓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
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
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
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
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
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.
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.
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.
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.
Dim shopRoles As String = ""
For Each objShopRole In arrShopRoles
shopRoles = objShopRole.AuthorizedRoles.ToString
Next
We then loop through all roles available in the portal and place them in the correct list.
For Each objRole In arrRoles
Dim objListItem As New ListItem
objListItem.Value = objRole.RoleID.ToString
objListItem.Text = objRole.RoleName
If shopRoles.IndexOf(objRole.RoleID & ";") _
<> -1 Or objRole.RoleID = _
PortalSettings.AdministratorRoleId Then
arrAssignedAuthRoles.Add(objListItem)
Else
arrAvailableAuthRoles.Add(objListItem)
End If
Next
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
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.
="1.0"="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.