Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

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

Введение

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

Заключение

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


Может пригодится:


Автор: Артём Озорнин
Прочитано: 3020
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (0)

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру