ГлавнаяРегистрацияВход Приветствую Вас Гость | RSS
   
Меню сайта
Разделы новостей
mp3player
Главная » 2007 » Июль » 8 » Создание простого одностраничного портала.
Создание простого одностраничного портала.
21:02
Автор: Артём Озорнин (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. Чего хочется

Итак, попытаемся формализовать задачу. Из названия статьи :(=) очевидно, что хочется нам создать портал с одностраничной структурой, соответственно:

  1. Динамический контент портала должен формироваться из некоторого конечного количества пользовательских элементов управления - User Controls (далее называемых модулями).  Почему атомарной единицей портала удобнее всего выбирать пользовательский элемент управления, сказано много добрых и правильных слов :) в статье Dimona aka Manowar "Введение в пользовательские элементы управления" , так что на этом вопросе мы останавливаться не будем. Понятно, что функционал портала должен позволять достаточно свободно манипулировать месторасположением этих пользовательских элементов управления, ведь в зависимости от предназначения различных страниц один и тот же модуль может быть отображён в разных местах этих страниц, в разной последовательности относительно других модулей, либо не отображён вовсе, отсюда:
  2. Должна иметься возможность расположения модуля в любом из доступных контейнеров страницы.

    В нашем случае их три – additionalLeftPanel – контейнер для небольших по размеру модулей, mainLeftPanel – основной контейнер, используемый для отображения целевого контента страницы, и mainRightPanel, контейнер, используемый в случае, когда целевой контент сайта необходимо представить с разбиением на две колонки.
  3. Должна иметься возможность отображения модулей в предопределённом порядке.

Думаю, в особых комментариях данный этот пункт не нуждается - вряд ли вы (да и пользователь) хотите, что бы, например, модуль авторизации/регистрации пользователя каждый раз выводился в произвольном месте страницы, например, где–нибудь в нижней её части. Вот, наверное, и всё, чего мы можем хотеть на данном этапе выполнения проекта. Начинаем писать.

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, в процессе работы которого необходимые для корректного отображения запрашиваемой страницы модули загружаются и добавляются в заданные для них контейнеры в заданном порядке. В качестве выходных параметров хранимой процедуры мы получаем название страницы и изображение, ей соответствующее. Вот и всё :).

Заключение

Описанная реализация одностраничного портала наверняка имеет множество мелких недочётов и ненужностей, и не претендует на уникальность (или, упаси Боже, гениальность :)) однако, на мой взгляд, имея перед глазами описанную структуру, можно достаточно быстро и легко писать проекты не очень сложных по постановке задачи порталов. Почему и для кого была написана эта статья, я говорил в самом начале, так что не буду повторяться. Удачи!

Категория: ASP.Net | Просмотров: 546 | Добавил: VVS | Рейтинг: 0.0/0 |
Всего комментариев: 0
Имя *:
Email *:
Код *:
Поиск
Форма входа
Наш опрос
Чего Вам не хватает на сайте?
Всего ответов: 21
Друзья сайта
Статистика
Возраст