вторник, 30 марта 2010 г.

Пишем CMS. Шаг № 8. Unit-тестирование

Это архиполезная вещь, особенно в проектах, которые имеют функционал работы с данными, с БД, например. Я расскажу, как я организую юнит-тестирование в своих проектах.

1. Перво-наперво качаем последнюю версию NUnit (откуда, найдете сами).

2. Создаем в солюшене отдельный проект типа ClassLibrary и добавляем референс на библиотеку nunit.framework.dll, которая лежит в папке framework каталога, куда распаковался наш nunit.

3. Создаем новый класс, добавляем в него ссылку на пространство имен NUnit.Framework, затем перед классом добавляем атрибут [TestFixture], а перед методом-тестом (который должен быть обязательно public и возвращать void - атрибут [Test].

4. Теперь надо организовать удобный запуск тестов. Я обычно делаю так. В окне свойств проекта с тестами щелкаю вкладку Debug, а там - Start external program, где указываю путь к nunit.exe. А сам проект устанавливаю как SetUp as statrup project. Это значит, что теперь, когда я нажимаю F5, запускается именно этот проект, вернее, запускается оболочка nunit.exe, которая подтягивает наши проекты.

5. Теперь надо создать проект среды тестирования. Для этого нажмем F5 и увидим диалог NUnit, там мы выберем меню New Project, введем название и сохраним проект где-нибудь (я предпочитаю корневую папку проекта с тестами). Теперь можно добавить туда нашу dll с тестами (она обычно лежит в bin/debug) - через меню Project - Add assembly (некоторые версии NUnit имеют такой баг - после того, как вы создаете проект и загружаете туда dll, он не подхватывает библиотеку, выдает какую-то странную ошибку. Если это произошло, просто сохраните проект и закройте оболочку, затем запустите снова - должно пройти.)

6. Ну вот и все! Теперь можете писать свои тесты и запускать их. Да! Одна маленькая, но очень важная деталь - если вы не обратите на это внимание, вы можете часами биться об стену, пытаясь понять, в чем дело. А дело в том, что если вы работаете например, с ADO.Net Entity Framework и вам надо потестировать, хорошо ли из базы забираются данные, то вы обязательно должны скопировать файл App.Config в папку проекта с тестами - именно отсюда фреймворк будет брать информацию по соединению с БД. Но и это еще не все. В свойствах NUnit проекта в поле Configuration File Name нужно обязательно указать имя этого конфигурационного файла.

Теперь, кажется, все... удачного тестирования!

среда, 17 марта 2010 г.

Пишем CMS. Шаг № 7. Шаг вперед, два шага назад. Рефакторинг

Что-то мне не нравится контрол, который отвечает за показ результата на странице. Во-первых, надо добавить информацию, через сколько времени и куда будет перемещен пользователь, и что ему делать, если браузер не поддерживает перенаправления. Во-вторых, надо разбить метод Activate на два - один будет просто показывать сообщение об ошибке, а второй будет показывать подробную инфу, с ссылками, со всем прочим.

Добавим в ресурсный файл Messages переменную по имени TransferPageMessage, что-то вроде "Вы будете перемещены на страницу {0} через {1} сек. Или, если ваш браузер не поддерживает автоматическое перенаправление, перейдите по ссылке ниже:"

Изменим содержимое контрола:
  <asp:Panel ID="panelInfo" runat="server" Visible="true">
    <asp:Label ID="labInfo" runat="server" Text="" CssClass="LabelInfo"></asp:Label>
    <asp:Label ID="labLink" runat="server" CssClass="LabelInfo" Visible="false"></asp:Label> 
    <asp:HyperLink ID="linkInfo" runat="server" CssClass="LinkInfo" Visible="false">
    </asp:HyperLink>
  </asp:Panel>


* This source code was highlighted with Source Code Highlighter.


А теперь обновим код:

public void ShowFail(string Message)
{
  this.Visible = true;
  panelInfo.CssClass = "PanelInfoFail";
  if (Message != string.Empty)
    labInfo.Text = Message;
}
public void ShowOk(string Message, string PageToName, string LinkLabelCode, string Link, int SecondsToTransfer)
{
  this.Visible = true;
  panelInfo.CssClass = "PanelInfoOK";
  if (Message != string.Empty)
    labInfo.Text = Message + "<br><br>";
  if (PageToName != string.Empty)
  {
    labLink.Visible = true;
    labLink.Text = string.Format(Messages.TransferPageMessage, PageToName, SecondsToTransfer) + "<br><br>";
  }
  if (LinkLabelCode != string.Empty && Link != string.Empty)
  {
    linkInfo.Visible = true;
    linkInfo.Text = LinkLabelCode;
    linkInfo.NavigateUrl = Link;
  }
}

* This source code was highlighted with Source Code Highlighter.


По-моему, стало лучше. И не забудем обновить вызовы в коде нашей страницы:
try
{
  if (context == null)
    context = new PBPWebDatabaseConnection();
  if (languageObject != null)
  {
    languageObject.LanguageTitle = Server.HtmlEncode(txtLangName.Text);
    languageObject.LanguageCode = Server.HtmlEncode(txtLangCode.Text);
    if (filePathToUpload != "")
      languageObject.IconName = Path.GetFileName(filePathToUpload);
    if (languageObject.LanguageId == 0)
      context.AddObject("Language", languageObject);

    int saved = context.SaveChanges();
    if (saved == 1)
    {
      panelInfo.ShowOk(Messages.LanguageSaveOK, Messages.LanguagePageToTransfer,
        Messages.LabelLinkToLanguages, Pages.Languages, 4);
      Master.AddRedirect(Pages.Languages, 4);
      panelMain.Visible = false;
      result = true;
    }
  }
}
catch (Exception ex)
{
  //log
}
if (!result)
  panelInfo.ShowFail(Messages.LanguageSaveFail);

* This source code was highlighted with Source Code Highlighter.


До идеала, конечно, далеко, но мы постепенно будем улучшать код. Вот такие сообщения у меня получились в итоге:

четверг, 11 марта 2010 г.

Пишем CMS. Шаг № 6. Добавление нового языка

Продолжим.
Сегодня мы добавим функциональность на страницу LanguageEdit.aspx, и сможем добавлять новые языки или редактировать уже существующие.

Для начала определим две переменные: одна будет представлять собой объект Language, который мы создали еще раньше с помощью ADO.NET Framework Entity, а вторая - собственно инструмент работы с БД - контекст:
private Language languageObject;
private DatabaseConnection context;


* This source code was highlighted with Source Code Highlighter.


Далее в обработчик загрузки страницы Page_Load добавим проверку на наличие id:
      if (Request["id"] != null)
      {
        int id = 0;
        int.TryParse(Request["id"], out id);
        if (id != 0)
        {
          try
          {
            context = new DatabaseConnection();
              var lan = from l in context.Language
                   where l.LanguageId == id
                   select l;
              languageObject = (Language)lan.First();
          }
          catch
          {
            //log
          }
        }
      }
      if (languageObject == null)
      {
        languageObject = new Language();
      }
      if (!IsPostBack)
        Page.DataBind();


* This source code was highlighted with Source Code Highlighter.


Логика работы этого блока проста: если мы получаем в качестве параметра id, то извлекаем из базы объект по этому id. Если же параметра нет, значит, мы хотим создать новый язык.

В обработчик события Unload можно добавить "уничтожение" объекта context.Dispose(), хотя, полагаю, ресурсы и так будут автоматически очищены по завершении обработки страницы.

Теперь разместим на странице наши контролы, для начала это буду два поля ввода - в одном заголовок языка, во втором его кодовое представление (например, "en-EN"), с его помощью удобно сразу выставлять региональные настройки. Позднее мы добавим контрол загрузки файла, с помощью которого будем грузить на сервер иконки с изображением флага, а пока надо разобраться с основными вещами.

Мы подошли к ключевому вопросу - привязка данных к форме. ASP.NET предлагает несколько контролов для привязки данных, но после некоторой возни с ним мне пришлось отказаться от их использования. Фактически, только два контрола - DetailsView и FormView - позволяют отображать данные по одному объекту, но мне так и не удалось заставить их привязываться к EntityObject. По какой-то, неведомой мне причине, эти контролы в качестве источника данных требуют обязательно объект, который наследут IListSource, IEnumerable, или IDataSource. Наш объект EntityObject не реализует ни один из этих интерфейсов. В качестве источника данных можно было бы использовать ObjectDataSource, но для этого нужно соблюсти определенные требования к классам доступа DAL - поскольку мы используем классы, сгенерированные EntityFramework, использование их не получается. Можно, конечно, было бы написать обертку к этим классам, но плодить лишний код не хочется совершенно.

После некоторых раздумий, а также просмотра обновленного кода проекта TheBeerHouse пришлось остановится на простой односторонней привязке данных к объекту. Что это значит? Очень просто - при открытии страницы мы создаем объект, а наши поля, привязанные к нему, заполняются автоматически. Когда мы хотим сохранить измененный объект, мы извлекаем содержимое полей вручную. Полагаю, ручное извлечение неизбежно - ведь объект еще не сохранен, а значит, поля невозможно привязать. Если кто подскажет более удачное решение, будет здорово.

Итак, создаем на странице простое свойство, которое имеет только метод get и возвращает наш объект:
    protected Language LanguageObject
    {
      get
      {
        return languageObject;
      }
    }


* This source code was highlighted with Source Code Highlighter.


На страницу добавляем текстбоксы и осуществляем привязку, например:
<asp:TextBox ID="txtLangCode" runat="server"
          Text="<%# LanguageObject.LanguageCode %>" Width="214px" />


* This source code was highlighted with Source Code Highlighter.


А теперь вернемся к первому коду, обратите внимание:
if (!IsPostBack)
    Page.DataBind();


* This source code was highlighted with Source Code Highlighter.


Этот код проверяет, был ли постбэк, и, если нет, привязывает все контролы формы. Если мы будем делать привязку и после постбэка, то введенные нами данные в форму будут затерты извлеченными из объекта, что нам совсем не нужно.

Ну вот, осталось дело за малым: сохранить измененные данные. Кидаем на форму кнопку, добавляем событие Click и в обработчике пишем примерно такой код:
bool result = false;
try
      {
        if (context == null)
          context = new PBPWebDatabaseConnection();
        if (languageObject != null)
        {
          languageObject.LanguageTitle = Server.HtmlEncode(txtLangName.Text);
          languageObject.LanguageCode = Server.HtmlEncode(txtLangCode.Text);
          if (filePathToUpload != "")
            languageObject.IconName = Path.GetFileName(filePathToUpload);
          if (languageObject.LanguageId == 0)
            context.AddObject("Language", languageObject);

          int saved = context.SaveChanges();
          if (saved == 1)
          {
            panelInfo.Activate(true, Messages.LanguageSaveOK, Messages.LabelLinkToLanguages, Pages.Languages);
            Master.AddRedirect(Pages.Languages, 4);
            panelMain.Visible = false;
            result = true;
          }
        }
      }
      catch (Exception ex)
      {
        //log
      }
      if (!result)
        panelInfo.Activate(false, Messages.LanguageSaveFail, string.Empty, string.Empty);


* This source code was highlighted with Source Code Highlighter.


Сначала мы проверяем, не нуль ли контекст. Если вдруг он равен null, создаем новый. Объект languageObject уже должен быть создан к этому времени (пустой или взятый из базы). Перезаписываем его свойства данными из полей формы. Затем, если объект был заново созданный (id у него равно 0), то добавляем в контекст. Если нет, ничего не делаем, потому что в этом случае объект уже привязан к контексту (это произошло, когда мы извлекали объект по id). Осталось просто сохранить объект и отреагировать на полученный результат.

Вроде ничего сложного. Если что непонятно, спрашивайте. А загрузку файлов мы чуть попозже обсудим.

среда, 10 марта 2010 г.

Пишем CMS. Шаг № 5. Перенаправляем пользователя

Итак, мы показали пользователю сообщение, а том, что все в порядке, и теперь хотим через 4 секунды перенаправить его на другую страницу.

Делается это очень просто. Чтобы браузер, открыв страница, через положенное время перенаправил пользователя в другое место, достаточно, чтобы эта страница имела следующий метатэг:

<meta http-equiv="Refresh" content="4;url=http://site.com/anotherpage.aspx">

* This source code was highlighted with Source Code Highlighter.


Разумеется, мы хотим управлять перенаправлением, иметь возможность менять время, страницу и т.д., поэтому данный метатэг мы добавим программным образом. На мой взгляд, разумно добавить этот код в мастер-страницу, чтобы иметь возможность без лишнего дублирования кода всегда осуществлять перенаправление.

Сначала добавим в .cs файл мастер-страницы наш код:

    public void AddRedirect(string PageToCode, int SecondsToWait)
    {
      HtmlMeta metaRedirect = new HtmlMeta();
      metaRedirect.HttpEquiv = "Refresh";
      metaRedirect.Content = string.Format("{0};url={1}", SecondsToWait, ResolveUrl(PageToCode));
      this.Page.Header.Controls.Add(metaRedirect);
    }


* This source code was highlighted with Source Code Highlighter.



Теперь откроем страницу LanguageEdit.aspx, с помощью которой мы редактируем и добавляем языки. Там мы добавим следующую директиву:

<%@ MasterType VirtualPath="~/Main.Master" %>

* This source code was highlighted with Source Code Highlighter.


Эта директива позволяет нам обращаться к мастер странице не просто как к объекту типа System.Web.UI.MasterPage, а к объекту нашего конкретного типа. Если вы запутались, поясняю: когда мы добавляем в проект мастер-страницу, всегда создается класс, который наследует вышеприведенный класс. Однако наша страница имеет объект Master, который всегда определен как MasterPage, и поэтому любые дополнительные члены мастера будут нам недоступны. Мы, конечно, можем осуществлять приведение типа ((Main)Master).OurMethod(), но согласитесь, это не очень красиво. Зато теперь наша страница знает, что мастер не просто MasterPage, а Main, и поэтому мы может осуществлять вызов метода напрямую:

Master.AddRedirect(Pages.Languages, 4);

* This source code was highlighted with Source Code Highlighter.


Здесь мы опять же передаем переменную из ресурсного файла, которая имеет значение ~/Languages/Languages.aspx - т.е. страница, которая показывает список всех языков.

Да, не забываем скрывать основную часть формы. Для этого ее (основную часть) можно разместить на панели и просто выставлять ей Visible = false. Не волнуйтесь, что она потащит за собой все данные. ASP.NET, когда видит что что-то невидимое, даже не генерирует html-код.

Таким образом, когда пользователь добавляет новый язык, или редактирует уже существующий, он после этого перемещается на страницу со списком всех языков.

вторник, 9 марта 2010 г.

Пишем CMS. Шаг № 4. Показываем пользователю результат действия

Итак, в прошлом номере нашей программы мы решили, что, как только пользователь что-нибудь такое сделает (добавит, удалит, отредактирует и т.д.) он сначала увидит сообщение (на той же странице по постбэку), а потом результат своего действия (через 3-4 секунды будет перенаправлен на нужную страницу).

Итак, как же сказать пользователю "Все в порядке!"? Первое, что приходит в голову - разместить на странице Label, в методе обработке результата менять цвет и текст в зависимости от результата. Что мне не нравится в этом подходе? Ну допустим мы определяем внешний вид стилями, которые храним в файле CSS, одном на весь сайт. А что, если мы захотим рядом с текстом добавлять картинку или еще как-то украсить его? Будет весьма затруднительно поменять Label на Div на всех страницах, где мы используем сообщения. Поэтому мы так делать не будем. А мы поэтому создадим простой пользовательский контрол. Делается это очень просто:

1. Добавляем новый item в наш проект (предлагаю сначала создать папку Controls и хранить все контролы там), выбираем тип Web User Control.

2. Открываем файл ascx и добавляем такую панель:

  <asp:Panel ID="panelInfo" runat="server" Visible="true">
    <asp:Label ID="labInfo" runat="server" Text="" CssClass="LabelInfo"></asp:Label> <p></p> 
    <asp:HyperLink ID="linkInfo" runat="server">
      <asp:Label ID="labLink" runat="server" Text=""></asp:Label>
    </asp:HyperLink>
  </asp:Panel>


* This source code was highlighted with Source Code Highlighter.



Мы видим, что внутри панели размещается одна метка (текст сообщения), а также ссылка, внутри которой размещается еще одна метка. Это будет та ссылка, по которой мы предложим перейти пользователю, и по которой он обязательно перейдет, если его браузер не поддерживает автоматическое перенаправление.

Теперь заглянем в код. У меня это выглядит так:

public void Activate(bool? IsResultOK, string Message, string LinkLabelCode, string Link)
    {
      this.Visible = true;
      if (IsResultOK == null)
      {
        panelInfo.CssClass = "PanelInfoFYI";
      }
      if (IsResultOK.Value)
      {
        panelInfo.CssClass = "PanelInfoOK";
      }
      else if (!IsResultOK.Value)
      {
        panelInfo.CssClass = "PanelInfoFail";
      }
      if (Message != string.Empty)
        labInfo.Text = Message;
      if (LinkLabelCode != string.Empty && Link != string.Empty)
      {
        linkInfo.Visible = true;
        string linkText = LinkLabelCode;
        linkInfo.Text = linkText;
        labLink.Text = linkText;
        linkInfo.NavigateUrl = Link;
      }
      else
        linkInfo.Visible = false;
    }


* This source code was highlighted with Source Code Highlighter.



Первый параметр говорит о том, каков результат - если все плохо (false), то подключаем стиль ошибки, если хорошо (true) - положительный результат. Иногда требуется просто проинформировать пользователя, в этом случае передадим null и отобразим сообщение голубеньким.

Теперь посмотрим, как происходит вызов этого метода. После того, как мы положили на форму наш контрол (не забываем о регистрации - <%@ Register src="../Controls/InfoPanel.ascx" tagname="InfoPanel" tagprefix="controls" %> ), мы можем обращаться к нему напрямую:


if (result)
panelInfo.Activate(true, Messages.LanguageSaveOK, Messages.LabelLinkToLanguages, Pages.Languages);


Вот тут немного интереснее. Что это за Messages и Pages? Это имена ресурсных файлов (вернее, классов, которые сгенерировались как только мы добавили файл ресурса). Зачем, спрашивается? Нам нужно, чтобы ссылка отображала текст. Мы, конечно, можем просто захардкодить этот текст, но, во-первых, мы с самого начала договорились, что будем писать красивый код, а во-вторых, и это, конечно, главное, подобный подход может привести к очень большому геморрою в будущем. Представим себе, что мы таким образом захардкодили сообщений эдак... двести по всему проекту... а потом пришла директива сверху: все сообщения начинать со слов "Дорогой пользователь!" И вот мы ползаем по сайту, и, чертыхаясь, исправляем все двести сообщений...

В-общем, наши сообщения мы будем хранить в ресурсных файлах. Если что, его даже без проблем можно будет перевести на другие языки. Но сейчас для наших целей нам нужно два обычных ресурсных файла. Один - для сообщений, второй - для адресов страниц. Второй, возможно, нам в будущем не понадобится (пока не могу точно сказать), но пока что он избавляет от необходимости хардкодить ссылки, по которым надо переходить.

Можно, конечно, хранить сообщения в отдельном классе, но по-моему, в ресурсах хранить удобнее. Теперь мы с легкостью можем ссылаться на любые переменные, добавленные в текстовый ресурс, кроме того, студия позволяет сортировать переменные по именам и значениям, что, согласитесь, очень удобно. Значения, которые мы забиваем в табличке ресурсов, можно посмотреть в файле <НазваниеРесурса>.designer.cs

В следующем шаге мы перенаправим пользователя на указанную страницу.

пятница, 5 марта 2010 г.

Пишем CMS. Шаг № 3. Обработка действий пользователя

Не волнуйтесь, друзья, все в порядке. Никто ничего не забросил. Шла работа мысли. Оказывается, такие достаточно простые вопросы, как оповещение пользователя и разработка поведения системы в целом вовсе не являются простыми. То есть то, что получилось в результате, конечно, не является биномом Ньютона, однако, как показывает практика, огромное количество сайтостроителей правильный, хотя и простой подход игнорируют начисто. Я имею в виду либо отсутствие таких очевидных вещей, как редирект на нужную страницу, либо отсутствие вообще какой-то информабельности. Интернет-серферы меня поймут - как, бывает, бесит, когда регистрация заканчивается ничем! И непонятно, зарегистрировала тебя система или нет, а если да, то почему ничего не сказала?

Рассмотрим несколько моделей поведения системы.
1. Пользователь нажимает кнопку "Сохранить", система делает Postback и на странице появляется сообщение, что все в порядке. И что потом? Если пользователь нажмет кнопку "Сохранить" или "Отправить", что произойдет? Это зависит от того, что вообще происходит. Вполне может вылететь ошибка, если это, например, удаление, и производится дважды. И вообще, зачем, например, опять показывать пользователю форму с только что введенными данными, если все прошло нормально? Фтопку.

2. Пользователь нажимает кнопку "Сохранить", система благополучно перенаправляет его на список чего-нибудь (где пользователь увидит добавленное). Немного лучше. По крайней мере F5 не будет вызывать ошибку. Минусы этого дела:
- чтобы страница могла определить, какое сообщение показывать, очевидно, надо передавать его через строку запроса, которая будет иметь некрасивый вид;
- гораздо более важно то, что если перенаправление в случае ошибки производится на эту же страницу, пользователь теряет все введенные данные.
Не подходит тоже.

3. Нечно среднее (как сделано в форуме PHPbb). Пользователь видит сообщение об успешной отправке данных 3-4 секунды, после чего система автоматически перенаправляет его на нужную страницу. В случае ошибки пользователь также видит сообщение, перенаправления не происходит, и все данные, введенные пользователем, по-прежнему видны. Если браузер не поддерживает автоматическое перенаправление, пользователь видит на странице, где сообщение, ссылку, и переходит по ней вручную. По-моему, отличный подход.

Вот его мы и будем реализовывать. Рассмотрим сценарий с точки зрения asp.net. Пользователь нажимает кнопку, происходит Postback, мы возвращаемся на ту же страницу, обрабатываем введенные данные. Если все в порядке, показываем сообщение (а саму форму можно спрятать, чтобы не путать пользователя) со ссылкой, примерно 3-4 секунды, после чего перенаправляем пользователя на нужную страницу. Если ошибка, ничего особенного не делаем, просто показываем сообщение. Все данные, введенные пользователем, в результате будут сохранены.

Теперь разобьем задачу на несколько подзадач:
1. Как показывать сообщение и ссылку
2. Как обрабатывать данные
3. Как перенаправить пользователя

Начнем с первой задачи, поскольку она независима, и будет намного лучше, если к моменту обработки данных мы будем знать, что делать с результатом. Как организовать сообщение о результате - в следующем шаге.

четверг, 18 февраля 2010 г.

Пишем CMS. Совет - удаленная отладка

Иногда бывает так, что кажется - невозможно понять, откуда у ошибки ноги растут. Или как решить данную проблему. В этом случае мне всегда помогает удаленная отладка. "Удаленная" - это не про код и не про машину, а про разработчика. Т.е. разработчик в прямом смысле слова удален от компьютера и кода. Можно пойти в душ, хорошо думается на обратном пути к дому. А главное - ничего не отвлекает. Когда вы сидите за компьютером и мельтешите по коду, замыливаются и глаз, и мозг. Когда код только в уме, это настоящая квинтэссенция мышления. Какое-то там сияние чистого разума. Рекомендую. Во время удаленной отладки надо представить себе вызовы, что почему вызывается и где что может ломаться. То же самое и с программированием - мысленно располагайте проблему перед собой и ищите пути решения. Это даже лучше работает, чем листок бумаги. Серьезно!

среда, 17 февраля 2010 г.

Пишем CMS. Шаг № 2. Подключаем ADO.NET Entitry Framework

Сейчас нам надо разобраться, как работать с Entity Framework. Этот момент довольно подробно описан в моем старом посте здесь, поэтому я не буду повторяться, а лишь опишу шаги и действия, которые надо сделать, чтобы:
1) Сгенерировать классы для работы с БД
2) Просмотреть список доступных языков.
Не так уж много, но для этого шага достаточно.

Итак, создайте в солюшене новый проект, который назовите, например, DAL (data access layer). После создания проекта рекомендуется указать в его свойствах название dll, которая будет сгенерировать и пространство имен, создаваемое по умолчанию. Общепринятой практикой является давать названия в формате <Название производителя>.<Название всего проекта>.<Название конкретного проекта>.

После этого добавьте в проект новый айтем - выберите ADO.NET Entity Data Model. На экране появится визард, в котором вы указываете сервер базы данных (выберите сперва Generate from database), затем укажите таблицы, которые будут использованы для генерирования классов. После этого в вашем проекте появится файлы .edmx и .designer.cs.

Поскольку мы будем вызывать методы из классы из нашего веб-проекта (на данном этапе), нужно скопировать строку подключения из файла app.config в файл web.config нашего веб-проекта.

Теперь в веб-проекте создайте каталог Language и добавьте в него две формы (на основе Master page - это будут Web Content Form). Про мастер-страницу мы поговорим немного позже, а пока, если вы еще не создали ее, то создайте, и добавьте две страницы Languages.aspx и LanguageEdit.aspx. Первая страница позволит нам просматривать список языков и удалять ненужные, вторая - создавать новые и редактировать уже имеющиеся языки.

Откройте страницу Languages.aspx и разместите на ней контрол GridView (на самом деле вы можете разместить любой другой контрол, позволяющий просматривать данные в режиме таблицы, но пусть пока будет этот). Выставьте свойство AutoGenerateColumns = false (нам не нужны столбцы, генерируемые автоматически) и добавьте в таблицу столбцы типа BoundField, для каждого столбца укажите в поле DataField соответствующее поле таблицы. После этого добавьте в референсы проекта референс на наш DAL-проект, затем откройте файл кода Languages.aspx.cs и добавьте в список пространств имен наше пространство имен .DAL.

Создайте метод, например LoadLanguages и добавьте в него простой код, который будет вытягивать все наши языки из базы (пейджинг пока не рассматриваем).

private void LoadLanguages()
{
using (WebDatabaseConnection context = new WebDatabaseConnection())
{
var query = from l in context.Language
select l;
gridLanguages.DataSource = query;
gridLanguages.DataBind();

}
}

Здесь WebDatabaseConnection context - это главный класс для работы с базой данных. Он был сгенерировать Entity Framework и найти его можно в соответствующем файле нашего DAL-проекта. Разумеется, этот код следует обрамить конструкцией try-catch, но мы этого пока делать не будем. Во-первых, потому, что нам надо четко решить, что делать в случае возникновения ошибки, а во-вторых, неплохо бы знать, какое исключение может быть выброшено, чтобы правильно интерпретировать результат. Дело в том, что мы не можем показывать пользователю исходное сообщение, которое будет содержать эксепшн, это противоречит принципам безопасности, да и не нужно особо - ведь уровень подготовки пользователя неизвестен. Поэтому обработкой ошибок мы займемся чуть позже.

Заметьте, что в этом коде мы используем простейшую конструкцию Linq to Entity Framework, чтобы получить все данные из таблицы.

Теперь в код метода PageLoad мы разместим вызов нашего метода, который будет грузить данные в таблицу:

protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
LoadLanguages();
}
}


Здесь мы сначала проверяем, является ли вызов обратным (postback) - это может случиться только в том случае, если мы производим какую-либо обработку данных, поэтому отображать данные нам в этот момент не нужно. Как же так? - спросите вы. А очень просто. Дело в том, что после каждой обработки данных мы будем делать редирект на эту же страницу. Объясню, зачем. Если мы ограничимся одним постбэком и откроем эту же страницу после постбэка без редиректа, а затем нажмем F5, чтобы обновить страницу, браузер покажет сообщение о том, что мы собираемся заново отправить данные. Допустим, мы перед этим удалили язык. У нас будет выбор - отправить данные заново или нажать Cancel. Если мы отменим действие, обновления страницы не произойдет, если нажмем Resend, наше приложение получит повторное сообщение об удалении языка, который уже был благополучно удален, в результате чего мы увидим ошибку. Конечно, можно эту ошибку обработать, но зачем? Куда как проще открыть страницу заново и, сколько мы не будем нажимать F5, повторного удаления (или иного действия) не будет.

пятница, 12 февраля 2010 г.

Пишем CMS. Шаг № 1. Подготовительная работа

Прежде всего, определимся со структурой-архитектурой-технологией. Технология - используем связку ASP.NET 3.5 + SQL Server 2005, все это будем делать в бесплатной студии 2008 (то есть конечно, наверное, не всем она досталась бесплатно, но мне лично удалось получить ее на дне разработчика). В процессе разработки надо будет постараться использовать преимущества асп3.5.
Структура сайта проста - есть основной проект, параллельно ему существует админский, а также библиотека для DAL (кто не знает, что это, отсылаю к википедии).

Пока что мне не нравится идея создавать каждую страницу на лету, поэтому сейчас мы этого делать не будем, а будем создавать спокойно странички для каждого раздела в отдельности. Например, для управления языками у нас будет две страницы Languages.aspx и LanguageEdit.aspx, которые мы положим в каталог Languages. Вполне возможно, что в процессе работы эти страницы исчезнут, но не будем загадывать раньше времени. А пока надо сделать несколько важных вещей.

1. Настроить систему контроля версий. Как это сделать, очень хорошо описано вот в этой книге: ASP.NET 3.5 Social Networking . Вообще классная книга, рекомендую (шепотом: на этом сайте ее можно скачать; disclaimer: а у меня бумажная, толстая). Можно читать вместо моего блога. Но меня чтение подобных книг неизменно уводит в сторону, я не знаю, почему. Вот еще хорошая книга Разработка Web-приложений в среде ASP.NET 2.0. Задача - проект - решение. Но код в этой книге ужасен. Поэтому рекомендую только для поиска конкретных решений, но ни в коем случае не как пример для подражания.

2. Чтобы вас с самого начала не тошнило от собственного сайта, рекомендую найти ему подходящий шаблон. Его всегда можно будет изменить, а так хоть будем видеть, что где показывать. Огромное количество бесплатных классных шаблонов здесь http://www.oswd.org/. Рекомендую сразу же поменять картинки и цвета, а то, шарясь по интернету, вы с удивлением будете обнаруживать своей собственный сайт, только с другой начинкой. =))

3. Создать БД, таблицы. Для организации локализации нам нужна таблица, в которой мы будем хранить название языка, его код (типа "ru-RU") и название файла иконки.

4. Ну и не забудьте про TDD! Я обычно использую NUnit, очень удобная штука. Для тестов стоит создать отдельный проект. Мне кажется, все знают, как подсоединить NUnit к проекту для тестирования, но если кто-то забыл, спрашивайте.

5. Также желательно подключить и настроить log4net для логгирования событий (если это вам, конечно, нужно). В той же книжке все это рассказывается тоже. С логгированием надо быть осторожным, легко можно переборщить. Сохранять в логе слеует только действительно необходимую информацию, а не все подряд. Иначе вы рискуете иметь миллионы никому не нужных записей.

Следующим номером: очень краткий курс по работе с ADO.NET Entity Framework на примере работы с языками.

Пишем CMS или любителям изобретать велосипеды

Не знаю, как вам, а мне периодически хочется сделать какой-нибудь интересный сайт или сервис. Иногда довожу свою идею до конца, чаще бросаю на середине. Но каждый раз в самом начале встает вопрос - делать с нуля или воспользоваться готовой CMS? Сначала, конечно, я начинаю шарить по интернету в поисках приличной CMS. Ну, я люблю C# и ASP.NET, а не PHP и VB.NET, поэтому список подозреваемых невелик. Но и он очень быстро сужается - почти сразу же выясняется что данная CMS или глючная или очень медленная или какая-то слишком крутая, а мне надо попроще. И, конечно, остается в итоге один вариант - опять писать с нуля. Конечно, мой сайт будет также содержать глюки и тормозить, но это будут мои глюки, которые я постепенно исправлю, и тормоза, наверное, со временем пройдут как-нибудь... сами... Но это ладно, самое-то глупое во всей этой ситуации - писать с нуля один и тот же код. Потому что почти все сайты содержат примерно одинаковую информацию и функционируют по сути совершенно одинаково. Поэтому можно попытаться создать ну не CMS, конечно, это громко сказано, а так... шаблон... фреймворк, ну что-то такое, что можно использовать и потом.
Ну что ж, сказано-сделано.
Во-первых, что за сайт. Сейчас мне нужно сделать один довольно простой сайт, который будет содержать информацию о программном продукте. Возможно, в дальнейшем в него будет добавлено что-то вроде соцсети и магазин. А пока - новости, локализация, фидбэк, страница со скриншотами ну и разное по мелочи. Самое главное - возможность быстро в админке добавлять новости, информацию о продукте, скриншоты, менять иконки флажков для языков, возможность быстро и безболезненно добавить новый язык. Ничего сложного и навороченного, как видите. Можно было бы вообще не писать все это, если бы не одно но. В процессе работы меня периодически осеняет. Ну не то чтобы конечно, в прямом смысле, но все время приходят разные идеи, как можно отрефакторить, как можно сделать удобнее. Меня бесит, например, когда код разметки смешан c JavaScript-кодом, когда переадресация на нужную страницу делается в коде, и стоит потом эту страницу переместить, как переадресация валится, и все в таком же духе. Поэтому я буду свои идеи по ходу дела выкладывать сюда. Если конечно, мне не надоест. А называться это безобразие будет "Пишем CMS. 100 шагов". Я, конечно, не думаю, что приличную CMS можно написать даже за 300 шагов, но, во-первых, 100 звучит красиво и солидно, во-вторых, у меня нет уверенности, что меня хватит даже на 50, в третьих, накрайняк всегда можно поменять первую цифру.
Поехали!