Rambler's Top100

Создание простого одностраничного портала.

Автор: Артём Озорнин
Опубликовано:  27 April 2003
Уровень:  Учебник
Проголосовало 80 читателей
Средняя оценка 3.91

Введение

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

Заключение

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

Rambler's Top100    ¦хщЄшэу@Mail.ru