Friday, October 14, 2011
Personal .NET Portal
Introduction
This article describes how I build my Personal Portal (in other words: a Homepage). The idea was to develop a Portal that is easy to deploy (e.g. no Database) and easy to use.
These technical problems are solved in this solution
- Storage The portal doesn't use a database. Instead XML-Files are used. Reading/Writing is done with the XML Serializer and
Dataset
s. - Dynamically loading of Web User Controls The portal has Tabs and Modules. Modules are Web User Controls which are loaded at runtime. The definition is stored in a XML File.
- Authentication Forms Authentication is used. Users and Roles are stored in a XML File.
- HttpHandler A HttpHandler is used to access Tab-Pages.
Background
First I wanted to use the IBuySpy Portal. But it isn't (or wasn't) free and uses a database. After some searching at Codeproject and Sourceforge I decided to implement my own Portal.
This Project is hosted at Sourceforge. I would be happy about feedback and suggestions . Also new modules or improvements are welcome. A Demo Portal can be found here .
Using the code
Feel free to use the Portal under the GPL License or code snippets freely.
Summary
A Portal has Tabs. Each Tab can have Sub-Tabs and Modules. Modules are providing content. A Module can be placed right, in the middle or on the left side on a Tab.
A Module consists of a View Web User Control and a Edit Web User Control. The edit control is optional. Both controls are derived from the Module/EditModule class. Those classes are providing information (Reference, Name, etc.) and a configuration infrastructure. Modules are located in the Modules/<ModuleName> directory.
Tabs and Modules have Roles assigned. Users in those roles may view/edit Tabs/Modules. There are four built-in roles:
- Admin - Administrator Role. May edit/view everything
- User - Signed in User
- Everyone - Signed in User or Anonymous User
- Anonymous - Not signed in User
The Portal definition and Users are stored in a XML File.
Currently there is a "Frame" and a "Table" version (configured in the web.config file). This is how the Portal is rendered: In Frames or in a Table.
Classes
Class Name | Description |
---|---|
Definitions | |
PortalDefinition | This is the main Portal Definition class. It contains various helper functions and a list of Tabs. |
PortalDefinition.Tab | The Tab Definition. A Tab has a Name, Reference, Roles, Sub Tabs and left/middle/right Modules. |
PortalDefinition.Module | A Module has a Name, Reference and Roles. |
PortalDefinition.Role | A Role is just a Name and Base Class for View or Edit Roles |
PortalDefinition.EditRole | Edit Role. |
PortalDefinition.ViewRole | View Role. |
Users | User/Roles Dataset. |
ModuleSettings | Defines Module Settings like the Module Control Name. |
Controls/Rendering | |
EditLink | Renders the Edit Link. |
ModuleFailed | Renders a "Module failed to load". |
ModuleHeader | Renders the Module Header. |
OverlayMenu | Renders a Menu. |
OverlayMenuItem | Renders a Menu Item. |
PortalTab | This is the main Protal Render Control! |
TabMenu | Renders the Tab Menu. |
TabPath | Renders the current Tab Path. |
TabHttpHandler | The HttpModule for "translating" URLs. |
Helper | |
Helper | Common Helper Class. |
UserManagement | Usermanagement Methods. |
API | |
Config | Configuration Helpers. |
EditModule | Base Class for Module Edit Controls. |
Module | Base Class for Module View Controls. |
ASPX-Pages are used as container for Web User Controls. They have hardly program logic.
Storage
Two things must be stored: the Portal Definition and Users/Roles.
The Portal definition is stored with the XML-Serializer.
[XmlRoot("portal"), Serializable]
public class PortalDefinition
{
// Static Serializer
private static XmlSerializer xmlPortalDef =
new XmlSerializer(typeof(PortalDefinition));
...
// Loads the Portal Definition
public static PortalDefinition Load()
{
// Create a Text Reader which reads the file
XmlTextReader xmlReader = new XmlTextReader(
Config.GetPortalDefinitionPhysicalPath());
// Deserialize
PortalDefinition pd = (PortalDefinition)xmlPortalDef.Deserialize(
xmlReader);
// Close the file
xmlReader.Close();
return pd;
}
// Saves the Portal Definition
public void Save()
{
// Create a Text Writer which writes the file
XmlTextWriter xmlWriter = new XmlTextWriter(
Config.GetPortalDefinitionPhysicalPath(), System.Text.Encoding.UTF8);
// Set Formating
xmlWriter.Formatting = Formatting.Indented;
// Serialize
xmlPortalDef.Serialize(xmlWriter, this);
// Close the file
xmlWriter.Close();
}
...
}
Users are stored in a
Dataset
public class UserManagement
{
...
// Reads the User Dataset from a file
public static Users GetUsers()
{
Users u = new Users();
u.ReadXml(Config.GetUserListPhysicalPath());
return u;
}
// Writes the User Dataset to a file
public static void SetUsers(Users u)
{
u.WriteXml(Config.GetUserListPhysicalPath());
}
...
}
Authentication
Forms Authentication is used, but I can imagine that Windows Authentication would also work.
First, in the Login Module, your credentials are validated. This is done through a helper method.
Login.ascx
void OnLogin(object sender, EventArgs args)
{
if(Portal.UserManagement.Login(account.Text, password.Text))
{
Response.Redirect(Request.RawUrl);
}
else
{
lError.Text = "Invalid Login";
}
}
UserManagement.cs
public static bool Login(string account, string password)
{
// Load Dataset
Users u = GetUsers();
// Find user
Users.UserRow user = u.User.FindBylogin(account.ToLower());
if(user == null) return false;
// Check password
if(user.password != password) return false;
// Set Authentication Cookie
FormsAuthentication.SetAuthCookie(account, false);
return true;
}
When a Http-Request occurs,
Global.Application_AuthenticateRequest
is called. There the user roles are set.protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
if(Request.IsAuthenticated)
{
// Read users roles
string[] roles = UserManagement.GetRoles(
HttpContext.Current.User.Identity.Name);
// Set a new Principal with the proper roles
HttpContext.Current.User = new GenericPrincipal(
HttpContext.Current.User.Identity, roles);
}
}
UserManagement.cs
public static string[] GetRoles(string account)
{
// Load the User Dataset
Users u = GetUsers();
// Find the current user
Users.UserRow user = u.User.FindBylogin(account.ToLower());
if(user == null) return new string[0];
// Read users roles and add to a string array
Users.UserRoleRow[] roles = user.GetUserRoleRows();
string[] result = new string[roles.Length];
for(int i=0;i<roles.Length;i++)
{
result[i] = roles[i].RoleRow.name;
}
return result;
}
Loading Controls Dynamically
The PortalTab.ascx Web User Control renders the Tabs. The current Tab reference is passed as a URL Parameter. Modules are loaded in the
OnInit
Event so they can process their OnLoad
Events.public abstract class PortalTab : System.Web.UI.UserControl
{
// Table Cells where the Modules are rendered
protected HtmlTableCell left;
protected HtmlTableCell middle;
protected HtmlTableCell right;
// Renders a Module Column
private void RenderModules(HtmlTableCell td, PortalDefinition.Tab tab,
ArrayList modules)
{
// Hide the cell if there where no Modules
if(modules.Count == 0)
{
td.Visible = false;
return;
}
foreach(PortalDefinition.Module md in modules)
{
// Only if the User has view rights
if(UserManagement.HasViewRights(Page.User, md.roles))
{
md.LoadModuleSettings();
// Initialize the Module
Module m = null;
// Load the Module
if(md.moduleSettings == null)
{
m = (Module)LoadControl(Config.GetModuleVirtualPath(md.type)
+ md.type + ".ascx");
}
else
{
m = (Module)LoadControl(Config.GetModuleVirtualPath(md.type)
+ md.moduleSettings.ctrl);
}
// Initialize the Module
m.InitModule(tab.reference, md.reference,
Config.GetModuleVirtualPath(md.type),
UserManagement.HasEditRights(Page.User, md.roles));
// Each Module can decide if it wants to be rendered.
// The Login Module does so.
if(m.IsVisible())
{
// Add Module Header
ModuleHeader mh = (ModuleHeader)LoadControl("ModuleHeader.ascx");
mh.SetModuleConfig(md);
td.Controls.Add(mh);
// Add Module
HtmlGenericControl div = new HtmlGenericControl("div");
div.Attributes.Add("class", "Module");
div.Controls.Add(m);
td.Controls.Add(div);
}
}
}
}
// Called by the ASP.NET Framework
override protected void OnInit(EventArgs e)
{
PortalDefinition.Tab tab = PortalDefinition.GetCurrentTab();
// Check user rights
if(UserManagement.HasViewRights(Page.User, tab.roles))
{
// Render
RenderModules(left, tab, tab.left);
RenderModules(middle, tab, tab.middle);
RenderModules(right, tab, tab.right);
}
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
InitializeComponent();
base.OnInit(e);
}
...
}
HttpHandler
The
HttpHandler
is used to "translate" URLs from "http://server/Portal/main.tab.ascx" to"http://server/Portal/RenderTable.aspx?TabRef=main". This is interesting if you want to analyze your Web Server Log Files.To get this work you must add this to your web.config file
<system.web>
<httpHandlers>
<add verb="*" path="*.tab.aspx" type="Portal.TabHttpHandler, Portal" />
<httpHandlers>
<system.web>
".tab.aspx" is used as a extension, because otherwise you have to reconfigure your IIS.
This
HttpModule
does nothing else than a simple Server.Transfer
public class TabHttpHandler : IHttpHandler, IRequiresSessionState
{
public void ProcessRequest(HttpContext context)
{
// Parse the URL and extract the Tab Reference
string path = context.Request.Url.AbsolutePath.ToLower();
string tabRef = path.Substring(path.LastIndexOf("/") + 1);
// get "TabRef.tab"
tabRef = tabRef.Substring(0, tabRef.LastIndexOf(".tab.aspx"));
// get "TabRef"
// Save URL Parameter
Hashtable r = new Hashtable();
foreach(string key in context.Request.QueryString.Keys)
{
r[key] = context.Request[key];
}
r["TabRef"] = tabRef;
// Read the configuration to determinate the current main page
string url = Portal.API.Config.GetMainPage();
// Build the URL
bool firstParam = true;
foreach(DictionaryEntry e in r)
{
if(firstParam)
{
url += "?";
firstParam = false;
}
else
{
url += "&";
}
url += e.Key.ToString() + "=" + e.Value.ToString();
}
// Redirect
context.Server.Transfer(url);
}
public bool IsReusable
{
get
{
return true;
}
}
}
Creating a Module
Creating a Module is simple. Just create a directory in the Modules directory and put there the View- and Edit Web User Control. The Controls names must be (or can be reconfigured) <ModuleName>.ascx and Edit<ModuleName>.ascx.
Implementation
Just derive form
The Module Class provides some properties and methods which describes the Module.Portal.API.Module
or Portal.API.EditModule
and implement "IsVisible
" if necessary.Current Module Settings
Name | Description |
---|---|
IsVisible | Can be overridden. Tells the Portal Framework if the Module should be rendered or not. |
TabRef | The current Tab Reference. This is a unique string. |
ModuleRef | The current Module Reference. The Module Reference is not necessarily unique. TabRef + ModuleRef is unique. |
ModuleVirtualPath | The virtual path to the Module. |
ModulePhysicalPath | The physical path to the Module. |
BuildURL | Build a URL to the current Page. Use this method to implement Modules that needs URL Parameter. |
ModuleHasEditRights | True if the current user has edit rights. |
Configuration
Each Module has the responsibility to store its configuration and state. The Portal API provides some Helper Methods.
Name | Description |
---|---|
ModuleConfigFile | Physical Path to the configuration file. (<ModulePhysicalPath>\Module_<ModuleRef>.config) |
ModuleConfigSchemaFile | Physical Path to the configuration schema file. (<ModulePhysicalPath>\ Module_<ModuleRef>.configModule.xsd) |
ReadCommonConfig | Reads (XML Deserialize) the common configuration file. |
ReadConfig | Reads (XML Deserialize/Dataset) the configuration file. |
WriteConfig | Writes (XML Serialize/Dataset) the configuration file. |
Furthermore each Module can a configure its control files. These settings are stored in the "ModuleSettings.config" file.
<module>
<ctrl>Counter.ascx</ctrl>
<editCtrl>none</editCtrl>
</module>
The "ctrl" Tag defines the View Web User Control. The "editCtrl" Tag can contain "none", which means: there is no Edit Control. E.g. the Login or HitCounter Modules are using this.
Example (Simple Html Module)
This simple Module reads a .htm file and renders it into a
DIV
Tag.View Control Html.ascx:
<%@ Control Language="c#" Inherits="Portal.API.Module" %>
<%@ Import namespace="System.IO" %>
<script runat="server">
private string GetPath()
{
return ModulePhysicalPath + ModuleRef + ".htm";
}
void Page_Load(object sender, EventArgs args)
{
// Open file
if(File.Exists(GetPath()))
{
FileStream fs = File.OpenRead(GetPath());
StreamReader sr = new StreamReader(fs);
content.InnerHtml = sr.ReadToEnd();
fs.Close();
}
}
</script>
<div id="content" runat="server">
</div>
Edit Control EditHtml.ascx:
<%@ Control Language="c#" autoeventwireup="true"
Inherits="Portal.API.EditModule" %>
<%@ Import namespace="System.IO" %>
<script runat="server">
private string GetPath()
{
return ModulePhysicalPath + ModuleRef + ".htm";
}
void Page_Load(object sender, EventArgs args)
{
if(!IsPostBack)
{
// Open file
if(File.Exists(GetPath()))
{
FileStream fs = File.OpenRead(GetPath());
StreamReader sr = new StreamReader(fs);
txt.Text = sr.ReadToEnd();
fs.Close();
}
}
}
void OnSave(object sender, EventArgs args)
{
FileStream fs = null;
try
{
fs = new FileStream(GetPath(), FileMode.OpenOrCreate,
FileAccess.ReadWrite, FileShare.None);
fs.SetLength(0); // Truncate
StreamWriter sw = new StreamWriter(fs);
sw.Write(txt.Text);
sw.Close();
}
finally
{
if(fs != null)
{
fs.Close();
}
}
RedirectBack();
}
</script>
<asp:TextBox id="txt" Width="100%" Height="300px"
TextMode="MultiLine" Runat="server"></asp:TextBox>
<asp:LinkButton CssClass="LinkButton" runat="server" OnClick="OnSave">
Save & Back</asp:LinkButton>