Автор:
Артём Озорнин (
oz@as.ru)
Источник:
www.aspnetmania.comВведение
На Мании не один раз поднималась тема создания одностраничных порталов, при этом обычно обсуждался IBuySpy Portal. Причина написания этой статьи одновременно является и причиной самой возможности её написания, и она очень проста: у меня до сих пор не хватает времени более или менее основательно разобраться со структурой и принципами функционирования данного Shared Source проекта, и, соответственно, его использовать, поэтому пришлось реализовывать собственный engine. Предполагая, что не один я нахожусь в ситуации острой нехватки времени (и/или слабого знания английского, которое у меня наличествовало на момент разработки собственного одностраничного портала), я решил, что мой опыт решения названной выше задачи, оформленный в виде статьи, может быть полезен некоторым начинающим ASP.NET программерам. Так что начнём, пожалуй :).
1. Что было в начале
То, что и бывает в начале – концепт, созданный дизайнером :). Приведён ниже.
И, созданный совместно с дизайнером макет html страницы:
Представленный макет немного отличается от оригинального – названия additionalLeftPanel, mainLeftPanel и mainRightPanel вставлены мною в одноимённые ячейки таблиц для наглядности местонахождения этих ячеек. Эти панели, как вы наверное догадываетесь, являются контейнерами для динамического контента проектируемого сайта. Пожалуй, в данном разделе писать больше нечего, кроме небольшого примечания: статья написана по мотивам сайта, на момент написания статьи доступного по адресу http://www.new.as.ru, а к моменту публикации статьи, возможно, данный сайт уже будет доступен по адресу http://www.as.ru
2. Чего хочется
Итак, попытаемся формализовать задачу. Из названия статьи :(=) очевидно, что хочется нам создать портал с одностраничной структурой, соответственно:
- Динамический контент портала должен формироваться из некоторого конечного количества пользовательских элементов управления - User Controls (далее называемых модулями). Почему атомарной единицей портала удобнее всего выбирать пользовательский элемент управления, сказано много добрых и правильных слов :) в статье Dimona aka Manowar "Введение в пользовательские элементы управления" , так что на этом вопросе мы останавливаться не будем. Понятно, что функционал портала должен позволять достаточно свободно манипулировать месторасположением этих пользовательских элементов управления, ведь в зависимости от предназначения различных страниц один и тот же модуль может быть отображён в разных местах этих страниц, в разной последовательности относительно других модулей, либо не отображён вовсе, отсюда:
- Должна иметься возможность расположения модуля в любом из доступных контейнеров страницы.
В нашем случае их три – additionalLeftPanel – контейнер для небольших по размеру модулей, mainLeftPanel – основной контейнер, используемый для отображения целевого контента страницы, и mainRightPanel, контейнер, используемый в случае, когда целевой контент сайта необходимо представить с разбиением на две колонки.
- Должна иметься возможность отображения модулей в предопределённом порядке.
Думаю, в особых комментариях данный этот пункт не нуждается - вряд ли вы (да и пользователь) хотите, что бы, например, модуль авторизации/регистрации пользователя каждый раз выводился в произвольном месте страницы, например, где–нибудь в нижней её части. Вот, наверное, и всё, чего мы можем хотеть на данном этапе выполнения проекта. Начинаем писать.
3. Реализация3.1 Разработка структуры БД
Точнее, сегмента БД, который будет хранить информацию о структуре нашего портала. Так как наш портал будет строиться из кирпичиков–модулей, первая сущность, которую нам необходимо описать, это модули. Описываем:
CREATE TABLE [Modules]
(
[Id] [int] NOT NULL IDENTITY (1, 1),
[Name] [varchar] (32) NOT NULL CONSTRAINT [DF_Modules_Name] DEFAULT ('New module'),
[Path] [varchar] (1024) NOT NULL ,
[Description] [varchar] (2048) NOT NULL ,
CONSTRAINT [PK_Modules] PRIMARY KEY CLUSTERED ( [Id] ) ON [PRIMARY],
CONSTRAINT [IX_Modules] UNIQUE NONCLUSTERED ( [Name] ) ON [PRIMARY]
) ON [PRIMARY]
GO
Id |
– идентификатор, он же первичный ключ, |
Name |
– название модуля, |
Path |
– относительный путь к файлу модуля от виртуального каталога портала, |
Description |
– описание модуля, некоторый набор комментариев. |
Помимо самих модулей, нам нужно место, куда их поместить, соответственно следующая подлежащая описанию сущность – контейнеры:
CREATE TABLE [Containers]
(
[Id] [int] NOT NULL IDENTITY (1, 1),
[Name] [varchar] (32) NOT NULL CONSTRAINT [DF_Containers_Name] DEFAULT ('NewContainer'),
[Description] [varchar] (2048) NOT NULL CONSTRAINT [DF_Containers_Description] DEFAULT ('Описание отсутсвует'),
CONSTRAINT [PK_Containers] PRIMARY KEY CLUSTERED ( [Id] ) ON [PRIMARY],
CONSTRAINT [IX_Containers] UNIQUE NONCLUSTERED ( [Name] ) ON [PRIMARY]
) ON [PRIMARY]
GO
Id |
– идентификатор, он же первичный ключ, |
Name |
– название контейнера, |
Description |
– описание контейнера, некоторый набор комментариев. |
В нашем случае имеет место одна страница и три контейнера, соответственно содержимое данной таблицы выглядит так:
И контейнеры, и модули должны отображаться на странице, следовательно, нам нужна сущность, описывающая страницы портала. Может быть, правильнее было бы назвать её представлениями страницы (так как страница то у нас будет одна), но у меня она называется Pages.
CREATE TABLE [Pages]
(
[Id] [int] NOT NULL IDENTITY (1, 1),
[Name] [varchar] (32) NOT NULL,
[Header] [varchar] (256) NOT NULL CONSTRAINT [DF_Pages_Header]
DEFAULT ('Новая страница сайта'),
[ImagePath] [varchar] (1024) NOT NULL,
[Description] [varchar] (2048) NOT NULL CONSTRAINT [DF_Pages_Description]
DEFAULT ('Описание отсутствует'),
CONSTRAINT [PK_Pages] PRIMARY KEY CLUSTERED ( [Id] ) ON [PRIMARY],
CONSTRAINT [IX_Pages] UNIQUE NONCLUSTERED ( [Name] ) ON [PRIMARY]
) ON [PRIMARY]
GO
Id |
– идентификатор, он же первичный ключ, |
Name |
– название страницы, |
Header |
– заголовок страницы |
ImagePath |
– относительный путь к картинке, соответствующей данной странице, от виртуального каталога портала. |
Description |
– описание страницы, некоторый набор комментариев. |
Итак, у нас есть таблицы, описывающие страницы, модули и контейнеры. Теперь нам необходимо некое правило, по которому одни помещаются в другие, и задающее порядок их размещения (конкретно мы это сформулировали в разделе 2). Для реализации этого создаём таблицу PagesContents:
CREATE TABLE [PagesContents]
(
[PageId] [int] NOT NULL,
[ModuleId] [int] NOT NULL,
[ContainerId] [int] NOT NULL CONSTRAINT [DF_PagesContents_ModuleLayout] DEFAULT (2),
[ModuleOrder] [int] NOT NULL, CONSTRAINT [PK_PagesContents]
PRIMARY KEY CLUSTERED ( [PageId], [ModuleId] ) ON [PRIMARY],
CONSTRAINT [FK_PagesContents_Containers] FOREIGN KEY ( [ContainerId] ) REFERENCES [Containers] ( [Id] ),
CONSTRAINT [FK_PagesContents_Modules] FOREIGN KEY ( [ModuleId] ) REFERENCES [Modules] ( [Id] ),
CONSTRAINT [FK_PagesContents_Pages] FOREIGN KEY ( [PageId] ) REFERENCES [Pages] ( [Id] )
) ON [PRIMARY]
GO
PageId |
– внешний ключ на идентификатор страницы, |
ModuleId |
– внешний ключ на идентификатор модуля, |
ContainerId |
– внешний ключ на идентификатор контейнера, |
ModuleOrder |
– порядок отображения модуля. |
Несколько слов о первичном ключе этой таблицы: работая над структурой сегмента БД, отвечающего за структуру проектируемого портала, я исходил из того, что один и тот же модуль не может отображаться на одной и той же странице больше одного раза, поэтому ключ у меня состоит из двух полей, но в принципе возможна ситуация, когда один и тот же модуль может отображаться больше одного раза даже в одном контейнере, поэтому формирование Primary Key для данной страницы – по ситуации и желанию. Посмотрим на диаграмму, демонстрирующую созданные нами таблицы:
Как оказалось, всё достаточно просто : некоторое количество строк (одна строка описывает один модуль) таблицы PagesContetnts может полностью описать страницу портала. Если написать SELECT запрос с условием отбора по заданному PageId, то как раз получим описание страницы с этим идентификатором: модули, принадлежащие странице, принадлежность модулей конкретным контейнером, последовательность расположения модулей в конкретном контейнере. Но мы не будем писать SELECT, лучше напишем хранимую процедуру:
CREATE PROCEDURE dbo.GetPageSettings
(
@PageId int = NULL,
@PageName varchar(32) = NULL,
@Header varchar(256) OUTPUT,
@ImagePath varchar(1024) OUTPUT )
AS
BEGIN
-- Получаем информацию о заголовке, иконке и внешнем виде виде страницы
SELECT
@PageId = Id,
@Header = Header,
@ImagePath = ImagePath
FROM
Pages
WHERE
Id = @PageId OR
Name = @PageName
-- Получаем информацию о наборе модулей на странице
SELECT
M.Name AS ModuleName,
M.Path AS ModulePath,
C.Name AS ModuleContainer,
PC.ModuleOrder AS ModuleOrder
FROM
PagesContents PC JOIN Modules M
ON PC.PageId = @PageId AND PC.ModuleId = M.Id
JOIN Containers C ON PC.ContainerId = C.Id
ORDER BY
C.Name,
PC.ModuleOrder
RETURN
END
GO
Здесь тоже всё просто – входным параметром процедуры может быть имя либо идентификатор страницы, а выходными параметрами является заголовок страницы, соответствующее ей изображение, ей и набор строк, содержащих информацию о модулях, отображаемых на странице. Мы создали таблицы, необходимые для хранения информации о структуре одностраничного портала, и написали хранимую процедуру, возвращающую полное описание странице, заданной входным параметром этой процедуры. На том с Sql-серверной частью и закончим. Переходим к кодингу.
3.2 Кодинг портала
Я приведу полный код страницы портала, а затем дам некоторые пояснения.
using System;
using System.Configuration;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using Aequitas.Data;
namespace Aequitas
{
/// <summary>
/// Summary description for WebForm1.
/// </summary>
public class Default : System.Web.UI.Page
{
#region Protected fields
protected System.Web.UI.HtmlControls.HtmlTableCell additionalLeftPanel;
protected System.Web.UI.HtmlControls.HtmlTableCell mainLeftPanel;
protected System.Web.UI.HtmlControls.HtmlTableCell mainRightPanel;
protected System.Web.UI.HtmlControls.HtmlGenericControl title;
protected System.Web.UI.HtmlControls.HtmlImage toolTip;
protected System.Data.SqlClient.SqlCommand sqlGetPageSettingsCommand;
#endregion Protected fields
#region Private properties
/// <summary>
/// Свойство определяет, является ли запрос этой страницы первым
/// </summary>
private bool IsFirstVisit
{
get
{
return (this.Request.Cookies["NotFirstVisit"] == null);
}
}
#endregion Private properties
#region Constructors
public Default()
{
Page.Init += new System.EventHandler(Page_Init);
}
#endregion Constructors
#region Private methods
private void Page_Init(object sender, EventArgs e)
{
//
// CODEGEN: This call is required by the ASP.NET Web Form Designer.
//
this.InitializeComponent();
this.CustomInitializeComponent();
}
private void Page_Load(object sender, System.EventArgs e)
{
// Put user code to initialize the page here
}
#region Web Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.sqlGetPageSettingsCommand = new System.Data.SqlClient.SqlCommand();
//
// sqlGetPageSettingsCommand
//
this.sqlGetPageSettingsCommand.CommandText = "dbo.[GetPageSettings]";
this.sqlGetPageSettingsCommand.CommandType = System.Data.CommandType.StoredProcedure;
this.sqlGetPageSettingsCommand.Parameters
.Add(new System.Data.SqlClient.SqlParameter("@RETURN_VALUE",
System.Data.SqlDbType.Int, 4,
System.Data.ParameterDirection.ReturnValue,
false,
((System.Byte)(10)),
((System.Byte)(0)),
"",
System.Data.DataRowVersion.Current,
null));
this.sqlGetPageSettingsCommand.Parameters
.Add(new System.Data.SqlClient.SqlParameter("@PageId",
System.Data.SqlDbType.Int,
4,
System.Data.ParameterDirection.Input,
false,
((System.Byte)(10)),
((System.Byte)(0)),
"",
System.Data.DataRowVersion.Current,
null));
this.sqlGetPageSettingsCommand.Parameters
.Add(new System.Data.SqlClient.SqlParameter("@PageName",
System.Data.SqlDbType.VarChar,
32));
this.sqlGetPageSettingsCommand.Parameters
.Add(new System.Data.SqlClient.SqlParameter("@Header",
System.Data.SqlDbType.VarChar,
256,
System.Data.ParameterDirection.Output,
false,
((System.Byte)(0)),
((System.Byte)(0)),
"",
System.Data.DataRowVersion.Current,
null));
this.sqlGetPageSettingsCommand.Parameters
.Add(new System.Data.SqlClient.SqlParameter("@ImagePath",
System.Data.SqlDbType.VarChar,
1024,
System.Data.ParameterDirection.Output,
false,
((System.Byte)(0)),
((System.Byte)(0)),
"",
System.Data.DataRowVersion.Current,
null));
this.Load += new System.EventHandler(this.Page_Load);
}
#endregion
#region Further to Web Form Designer generated code
#endregion
/// <summary>
/// Метод инициализации компонентов, дополняет функционал метода
/// InitializeComponent
/// </summary>
private void CustomInitializeComponent()
{
this.additionalLeftPanel.Visible = false;
this.mainLeftPanel.Visible = false;
this.mainRightPanel.Visible = false;
SqlClient.Processing(new SqlClient.ProcessingLogic(this.BuildPage));
}
/// <summary>
/// Метод реализует получение информации о запрашиваемой странице и
/// загружает все необходимые для данной страницы модули (UC)
/// </summary>
private void BuildPage(SqlConnection sqlConnection)
{
this.sqlGetPageSettingsCommand.Connection = sqlConnection;
// Задаём параметры хранимой процедуры для запрашиваемой страницы
if (this.Request.QueryString["PageId"] != null)
{
this.sqlGetPageSettingsCommand
.Parameters["@PageId"].Value = this.Request.QueryString["PageId"];
}
else
{
if (this.Request.QueryString["PageName"] != null)
{
this.sqlGetPageSettingsCommand
.Parameters["@PageName"].Value = this.Request.QueryString["PageName"];
}
else
{
this.sqlGetPageSettingsCommand
.Parameters["@PageName"].Value = ConfigurationSettings.AppSettings["PageDefaultName"];
}
}
// Заполняем контейнеры страницы соответствующими модулями
SqlDataReader sqlDataReader = this.sqlGetPageSettingsCommand.ExecuteReader();
string currentModuleContainerName = "";
Control moduleContainerControl = new Control();
while(sqlDataReader.Read())
{
if (currentModuleContainerName != sqlDataReader["ModuleContainer"].ToString().Trim())
{
currentModuleContainerName = sqlDataReader["ModuleContainer"].ToString().Trim();
moduleContainerControl = this.FindControl(currentModuleContainerName);
}
// this.FindControl может вернуть null, если Control с таким именем
// отсутствует на странице, поэтому переходим к следующему модулю.
if (moduleContainerControl == null)
{
continue;
}
// Пробуем загрузить модуль в контрол - контейнер
try
{
moduleContainerControl.Controls
.Add(this.LoadControl(sqlDataReader["ModulePath"].ToString()));
// Поскольку контрол нормально загрузили, делаем его видимым
if (moduleContainerControl.Visible != true)
{
moduleContainerControl.Visible = true;
}
}
catch(System.IO.FileNotFoundException ex)
{
// Ничего не делаем :(
}
}
sqlDataReader.Close();
if (this.IsFirstVisit)
{
this.toolTip.Src = "Resources/Menu/PervertMenu/ToolTips/WelcomeHand.gif";
this.Response.Cookies["NotFirstVisit"].Value = Convert.ToString(true);
this.Response.Cookies["NotFirstVisit"].Expires = DateTime.Now.AddYears(5);
}
else
{
// Получаем иконку страницы
this.toolTip.Src = this.sqlGetPageSettingsCommand.Parameters["@ImagePath"]
.Value.ToString();
}
// Получаем заголовок страницы
this.title.InnerText = this.sqlGetPageSettingsCommand.Parameters["@Header"]
.Value.ToString().ToUpper();
}
#endregion
}
}
Зачем нужны additionalLeftPanel, mainLeftPanel и mainRightPanel мы уже знаем, HtmlGenericControl title – это заголовок страницы, представленный на клиентской стороне тэгом DIV . C равным, а точнее, большим успехом это может быть Label или LiteralControl. То, что я использую DIV и его серверное представление HtmlGenericControl, скорее частный случай. HtmlImage toolTip – это изображение, соответствующее запрошенной пользователем страницы. Поля Header и ImagePath таблицы Pages, описанной в разделе 3.1, отображаются серверными элементами title и toolTip. SqlCommand sqlGetPageSettingsCommand будет использоваться для получения результатов работы хранимой процедуры GetPageSettings, также описанной в разделе 3.1. Назначение свойства IsFirstVisit достаточно очевидно, и если честно, к теме статьи имеет мало отношения. В зависимости от значения этого свойства в HtmlImage toolTip выводится либо “родное” изображение страницы, либо, если это первый визит пользователя, изображение, соответствующее первому визиту пользователя. В конструкторе страницы происходит подписка метода Page_Init на событие Init. Сам метод последовательно вызывает сгенерированный дизайнером метод InitializeComponent, работа которого в нашем случае сводится к заполнению свойств sqlGetPageSettingsCommand и метод CustomInitializeComponent, делающий невидимыми наши контейнеры additionalLeftPanel, mainLeftPanel и mainRightPanel – т.к. на момент запроса страницы неизвестно, какие из них нам понадобятся, и выполняющий статический метод SqlClient.Processing, в который при помощи делегирования передаётся метод BuildPage, реализует операции открытия / закрытия соединения с Sql сервер. Код класса, подобного этому, я приводил здесь . Метод BuildPage – это как раз реализация задачи построения страницы из некоторого набора модулей, можно даже сказать, инкапсуляция логики построения нашего одностраничного портала :). И, как можно легко увидеть, ничего гениального он в себе не содержит: в зависимости от параметров запроса страницы формируются параметры для передачи в хранимую процедуру GetPageSettings, создаётся sqlDataReader, в процессе работы которого необходимые для корректного отображения запрашиваемой страницы модули загружаются и добавляются в заданные для них контейнеры в заданном порядке. В качестве выходных параметров хранимой процедуры мы получаем название страницы и изображение, ей соответствующее. Вот и всё :).
Заключение
Описанная реализация одностраничного портала наверняка имеет множество мелких недочётов и ненужностей, и не претендует на уникальность (или, упаси Боже, гениальность :)) однако, на мой взгляд, имея перед глазами описанную структуру, можно достаточно быстро и легко писать проекты не очень сложных по постановке задачи порталов. Почему и для кого была написана эта статья, я говорил в самом начале, так что не буду повторяться. Удачи!