Rainbow Portal: Design and Implementation
DUEMETRI
March 2003
Summary: This article describes the design and architectural decisions for the Rainbow Portal application. In addition, a detailed examination and explanation of the code required to extend it is also covered. (2517 printed pages)
Overview
What is the Rainbow Portal?
The Rainbow Portal is based on Microsoft’s IBuySpy Portal Solution Kit which demonstrates how you can use ASP.NET along with the Microsoft .NET Framework to build intranet and Internet portal applications. The sample shows off many key features available with ASP.NET and also provides a “best practices” application that developers can use as a base to build their own ASP.NET applications.
The Rainbow Portal demonstrates many features offered by the ASP.NET technology including:
· Multiple portals managed by the same code and storing data on the same database
· Cross-browser support for Netscape and Internet Explorer
· Wide support for different cultures and custom regional settings
· Mobile device support for WAP/WML and Pocket Browser devices
· Clean code/html content separation using server controls
· Pages that are constructed from dynamically-loaded user controls
· Extensive use of themes and layouts providing tools for customizing the portal design
· Configurable output caching of portal page regions and Portal Settings
· Multi-tier application architecture
· ADO.NET data access using SQL stored procedures
· Windows authentication - username/password in Active DS or NT SAM
· Forms authentication using a database for usernames/passwords
· Role-based security to control user access to portal content
This white paper discusses the Rainbow Portal application in depth and provides insight from the perspective of the creators. In addition, the article covers how the Rainbow Portal can be used as a template for building online portals by examining many of the key application features and the technology used to implement them.
The original IBuySpy Portal is distributed in two variants, one version written directly to the .NET framework SDK and the second version written using Microsoft ® Visual Studio.NET™. The main difference between the two is that the latter uses code-behind. Both variants contain versions written in C#.NET and VB.NET. The Rainbow Portal is based on the VS.NET C# version.
Application Architecture
The Rainbow Portal uses multi-tier application architecture. The main data source for the application is a SQL Server database along with stored procedures. The data access is provided through a Microsoft .NET assembly that provides access to the data source via the stored procedures. In addition, the portal framework is built through the use of a number of assemblies that handle the security and configuration of the portal. Web Forms and user controls make up the presentation layer and handle the display and management of the portal data for the user.
Database
All of the configuration settings for portals are stored in a SQL Server database. This allows server administrators to farm the front-end of the portal across a number of servers each pulling from a single unique data store. This section provides an overview of the database used in the Rainbow Portal.
Database Schema
The Rainbow Portal database schema is shown in Figure 1Figure 1.
Figure 1. Physical Database Schema
Portal data tables
Rainbow stores the portals structure in a hierarchical set of tables.
At the top we have the Portal, each portal having its own unique design and custom properties, saved in the PortalSettings table in a Key/Value list.
Each portal contains Tabs. Tabs can be considered the “Pages” of the portal. Tabs can be nested in a tree like structure. The TabSettings table stores custom data for each Tab.
Tabs contain modules. Each module is an object that can handle a well defined set of data. There are modules for managing a list of Links, HTML text, Contacts and more.
The GeneralModuleDefinitions table contains a list of all available modules. Modules can be activated on Portal basis. The ModuleDefinitions table contains a list of modules associated with a single portal.
The Module table contains a list of module instances: where the module is placed (Tab) and permissions associated with the module itself. ModuleSettings table stores custom data for each Module.
Stored Procedures
The Rainbow Portal uses stored procedures to encapsulate all of the database queries. Stored procedures provide a clean separation between the database and the middle-tier data access layer. This in turn provides easier maintenance, since changes to the database schema will be invisible to the data access components. Using stored procedures also provide performance benefits since they are optimized the first time they are run and then retained in memory for subsequent calls.
The Portal Framework
The Rainbow Portal contains an extensible framework that allows users to build and use individual portal modules to handle the display and management of data. The following sections will cover the basics of the portal framework, as well as how it was built.
Portal Settings
The portal settings are represented by the PortalSettings Class, which is defined in the Configuration namespace component.
These settings include the following:
· The Portal ID
· The Portal Name
· The Desktop Tabs Collection
· The
· Portal Custom Settings
· Active Tab and its Custom Settings
· Consistent access to Portal and site properties
· Active Theme and Layout
The PortalSettings class is updated and placed into the “Context” object upon each web request of the portal application. This is achieved by using the Application_BeginRequest event in the Global.asax.cs file. The parameters passed are the current tab id (the current “Page”) and the Alias of the current Portal.
settings = new PortalSettings(tabId, portalAlias);
Once stored in the Context object, these settings can be obtained from anywhere in the application including all pages, components, and controls by accessing the Context item with the name “PortalSettings”. You can access current portal settings using the portalSettings property defined on Rainbow.UI.Page and Rainbow.UI.PortalModuleControl
Portal Tabs
The Tabs are stored in two public fields of the PortalSettings object we described in the previous section. The fields, DesktopTabs and MobileTabs, are of the type ArrayList and contain instances of the TabStripDetails class, which represents an individual tab. Access to the tabs collection is achieved through the PortalSettings Context item.
The display of the tabs (i.e. the top-level navigation) is handled by the Rainbow Layouts, usually in the DesktopPortalBanner.ascx user control.
Rainbow provides a set of controls, located in the Rainbow.UI.WebControls namespace, that can be used to build easily a custom page.
- HeaderImage
- HeaderMenu
- HeaderTitle
- DesktopNavigation
- MenuNavigation
- BreadCrumbs
- DesktopPanes
- Paging
DesktopNavigation and MenuNavigation controls iterate through the tabs collection checking whether the current user has rights to view the tab. If the necessary role is met, the tab is added to another collection that will be bound to a DataList.
Portal Modules
Portal Modules provide the actual content of a Rainbow Portal. The modules are user controls that inherit the PortalModuleControl base class, which provides the necessary communication between the modules and the underlying Portal Framework.
The IBuySpy Portal comes with 18 built-in portal modules that are available “out of the box” for displaying custom content and 8 modules delegated to Administrative functions.
Portal Security
The security design in the portal makes use of both authentication and authorization. Authentication is the process by which the application verifies a user’s identity and credentials. Authorization then verifies the authenticated user’s permissions for a particular requested resource.
The portal supports both forms based and windows based authentication. The authentication mode is defined in the web.config and the User.Identity.Name property maintains the user name. Forms based authentication stores the usernames and passwords in the database and Windows authentication uses a domain/active directory with the NTLM challenge/response protocol. The authorization for the portal is handled using role based security to determine whether or not a user has access to a particular resource. Users are grouped into various roles (admins, power users, devs, etc.) and the role mappings are stored in the database. Rainbow also defines special roles like “Authenticated Users” and “Unauthenticated Users” that are internally managed.
The tabs and modules in the portal maintain access control lists (ACL) to determine who has permission to access the control. This prevents a normal user accessing the administration functionality.
Rainbow extends the original IBuySpy
Security checks are delegated to bases classes in Rainbow so a Module developer can safely ignore tests for authorized roles.
The current user’s role mappings are set for the request in Global.asax in the Application_AuthenticateRequest() event. The Context.User is then set using the GenericPrincipal method and the User.IsInRole method can be used to verify whether the current user is in a specific role.
The database calls for all of the role-based checks are contained in Security namespace classes.
Administering the Rainbow Portal
The Rainbow Portal has an online administration tool that allows users in the “Admins” role to manage the security, layout, and content of the portal. Users that are logged in that belong to the “Admins” role will see an “Admin” tab link that takes them to the administration tool.
The portal administration allows the user to perform a variety of site management and configuration tasks. This is the place where new tabs (pages) can be added, modules can be added to them, security roles defined, etc.
Extending the Rainbow Portal: a tutorial
The Rainbow Portal was built with the idea of extensibility in mind, providing a way for developers to easily add portal modules that can “plug” into the framework. In this section we will look at the steps that you can follow to build your own portal modules. To do this, we will build a Milestones portal module that will display project milestones.
Extending the Data Layer
Most of the portal modules use the portal database as their primary data store. We will do the same for our example. Therefore the first step is to extend the data layer. We begin by creating a new Table called Milestones, as shown in Figure 2Figure 2.
Figure 2. Milestones Table
Create a relationship with the Module table:
Next, we will create the stored procedures required to handle access to the Milestones table.
Rainbow provides a tool for doing this, please download it at:
http://www.rainbowportal.net/Rainbow/_Rainbow/Documents/CodeWizard.zip
This tool will do two things:
· Will create the stored procedures.
· Will create the C# data layer for accessing procedures.
Launch the CodeWizard.
Select your server and authentication.
Select Database name and then the table to generate.
Press the Generate button.
Click on “Copy SP” then paste and execute the script in SQL Query Analayzer; you will get all the procedures you need.
Click on “Copy C#”, open the Visual Studio project and add a new MilestonesDB.cs file in DesktopComponents folder, then paste the code there.
This way you have created the data access layer (DAL) component to provide access to the Milestone procedures.
Obviously you can do this task by hand or use CodeWizard as a “first step” tool and refine you code by hand later. If you have DAL ready from old IBS modules there is no need to change this.
Creating the User Control
After the database is taken care of, the next step is to create the user control that will handle the milestone user interface.
The user control will contain a DataGrid that will define three columns: Title, Completion Date, and Status.
In this tutorial we use Visual Studio .Net.
Go to DesktopModules and add a new User Control called “Milestones.ascx”.
Add a DataGrid control on the Milestones Control page. Name it “myDataGrid”.
Milestones.ascx
Here is the front part of the control. It goes in the “Milestones.ascx” file.
<%@ Control Language="c#" AutoEventWireup="false" Codebehind="Milestones.ascx.cs" Inherits="Rainbow.DesktopModules.Milestones"%>
<%@ Register TagPrefix="tra" Namespace="Rainbow.UI.WebControls.Globalized" Assembly="Rainbow" %>
<asp:DataGrid id="myDataGrid" HeaderStyle-CssClass="Normal" HeaderStyle-Font-Bold="true" ItemStyle-CssClass="
<Columns>
<asp:TemplateColumn>
<ItemTemplate>
<tra:HyperLink id="editLink" TextKey="EDIT" Text="Edit" ImageUrl="~/images/edit.gif" NavigateUrl='<%# Rainbow.HttpUrlBuilder.BuildUrl("~/DesktopModules/MilestonesEdit.aspx", "ItemID=" + DataBinder.Eval(Container.DataItem,"ItemID") + "&Mid=" + ModuleId) %>' Visible="<%# IsEditable %>" runat="server" />
</ItemTemplate>
</asp:TemplateColumn>
<tra:BoundColumn DataField="Title" TextKey="MILESTONE_TITLE" HeaderText="Title" runat="server" />
<tra:BoundColumn DataField="EstCompleteDate" TextKey="MILESTONE_COMPL_DATE" HeaderText="Compl. Date" runat="server" DataFormatString="{0:d}" />
<tra:BoundColumn DataField="Status" TextKey="MILESTONE_STATUS" HeaderText="Status" runat="server" />
</Columns>
</asp:DataGrid>
Add support for localization
Rainbow includes great support for localizing pages. It requires no effort by the module developer and provides a consistent and powerful framework for localization.
There are several WebControls in the Rainbow.UI.WebControls.Globalized namespace that inherit directly from standard .Net WebControls.
First you have to register the Globalized control namespace at top of your page.
<%@ Register TagPrefix="tra" Namespace="Rainbow.UI.WebControls.Globalized"
Assembly="Rainbow" %>
Then add your controls using the “tra:” prefix instead of “asp:” this way:
<tra:Literal TextKey="MILESTONE_DETAIL" Text="Milestones Details" runat="server"></tra:Literal>
The TextKey property contains the Key used by the Localize Class for providing translated text at runtime. Be aware that this key must be unique across a rainbow installation, so if you are doing a particular module you should prefix names in some way to avoid possible conflicts.
It is important to define the Text property too (or the property that contains the Text of the control, for BoundColumn it will be HeaderText). The text value is used as a default translation for English language and a base for translating the Key into all supported languages. This can be really useful in case of a Key conflict because it can provide a default translation when you change the Key.
If a key of the same name is already used no error occurs but you will most likely get a wrong translation.
To track the key usage, look at the Sections table.
If you need to enter pure text please use the Literal control. Avoid the use of ASP inner script (<% - %> blocks like <%= Localize.GetString("MY_KEY")%>) because usage cannot be tracked.
If you need to get a key translation from code use this syntax:
Localize.GetString("MY_KEY", “My translation”, control).
The control should be the control on which you will use the translation. This can be a UserControl, a WebControl or a Page. If control is not applicable pass the page itself: each control has a Page reference in it.
Using the simple syntax also works but it is not recommended: Localize.GetString("MY_KEY”).
Milestones.ascx.cs
Show the Milestones.ascx.cs code page and copy there the code below. I have put comments in it. Read it carefully.
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using Rainbow.Admin;
namespace Rainbow.DesktopModules
{
/// <summary>
/// IBS Portal Milestone Module - Edit page part
/// Writen by: Elaine Ossipov -
/// Moved into Rainbow by
/// Updated by
/// </summary>
public class MilestonesEdit : Rainbow.UI.EditItemPage
{
protected Rainbow.UI.WebControls.Globalized.Literal Literal1;
protected Rainbow.UI.WebControls.Globalized.Literal Literal2;
protected System.Web.UI.WebControls.TextBox TitleField;
protected Rainbow.UI.WebControls.Globalized.RequiredFieldValidator Req1;
protected Rainbow.UI.WebControls.Globalized.Literal Literal3;
protected System.Web.UI.WebControls.TextBox EstCompleteDate;
protected System.Web.UI.WebControls.RequiredFieldValidator Req2;
protected System.Web.UI.WebControls.CompareValidator VerifyCompleteDate;
protected Rainbow.UI.WebControls.Globalized.Literal Literal4;
protected System.Web.UI.WebControls.TextBox StatusBox;
protected Rainbow.UI.WebControls.Globalized.RequiredFieldValidator Req3;
protected System.Web.UI.WebControls.Label CreatedBy;
protected System.Web.UI.WebControls.Label CreatedDate;
private void Page_Load(object sender, System.EventArgs e)
{
// If the page is being requested the first time, determine if a
// Milestone itemId value is specified, and if so,
// populate the page contents with the Milestone details.
if (Page.IsPostBack == false)
{
//Item id is defined in base class
if (ItemId > 0)
{
//Obtain a single row of Milestone information.
MilestonesDB milestonesDB = new MilestonesDB();
SqlDataReader dr = milestonesDB.GetSingleMilestones(ItemId);
//Load the first row into the DataReader
dr.Read();
TitleField.Text = (String) dr["Title"];
EstCompleteDate.Text = ((DateTime) dr["EstCompleteDate"]).ToShortDateString();
StatusBox.Text = (String) dr["Status"];
CreatedBy.Text = (String) dr["CreatedByUser"];
CreatedDate.Text = ((DateTime) dr["CreatedDate"]).ToShortDateString();
dr.Close();
}
else
{
//Provide defaults
EstCompleteDate.Text = DateTime.Now.AddDays(60).ToShortDateString();
}
}
}
/// <summary>
/// This procedure is automaticall
/// called on Update
/// </summary>
protected override void OnUpdate(EventArgs e)
{
// Calling base we check if the user has rights on updating
base.OnUpdate(e);
// Update onlyif the entered data is Valid
if (Page.IsValid == true)
{
MilestonesDB milestonesDb = new MilestonesDB();
if (ItemId <= 0)
milestonesDb.AddMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
else
milestonesDb.UpdateMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
// Redirects to the referring page
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
}
/// <summary>
/// This procedure is automaticall
/// called on Update
/// </summary>
override protected void OnDelete(EventArgs e)
{
// Calling base we check if the user has rights on deleting
base.OnUpdate(e);
if (ItemId > 0)
{
MilestonesDB milestonesDb = new MilestonesDB();
milestonesDb.DeleteMilestones(ItemId);
}
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
#region Web Form Designer generated code
/// <summary>
/// Raises OnInitEvent
/// </summary>
/// <param name="e"></param>
protected override void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}
Generate the guid
Each Rainbow Module must define its own internal Guid. This guid must be unique. To generate a guid use the GuidGen tool on Visual Studio’s Tools Menu.
Inherit the Rainbow Module Control Base Class
All portal modules are actually no more than simple ASP.NET user controls that inherit the PortalModuleControl base class. This base class provides all the hooks that are required for the portal module to interact with the framework.
Add the Module Title
One of the user controls that is available to portal modules is the Title user control. This control will generate the appropriate HTML for the Module’s title.
The title is added in the OnInit procedure.
Implement search support
Rainbow defines a framework for enable search on each module.
Enabling search on you module is an easy three steps procedure.
Implementing search on your modules
1. You have to remove abstract word from class definition.
Visual Studio automatically defines Codebehind pages as abstracts, anyway searchable modules must not be abstract classes because are instantiated using reflection.
2. Override the search method.
/// <summary>
/// Searchable module implementation
/// </summary>
/// <param name="portalID">The portal ID</param>
/// <param name="userID">Id of the user is searching</param>
/// <param name="searchString">The text to search</param>
/// <param name="searchField">The fields where perfoming the search</param>
/// <returns>The SELECT sql to perform a search on the current module</returns>
public override string SearchSqlSelect(int portalID, int userID, string searchString, string searchField)
{
// Parameters:
// Table Name: the table that holds the data
// Title field: the field that contains the title for result, must be a field in the table
// Abstract field: the field that contains the text for result, must be a field in the table
// Search field: pass the searchField parater you recieve.
Rainbow.Helpers.SearchDefinition s = new Rainbow.Helpers.SearchDefinition("Milestones", "Title", "Status", "CreatedByUser", "CreatedDate", searchField);
//Add here extra search fields, this way
//s.ArrSearchFields.Add("itm.ExtraFieldToSearch");
// Builds and returns the SELECT query
return s.SearchSqlSelect(portalID, userID, searchString);
}
3. Override Searchable property
/// <summary>
/// If the module is searchable you
/// must override the property to return true
/// </summary>
public override bool Searchable
{
get
{
return true;
}
}
Add Support for an Ed it Page
In order to allow users to edit and add additional Milestones, support for an edit page must be added.
This support is added by defining
An optional
The naming convention starts with ModuleName and then adds the function.
e.g. Milestones
Create the Ed it Page
Once we have added support for an edit page using the Title User Control, we need to create the actual edit page. In addition to the HTML, we will define four methods:
· Page_Load
· OnUpdate
· OnDelete
· OnCancel (already defined in base class)
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using Rainbow.Admin;
namespace Rainbow.DesktopModules
{
/// <summary>
/// IBS Portal Milestone Module -
/// Writen by: Elaine Ossipov -
/// Moved into Rainbow by
/// Updated by
/// </summary>
public class Milestones
{
protected System.Web.UI.WebControls.TextBox TitleField;
protected System.Web.UI.WebControls.RequiredFieldValidator Req1;
protected System.Web.UI.WebControls.TextBox EstCompleteDate;
protected System.Web.UI.WebControls.RequiredFieldValidator Req2;
protected System.Web.UI.WebControls.CompareValidator VerifyCompleteDate;
protected System.Web.UI.WebControls.TextBox StatusBox;
protected System.Web.UI.WebControls.RequiredFieldValidator Req3;
protected System.Web.UI.WebControls.Label CreatedBy;
protected System.Web.UI.WebControls.Label CreatedDate;
private void Page_Load(object sender, System.EventArgs e)
{
// If the page is being requested the first time, determine if a
// Milestone itemId value is specified, and if so,
// populate the page contents with the Milestone details.
if (Page.IsPostBack == false)
{
//Item id is defined in base class
if (ItemId > 0)
{
//Obtain a single row of Milestone
MilestonesDB milestonesDB = new MilestonesDB();
SqlDataReader dr = milestonesDB.GetSingleMilestones(ItemId);
//Load the first row into the DataReader
dr.Read();
TitleField.Text = (String) dr["Title"];
EstCompleteDate.Text = ((DateTime) dr["EstCompleteDate"]).ToShortDateString();
StatusBox.Text = (String) dr["Status"];
CreatedBy.Text = (String) dr["CreatedByUser"];
CreatedDate.Text = ((DateTime) dr["CreatedDate"]).ToShortDateString();
dr.Close();
}
}
}
/// <summary>
/// This procedure is automatically
/// called on Update
/// </summary>
override protected void OnUpdate()
{
// Calling base we check if the user has rights on updating
base.OnUpdate();
// Update onlyif the entered data is Valid
if (Page.IsValid == true)
{
MilestonesDB milestonesDb = new MilestonesDB();
if (ItemId <= 0)
milestonesDb.AddMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
else
milestonesDb.UpdateMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
// Redirects to the referring page
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
}
/// <summary>
/// This procedure is automatically
/// called on Update
/// </summary>
override protected void OnDelete()
{
// Calling base we check if the user has rights on updating
base.OnUpdate();
if (ItemId > 0)
{
MilestonesDB milestonesDb = new MilestonesDB();
milestonesDb.DeleteMilestones(ItemId);
}
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
#region Web Form Designer generated code
/// <summary>
/// Raises OnInitEvent
/// </summary>
/// <param name="e"></param>
protected override void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}
Workflow
Rainbow defines a powerful way for implementing workflow on custom modules.
How workflow works
1. The editor switches to the staging view and edit'sedits the content
2. The editor has finished editing it's content so he clicks the "Request approval button”
3. Then he's shown a page where he can (not must) send an email to the guy who has to approve to new content.
4. The approval guy get's it's email and navigates opens his browser and navigates to the specific module the approval guy has two option's: approve & reject. When he clicks the approve button he's shown a page where he can (not must) send an email to the guy who has to publish the new content.
Add Workflow support to you module
Workflow at the moment work’s only for modules which persist theirthere content in the Rainbow database.
The table in which the content is persisted must be linked through a foreign key to the modules table. If this table contains child tables itself then the workflow will fail. I can support this too, but then I have to make the “publish” stored procedure recursive.
For adding workflow support to your module do the following steps.
Database modifications
1) Create an identical table in the database to the table in which you store the modules content. The table must have the same name as the original with “st_” prefix.
2) Modify your stored procedure so it updates the staging table instead of the “production” table.
3) Add a trigger to the staging table. For example see below or to the trigger on the dbo.st_HtmlText table.
/* Trigger on staging table */
CREATE TRIGGER [st_MilestonesModified]
ON [st_Milestones]
FOR DELETE, INSERT, UPDATE
AS
BEGIN
DECLARE ChangedModules CURSOR FOR
SELECT ModuleID
FROM inserted
SELECT ModuleID
FROM deleted
DECLARE @ModID int
OPEN ChangedModules
FETCH NEXT FROM ChangedModules
INTO @ModID
WHILE @@FETCH_STATUS = 0
BEGIN
EXEC Module
FETCH NEXT FROM ChangedModules
INTO @ModID
END
CLOSE ChangedModules
DEALLOCATE ChangedModules
END
4) Add a parameter to the stored procedure you use for retrieving the content to indicate if you want the staging data or the production data. For example see below or dbo.GetHtmlText
/* Procedure GetMilestones*/
CREATE PROCEDURE GetMilestones
@ModuleId int,
@WorkflowVersion int
AS
IF ( @WorkflowVersion = 1 )
SELECT
ItemId,
ModuleId,
CreatedByUser,
CreatedDate,
Title,
EstCompleteDate,
Status
FROM
Milestones
WHERE
ModuleId = @ModuleId
ELSE
SELECT
ItemId,
ModuleId,
CreatedByUser,
CreatedDate,
Title,
EstCompleteDate,
Status
FROM
st_Milestones
WHERE
ModuleId = @ModuleId
C# source code modifications
1) Add a parameter to indicate the version you want to retrieve to the retrieve function in your DB component corresponding to your module
2) In the constructor of your module set the “SupportsWorkflow” property = true;
3) In the databinding procedure of your module use the “Version” property of the PortalModuleControl with the retrieving procedure of your DB component corresponding to your module
4) In the databinding of the edit page of your module always use WorkflowVersion.Staging, because you always have to bind your editing env to the staging environment.
MilestonesDB.cs
Show the MilestonesDB.cs code page and copy there the code below.
using System;
using System.Configuration;
using System.Data;
using System.Data.SqlClient;
using Rainbow.Configuration;
namespace Rainbow.DesktopModules
{
public class MilestonesDB
{
/// <summary>
/// GetSingleMilestones
/// </summary>
/// <param name="ItemId">ItemId</param>
/// <param name="version">version</param>
/// <returns>A SqlDataReader</returns>
// add version parameter
public SqlDataReader GetSingleMilestones(int ItemId, WorkFlowVersion version)
{
// Create Instance of Connection and Command Object
SqlConnection myConnection = PortalSettings.SqlConnectionString;
SqlCommand myCommand = new SqlCommand("GetSingleMilestones", myConnection);
// Mark the Command as a SPROC
myCommand.CommandType = CommandType.StoredProcedure;
// Add Parameters to SPROC
SqlParameter parameterItemId = new SqlParameter("@ItemId", SqlDbType.Int);
parameterItemId.Value = ItemId;
myCommand.Parameters.Add(parameterItemId);
// add version parameter to command
SqlParameter parameterWorkflowVersion = new SqlParameter("@WorkflowVersion", SqlDbType.Int, 4);
parameterWorkflowVersion.Value = (int)version;
myCommand.Parameters.Add(parameterWorkflowVersion);
// Execute the command
myConnection.Open();
SqlDataReader result = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
// Return the datareader
return result;
}
/// <summary>
/// GetMilestones
/// </summary>
/// <param name="ItemId">ItemId</param>
/// <param name="version">version</param>
/// <returns>A SqlDataReader</returns>
// add version parameter
public SqlDataReader GetMilestones(int ModuleId, WorkFlowVersion version)
{
// Create Instance of Connection and Command Object
SqlConnection myConnection = PortalSettings.SqlConnectionString;
SqlCommand myCommand = new SqlCommand("GetMilestones", myConnection);
// Mark the Command as a SPROC
myCommand.CommandType = CommandType.StoredProcedure;
// Add Parameters to SPROC
SqlParameter parameterModuleId = new SqlParameter("@ModuleId", SqlDbType.Int);
parameterModuleId.Value = ModuleId;
myCommand.Parameters.Add(parameterModuleId);
// add version parameter to command
SqlParameter parameterWorkflowVersion = new SqlParameter("@WorkflowVersion", SqlDbType.Int, 4);
parameterWorkflowVersion.Value = (int)version;
myCommand.Parameters.Add(parameterWorkflowVersion);
// Execute the command
myConnection.Open();
SqlDataReader result = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
// Return the datareader
return result;
}
/// <summary>
/// DeleteMilestones
/// </summary>
/// <param name="ItemId">ItemId</param>
/// <returns>Void</returns>
public void DeleteMilestones(int ItemId)
{
// Create Instance of Connection and Command Object
SqlConnection myConnection = PortalSettings.SqlConnectionString;
SqlCommand myCommand = new SqlCommand("DeleteMilestones", myConnection);
// Mark the Command as a SPROC
myCommand.CommandType = CommandType.StoredProcedure;
// Add Parameters to SPROC
SqlParameter parameterItemId = new SqlParameter("@ItemId", SqlDbType.Int);
parameterItemId.Value = ItemId;
myCommand.Parameters.Add(parameterItemId);
// Execute the command
myConnection.Open();
myCommand.ExecuteNonQuery();
myConnection.Close();
}
/// <summary>
/// AddMilestones
/// </summary>
/// <param name="ItemId">ItemId</param>
/// <returns>The newly created ID</returns>
public int AddMilestones(int ItemId, int ModuleId, string CreatedByUser, DateTime CreatedDate, string Title, DateTime EstCompleteDate, string Status)
{
// Create Instance of Connection and Command Object
SqlConnection myConnection = PortalSettings.SqlConnectionString;
SqlCommand myCommand = new SqlCommand("AddMilestones", myConnection);
// Mark the Command as a SPROC
myCommand.CommandType = CommandType.StoredProcedure;
// Add Parameters to SPROC
SqlParameter parameterItemId = new SqlParameter("@ItemId", SqlDbType.Int);
parameterItemId.Direction = ParameterDirection.Output;
myCommand.Parameters.Add(parameterItemId);
SqlParameter parameterModuleId = new SqlParameter("@ModuleId", SqlDbType.Int);
parameterModuleId.Value = ModuleId;
myCommand.Parameters.Add(parameterModuleId);
SqlParameter parameterCreatedByUser = new SqlParameter("@CreatedByUser", SqlDbType.NVarChar, 100);
parameterCreatedByUser.Value = CreatedByUser;
myCommand.Parameters.Add(parameterCreatedByUser);
SqlParameter parameterCreatedDate = new SqlParameter("@CreatedDate", SqlDbType.DateTime);
parameterCreatedDate.Value = CreatedDate;
myCommand.Parameters.Add(parameterCreatedDate);
SqlParameter parameterTitle = new SqlParameter("@Title", SqlDbType.NVarChar, 100);
parameterTitle.Value = Title;
myCommand.Parameters.Add(parameterTitle);
SqlParameter parameterEstCompleteDate = new SqlParameter("@EstCompleteDate", SqlDbType.DateTime);
parameterEstCompleteDate.Value = EstCompleteDate;
myCommand.Parameters.Add(parameterEstCompleteDate);
SqlParameter parameterStatus = new SqlParameter("@Status", SqlDbType.NVarChar, 100);
parameterStatus.Value = Status;
myCommand.Parameters.Add(parameterStatus);
// Execute the command
myConnection.Open();
SqlDataReader result = myCommand.ExecuteReader(CommandBehavior.CloseConnection);
// Return the datareader
return (int)parameterItemId.Value;
}
/// <summary>
/// UpdateMilestones
/// </summary>
/// <param name="ItemId">ItemId</param>
/// <returns>Void</returns>
public void UpdateMilestones(int ItemId, int ModuleId, string CreatedByUser, DateTime CreatedDate, string Title, DateTime EstCompleteDate, string Status)
{
// Create Instance of Connection and Command Object
SqlConnection myConnection = PortalSettings.SqlConnectionString;
SqlCommand myCommand = new SqlCommand("UpdateMilestones", myConnection);
// Mark the Command as a SPROC
myCommand.CommandType = CommandType.StoredProcedure;
// Update Parameters to SPROC
SqlParameter parameterItemId = new SqlParameter("@ItemId", SqlDbType.Int);
parameterItemId.Value = ItemId;
myCommand.Parameters.Add(parameterItemId);
SqlParameter parameterModuleId = new SqlParameter("@ModuleId", SqlDbType.Int);
parameterModuleId.Value = ModuleId;
myCommand.Parameters.Add(parameterModuleId);
SqlParameter parameterCreatedByUser = new SqlParameter("@CreatedByUser", SqlDbType.NVarChar, 100);
parameterCreatedByUser.Value = CreatedByUser;
myCommand.Parameters.Add(parameterCreatedByUser);
SqlParameter parameterCreatedDate = new SqlParameter("@CreatedDate", SqlDbType.DateTime);
parameterCreatedDate.Value = CreatedDate;
myCommand.Parameters.Add(parameterCreatedDate);
SqlParameter parameterTitle = new SqlParameter("@Title", SqlDbType.NVarChar, 100);
parameterTitle.Value = Title;
myCommand.Parameters.Add(parameterTitle);
SqlParameter parameterEstCompleteDate = new SqlParameter("@EstCompleteDate", SqlDbType.DateTime);
parameterEstCompleteDate.Value = EstCompleteDate;
myCommand.Parameters.Add(parameterEstCompleteDate);
SqlParameter parameterStatus = new SqlParameter("@Status", SqlDbType.NVarChar, 100);
parameterStatus.Value = Status;
myCommand.Parameters.Add(parameterStatus);
// Execute the command
myConnection.Open();
myCommand.ExecuteNonQuery();
myConnection.Close();
}
}
}
Milestones.ascx.cs
Show the Milestones.ascx.cs code page and copy there the code below.
namespace Rainbow.DesktopModules
{
using System;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
//Add Rainbow Namespaces
using Rainbow.UI;
using Rainbow.UI.WebControls;
using Rainbow.Configuration;
/// <summary>
/// Summary description for Milestones.
/// Notice we have changed base class from System.Web.UI.UserControl
/// to Rainbow.UI.WebControls.PortalModuleControl
/// </summary>
/// Remove abstract, searchable classes cannot be abstract
public class Milestones : Rainbow.UI.WebControls.PortalModuleControl
{
protected System.Web.UI.WebControls.DataGrid myDataGrid;
private void Page_Load(object sender, System.EventArgs e)
{
// Create an instance of MilestonesDB class
MilestonesDB milestones = new MilestonesDB();
// Get the Milstones data for the current module.
// ModuleId is defined on base class and contains
// a reference to the current module.
// and version is the requested version (production or staging)
myDataGrid.DataSource = milestones.GetMilestones(ModuleId, Version);
// Bind the milestones data to the grid.
myDataGrid.DataBind();
}
/// <summary>
/// Override base Guid implementation
/// to provide an unique id for your control
/// </summary>
public override Guid GuidID
{
get
{
return new Guid("{B8784E32-688A-4b8a-87C4-DF108BF12DBE}");
}
}
/// <summary>
/// If the module is searchable you
/// must override the property to return true
/// </summary>
public override bool Searchable
{
get
{
return true;
}
}
public Milestones()
{
// indicates the support for workflow
SupportsWorkflow = true;
}
/// <summary>
/// Searchable module implementation
/// </summary>
/// <param name="portalID">The portal ID</param>
/// <param name="userID">Id of the user is searching</param>
/// <param name="searchString">The text to search</param>
/// <param name="searchField">The fields where perfoming the search</param>
/// <returns>The SELECT sql to perform a search on the current module</returns>
public override string SearchSqlSelect(int portalID, int userID, string searchString, string searchField)
{
// Parameters:
// Table Name: the table that holds the data
// Title field: the field that contains the title for result, must be a field in the table
// Abstract field: the field that contains the text for result, must be a field in the table
// Search field: pass the searchField parater you recieve.
Rainbow.Helpers.SearchDefinition s = new Rainbow.Helpers.SearchDefinition("Milestones", "Title", "Status", "CreatedByUser", "CreatedDate", searchField);
//Add here extra search fields, this way
//s.ArrSearchFields.Add("itm.ExtraFieldToSearch");
// Builds and returns the SELECT query
return s.SearchSqlSelect(portalID, userID, searchString);
}
#region Web Form Designer generated code
override protected void OnInit(EventArgs e)
{
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
InitializeComponent();
// Create a new Title the control
ModuleTitle = new DesktopModuleTitle();
// Set here title properties
// Add support for the edit page
ModuleTitle.AddUrl = "~/DesktopModules/Milestones
// Add title ad the very beginnig of
// the control's controls collection
Controls.AddAt(0, ModuleTitle);
// Call base init procedure
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}
Milestones
Show the Milestones
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using Rainbow.Admin;
using Rainbow.Configuration;
namespace Rainbow.DesktopModules
{
/// <summary>
/// IBS Portal Milestone Module -
/// Writen by: Elaine Ossipov -
/// Moved into Rainbow by
/// Updated by
/// </summary>
public class Milestones
{
protected Rainbow.UI.WebControls.Globalized.Literal Literal1;
protected Rainbow.UI.WebControls.Globalized.Literal Literal2;
protected System.Web.UI.WebControls.TextBox TitleField;
protected Rainbow.UI.WebControls.Globalized.RequiredFieldValidator Req1;
protected Rainbow.UI.WebControls.Globalized.Literal Literal3;
protected System.Web.UI.WebControls.TextBox EstCompleteDate;
protected System.Web.UI.WebControls.RequiredFieldValidator Req2;
protected System.Web.UI.WebControls.CompareValidator VerifyCompleteDate;
protected Rainbow.UI.WebControls.Globalized.Literal Literal4;
protected System.Web.UI.WebControls.TextBox StatusBox;
protected Rainbow.UI.WebControls.Globalized.RequiredFieldValidator Req3;
protected System.Web.UI.WebControls.Label CreatedBy;
protected System.Web.UI.WebControls.Label CreatedDate;
private void Page_Load(object sender, System.EventArgs e)
{
// If the page is being requested the first time, determine if a
// Milestone itemId value is specified, and if so,
// populate the page contents with the Milestone details.
if (Page.IsPostBack == false)
{
//Item id is defined in base class
if (ItemId > 0)
{
//Obtain a single row of Milestone
MilestonesDB milestonesDB = new MilestonesDB();
SqlDataReader dr = milestonesDB.GetSingleMilestones(ItemId, WorkFlowVersion.Staging);
//Load the first row into the DataReader
dr.Read();
TitleField.Text = (String) dr["Title"];
EstCompleteDate.Text = ((DateTime) dr["EstCompleteDate"]).ToShortDateString();
StatusBox.Text = (String) dr["Status"];
CreatedBy.Text = (String) dr["CreatedByUser"];
CreatedDate.Text = ((DateTime) dr["CreatedDate"]).ToShortDateString();
dr.Close();
}
else
{
//Provide defaults
EstCompleteDate.Text = DateTime.Now.AddDays(60).ToShortDateString();
}
}
}
/// <summary>
/// This procedure is automaticall
/// called on Update
/// </summary>
protected override void OnUpdate(EventArgs e)
{
// Calling base we check if the user has rights on updating
base.OnUpdate(e);
// Update onlyif the entered data is Valid
if (Page.IsValid == true)
{
MilestonesDB milestonesDb = new MilestonesDB();
if (ItemId <= 0)
milestonesDb.AddMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
else
milestonesDb.UpdateMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
// Redirects to the referring page
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
}
/// <summary>
/// This procedure is automaticall
/// called on Update
/// </summary>
override protected void OnDelete(EventArgs e)
{
// Calling base we check if the user has rights on deleting
base.OnUpdate(e);
if (ItemId > 0)
{
MilestonesDB milestonesDb = new MilestonesDB();
milestonesDb.DeleteMilestones(ItemId);
}
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
#region Web Form Designer generated code
/// <summary>
/// Raises OnInitEvent
/// </summary>
/// <param name="e"></param>
protected override void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}
Using workflow
1) Go on Module properties and enable workflow support for specific module (You cannot enable support is the module does not support workflow).
2) Then define roles that can Approve and Publish the module contents.
3) Modify the staging version.
4) When you are ready submit it for approbation. You can send an email to authorized roles so that the can know that ere is something to publish.
5) The authorized roles can then approve or reject the new content.
When they reject it, the module comes back available for editing by the editors. An email can be sent to the editors to explain what the problem is.
When they approve it, an email can be sent to the publisher.
6) Publishers can then publish the new content.
Add Support for an Edit Page
In order to allow users to edit and add additional Milestones, support for an edit page must be added.
This support is added by defining Ed itUrl to the Module Title Control, if you have a page that edits the content of your module directly, like HTML module; or defining AddUrl, if you have a page for adding items to the control (like this Milestones Module).
An optional EditText or AddText can be defined. This text is a localization Key and will be localized.
The naming convention starts with ModuleName and then adds the function.
e.g. MilestonesEd it
Create the Edit Page
Once we have added support for an edit page using the Title User Control, we need to create the actual edit page. In addition to the HTML, we will define four methods:
· Page_Load
· OnUpdate
· OnDelete
· OnCancel (already defined in base class)
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Data.SqlClient;
using Rainbow.Admin;
namespace Rainbow.DesktopModules
{
/// <summary>
/// IBS Portal Milestone Module - Edit page part
/// Writen by: Elaine Ossipov - 9/11/2002 - admin@sbsc.net
/// Moved into Rainbow by Jakob Hansen , hansen3000@hotmail.com
/// Updated by Manu as Rainbow Tutorial
/// </summary>
public class MilestonesEdit : Rainbow.UI.EditItemPage
{
protected System.Web.UI.WebControls.TextBox TitleField;
protected System.Web.UI.WebControls.RequiredFieldValidator Req1;
protected System.Web.UI.WebControls.TextBox EstCompleteDate;
protected System.Web.UI.WebControls.RequiredFieldValidator Req2;
protected System.Web.UI.WebControls.CompareValidator VerifyCompleteDate;
protected System.Web.UI.WebControls.TextBox StatusBox;
protected System.Web.UI.WebControls.RequiredFieldValidator Req3;
protected System.Web.UI.WebControls.Label CreatedBy;
protected System.Web.UI.WebControls.Label CreatedDate;
private void Page_Load(object sender, System.EventArgs e)
{
// If the page is being requested the first time, determine if a
// Milestone itemId value is specified, and if so,
// populate the page contents with the Milestone details.
if (Page.IsPostBack == false)
{
//Item id is defined in base class
if (ItemId > 0)
{
//Obtain a single row of Milestone information.
MilestonesDB milestonesDB = new MilestonesDB();
SqlDataReader dr = milestonesDB.GetSingleMilestones(ItemId);
//Load the first row into the DataReader
dr.Read();
TitleField.Text = (String) dr["Title"];
EstCompleteDate.Text = ((DateTime) dr["EstCompleteDate"]).ToShortDateString();
StatusBox.Text = (String) dr["Status"];
CreatedBy.Text = (String) dr["CreatedByUser"];
CreatedDate.Text = ((DateTime) dr["CreatedDate"]).ToShortDateString();
dr.Close();
}
}
}
/// <summary>
/// This procedure is automatically
/// called on Update
/// </summary>
override protected void OnUpdate()
{
// Calling base we check if the user has rights on updating
base.OnUpdate();
// Update only if the entered data is Valid
if (Page.IsValid == true)
{
MilestonesDB milestonesDb = new MilestonesDB();
if (ItemId <= 0)
milestonesDb.AddMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
else
milestonesDb.UpdateMilestones(ItemId, ModuleId, Context.User.Identity.Name, DateTime.Now, TitleField.Text, DateTime.Parse(EstCompleteDate.Text), StatusBox.Text);
// Redirects to the referring page
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
}
/// <summary>
/// This procedure is automatically
/// called on Update
/// </summary>
override protected void OnDelete()
{
// Calling base we check if the user has rights on updating
base.OnUpdate();
if (ItemId > 0)
{
MilestonesDB milestonesDb = new MilestonesDB();
milestonesDb.DeleteMilestones(ItemId);
}
// This method is provided by the base class
this.RedirectBackToReferringPage();
}
#region Web Form Designer generated code
/// <summary>
/// Raises OnInitEvent
/// </summary>
/// <param name="e"></param>
protected override void OnInit(EventArgs e)
{
InitializeComponent();
base.OnInit(e);
}
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
}
}
Adding the New Portal Module to the Framework
The Milestones Portal Module is now complete. We have to rebuild our project before adding it to the portal.
The only step left to perform is to add the module to the portal framework by using the online administrator’s Module Definitions section. You can find it on the Admin All page because the Modules are installed to be common to all portals.
In this section, click on the Add New Module Type to bring up the page shown. Enter the info rmation for the new module and click Update. Remember to select the portals you want to be able to use the module. The module can then be added to the different tabs by using the online administrator’s “Tab Name and Layout” section.
If you get an error like: “Invalid module! External component has thrown an exception” there is an error parsing the control (the ascx html part). To get more info rmation about the specific error create an empty webform and add the control. Running the form may give a clue to identify and resolve the problem.
Figure 2. Module Type Definition
Conclusion
For More Information
· The complete documentation and source code can be obtained at http://www.rainbowportal.net
· Forums at: http://www.rainbowportal.net/AspNetForums
When they reject it, the module comes back available for editing by the editors. An email can be sent to the editors to explain what the problem is.
When they approve it, an email can be sent to the publisher.
浙公网安备 33010602011771号