На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: maxim84_
  
> ASP.NET , MVC статья
    Сегодня мы разберемся с созданием веб-приложений ASP.NET с использованием шаблонов MVC .
    Целью данной статье есть не обсуждение сильных и слабых сторон данного подхода при разработке веб-приложений,
    цель – демонстрация такого подхода на практике.

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

    Для работы нам понадобится загрузить MVC привью по следующему адресу:
    http://go.microsoft.com/?LinkID=8955108 , а также иметь установленную VS 2008 и SQL Server 2005.

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

    1. Создадим базу данных с именем PersonalSite.
    2. Добавим в базу таблицу с именем Messages
    3. Добавляем столбцы Id (int), Date(datetime), Msg(text), Nick(varchar(50)), Location(varchar(50)).
    Думаю названия столбцов говорят сами за себя.
    4. Делаем первичным ключом поле Id, не забывая выставить ему свойство identity в true,
    перелаживая ответственность за генерацию первичного ключа на БД.

    Пришло время создать проект. Если Вы, еще не установили MVC Preview 3, то настоятельно советую Вам это сделать прямо сейчас.
    1. Открываем Visual Studio 2008
    2. File -> New Project – C#
    3. Выбираем MVC ASP.NET Web Application
    4. Даем имя проекту Personal
    5. Подтверждаем создание проекта
    6. На следующем шаге этапе отказываемся от поддержки Unit тестов (в реальных проектах естественно их желательно писать и использовать!)

    Студия сгенерировала нам каркас нашего приложения. Я не хочу разбираться сейчас с тем, что она нам сгенерировала.
    Мы пойдем своей дорогой, а потому я предлагаю Вам в Solution Explorer переместиться к папке View и удалить вложенную папку Home,
    содержимое папки Shared, а также удалить содержимое папок Content и Controllers.

    Пришло время разобраться с тем, что осталось:
    Папка Content предназначена для хранения всевозможных ресурсов и вспомогательного контента, такого как: скрипты, таблицы стилей и изображения.
    Папка Models, судя из названия, позволит нам хранить классы для представления модели в нашем приложении.
    Папка Controllers будет хранить наши контроллеры.
    Папка Views – виды.
    Все очевидно и просто.

    Стоит отдельно упомянуть о каталоге Shared во View, здесь мы сможем хранить виды доступные любому контроллеру или хранить мастер страницы,
    которые в ASP.NET MVC представляют частный случай вида.

    Для тех, кто не знаком с MVC я советую предварительно почитать, к примеру, тут:
    http://martinfowler.com/eaaDev/uiArchs.html или тут
    http://martinfowler.com/eaaCatalog/modelViewController.html

    В конце концов, MVC можно представить как конвейер, каждая часть которого выполняет свою специфическую задачу.
    Модель представляет предметную область, контроллер принимает пользовательский ввод, просит поработать на нас модель,
    после чего решает какой вид и с каким данными отобразить пользователю. Работа вида сводиться к визуализации текущего представления данных, переданных виду из контроллера.
    Займемся моделью. Для ее построения воспользуемся Linq to Sql. Добавим в папку Model dbml файл.

    1. Выделяем папку Model
    2. Из контекстного меню Add New Item
    3. Ищем LINQ to SQL Classes
    4. Даем имя dbml файлу Guestbook
    5. Подтверждаем создание
    6. В Server Explorer создаем новое соединение с базой PersonalSite
    7. Перетаскиваем таблицу Messages в наш дизайнер.

    Можно скомпилировать приложение, убедившись в том, что генерация модели прошла нормально.
    Сейчас мы добавим в нашу модель пару методов, которые сделают работу с ней более удобной.
    1. Выбираем GuestBook.dbml в SolutionExplorer
    2. Из контекстного меню выбираем ViewCode
    3. Добавляем следующие методы в класс GuestbookDataContext
    4. Изменяем неймспейс на Personal.Models

    Ниже приводиться полный код из файла Guestbook.cs
    ExpandedWrap disabled
      using System.Data.Linq;
      using System.Data.Linq.Mapping;
      using System.Data;
      using System.Collections.Generic;
      using System.Reflection;
      using System.Linq;
      using System.Linq.Expressions;
      using System.ComponentModel;
      using System;
      using System.Collections.Specialized;
      using System.Runtime.Remoting.Contexts;
       
       
       
      namespace Personal.Models
      {
          partial class GuestbookDataContext
          {
              public List<Message> GetMessagePage(int pageNumber, int size, out int count)
              {
                  var results =
                      (from p in Messages select p)
                      .Skip((pageNumber - 1) * size)
                      .Take(size)
                      .ToList<Message>();
       
                  count = Messages.Count();
       
                  return results;
              }
       
              public void MessageAdd(Message message)
              {
                  Messages.InsertOnSubmit(message);
              }
          }
      }


    Методы крайне простые первый выполняет постраничную выборку, второй позволяет добавить сообщение в БД.
    Отлично теперь мы, вроде как, обзавелись моделью.

    Пришло время поговорить о контроллерах.

    В отличие от классической схемы разработки приложений в ASP.NET (Web Forms).
    Обработка запросов пользователя происходит посредством соответствующих контроллеров, а не соответствующих экземпляров страниц.
    Каждый запрос, приходящий к нашему приложению, ассоциируется с определенным контроллером,
    экземпляр которого создается после того как приложение решит какой именно контроллер должен обрабатывать запрос.
    Как же приложение решает, какой контроллер ему нужен? Очень просто, ответ лежит в отображении URL на контроллеры и на соответствующие методы контроллера(action в терминах MVC). Проще всего понять на примере. Давайте рассмотрим простой пример запроса,
    которое может получить наше приложение: http://PersonalSite/Home/Index/

    Данный запрос разбирается с помощью роутера. Который определяет, что запрос должен быть обработан экземпляром HomeController,
    который содержит метод (action в терминах MVC) Index. Все предельно просто.
    По сути, обращение к подкаталогу Home будет обработано контроллером с тем же именем, но с добавлением суффикса Controller.

    А, что было бы, если бы метод Index принимал параметры? Мы бы дописали их к нашему адресу.

    К примеру так: http://PersonalSite/Home/Index?id=0

    На самом деле, такое поведение можно изменить, реализовав собственный роутер или переопределив шаблон по для роутера по умолчанию.
    Пока у нас нет контроллеров. Давайте добавим в наше приложение контроллер PersonalController, его единственным назначением,
    будет переадресация к виду по умолчанию (с возможной информацией о хозяине ресурса) и единственным action (методом) Start().
    В папку Controllers добавим класс PersonalController

    ExpandedWrap disabled
      using System;
      using System.Data;
      using System.Configuration;
      using System.Linq;
      using System.Web;
      using System.Web.Security;
      using System.Web.UI;
      using System.Web.UI.HtmlControls;
      using System.Web.UI.WebControls;
      using System.Web.UI.WebControls.WebParts;
      using System.Xml.Linq;
      using System.Web.Mvc;
       
      namespace Personal.Controllers
      {
          public class PersonalController : Controller
          {
              public ActionResult Start()
              {
                  return View();
              }
          }
      }


    Каждый Action должен возвращать переменную типа ActionResult с указанием вида,
    который нужно отобразить в данный момент пользователю. Для этого мы в методе Start возвращаем результат выполнения метода View.
    Метод в данном случае не принимает никаких параметров, а это значит, это указание отобразить вид приложения по умолчанию.
    (в дальнейшем мы вернемся к моменту с назначением такого вида для приложения)

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

    ExpandedWrap disabled
      using System;
      using System.Data;
      using System.Configuration;
      using System.Linq;
      using System.Web;
      using System.Web.Security;
      using System.Web.UI;
      using System.Web.UI.HtmlControls;
      using System.Web.UI.WebControls;
      using System.Web.UI.WebControls.WebParts;
      using System.Xml.Linq;
      using System.Web.Mvc;
      using Personal.Models;
      using System.Collections.Generic;
       
      namespace Personal.Controllers
      {
          public class GuestbookController : Controller, IDisposable
          {
              private const int pageSize = 20;
       
              public ActionResult List(int pageNumber)
              {
                  using (var context = new GuestbookDataContext())
                  {
                      try
                      {
                          int count = 0;
                          var result = context.GetMessagePage(pageNumber, pageSize, out count);
       
                          ViewData["pageCount"] = count / pageSize + 1;
                          return View("List", result);
                      }
                      catch(Exception)
                      {
                          return View("Error");
                      }
                  }
              }
       
              public ActionResult Save()
              {
                  using (var context = new GuestbookDataContext())
                  {
                      try
                      {
                          Message message = new Message
                          {
                              Msg = Request.Form["message"],
                              Nick = Request.Form["nickname"],
                              Location = Request.Form["location"],
                              Date = DateTime.Now
                          };
       
                          context.MessageAdd(message);
                          context.SubmitChanges();
       
                          return List(1);
                      }
                      catch (Exception)
                      {
                          return View("Error");
                      }
                  }
              }
          }
      }


    Для упрощения я решил, что мы будем всегда отображать по 20 записей на страницу и содержит два простых метода List (выбирает данные для постраничного отображения сообщений), Save (сохраняющий новое сообщение в БД).
    При возникновении ошибок пользователю будет отображен вид Error.

    Данный код также демонстрирует несколько важных моментов.
    1. Контроллер может передавать данные виду с использованием спецального «словаря» ViewData. Этот «словарь» будет доступен нашим видам.
    2. Метод View может принимать параметры в нашем примере мы передаем туда имя вида для последующиго отображения и данные которые будут доступны виду через специальное свойство Model «словаря» ViewData в виде.

    Теперь нам нужно внести необходимые изменения в файл global.asax, что бы корректно маршрутизировались сообщения с учетом написанных нами контроллеров.

    Новая версия метода RegisterRoutes:

    ExpandedWrap disabled
      public static void RegisterRoutes(RouteCollection routes)
              {
                  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
       
                  routes.MapRoute(
                      "Default",                                              // Route name
                      "{controller}/{action}/{id}",                           // URL with parameters
                      new { controller = "Personal", action = "Start", id = "" }  // Parameter defaults
                  );
       
              }


    Дополнительно нужно сделать так, что бы по обращению к корню сайта нас отправляло к контроллеру PersonalController.
    Для этого мы в коде страницы default.aspx.cs меняем метод Page_Load на следующий:
    ExpandedWrap disabled
      public void Page_Load(object sender, System.EventArgs e)
      {
          Response.Redirect("~/Personal");
      }


    По сути код отправляет нас к контроллеру PersonalController.
    Теперь можем испытать наши контроллеры в действии.
    Для этого запустим по F5 приложение и посмотрим на то, что у нас получилось.
    Получаем ошибку:

    Цитата
    The view 'Start' could not be located at these paths: ~/Views/Personal/Start.aspx,
    ~/Views/Personal/Start.ascx, ~/Views/Shared/Start.aspx, ~/Views/Shared/Start.ascx


    На самом деле ничего страшного не произошло. Из текста ошибки можно легко сделать вывод о том,
    что приложение не нашло нужного ему вида по адресу ~/Views/Personal/

    Более того обратим внимание на подкаталог - его имя Personal, что совпадает с именем контроллера за вычитанием,
    оговоренного суфикса Сontroller. Также, из сообщения об ошибке, мы можем сделать вывод о том,
    что видом может быть либо aspx страница либо ascx контрол.

    Осталось дело за маленьким добавить соответствующие виды в приложение. Я предлагаю начать с мастер страницы.
    Ее мы поместим по адресу Views/Shared, как я уже говорил виды размещаемые там - глобальны относительно контролеров приложения.
    Для реализации мастер страницы я решил воспользоваться готовым Html шаблоном Simple доступным по адрессу:

    http://msdn.microsoft.com/en-us/asp.net/aa336613.aspx

    После инсталяции шаблона, перво наперво добавляем в папку Views/Shared мастер страницу с именем Site.Master.

    Следующий шаг есть копирование Html из шаблона в мастер страницу. Проще скопировать код приведенный мной ниже.
    Или же: открываем папку simple_html в проинсталеном шаблоне, там нас интересует файл msvs_template_simple_divs.html.
    Скопируем из него все содержимое тега HTML и заменяем им содержимое тега HTML в нашей мастер странице. Следующий шаг это перемещение папки themes из шаблона в папку Content нашего сайта. Ограничемся только темой default находящийся в папке themes шаблона.

    Создаем в Solution Explorer в папке Contents папку themes, а в ней каталог default.
    После чего из контекстного меню выбираем Add Exiting Items и указываем путь ко всем ресурсам из папки themes/default шаблона.
    Теперь нужно поправить пути к ресурсам в нашей мастер странице. Меняем пути к css и иконке на:

    ExpandedWrap disabled
          <link rel="shortcut icon" href="../../Content/themes/default/favicon.ico" />
          <link href="../../Content/themes/default/msvs_template_simple_divs.css" rel="stylesheet" type="text/css" />


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

    ExpandedWrap disabled
      <%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Site.master.cs" Inherits="Personal.Views.Shared.Site" %>
       
      <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
      <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
      <head>
          <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
          <meta name="description" content="Description of your web page goes here." />
          <meta name="keywords" content="" />
          <title>Personal Site</title>
          <link rel="shortcut icon" href="../../Content/themes/default/favicon.ico" />
          <link href="../../Content/themes/default/msvs_template_simple_divs.css" rel="stylesheet"
              type="text/css" />
      </head>
      <body>
          <!--
        
        TOP NAV
        
        -->
          <div id="mainnav-container">
              <div id="mainnav">
                  <div class="none">
                      <a href="#maincontent">skip to the main content area of this page</a></div>
                  <ul>
                      <li>
                          <%= Html.ActionLink("Home", "Start", "Personal") %></li>
                      <li>
                          <%= Html.ActionLink("Guest Book", "List", "Guestbook", new {pageNumber = 1}) %></a></li>
                  </ul>
              </div>
              <div class="clear">
              </div>
          </div>
          <!--
        
        SITE NAME & SLOGAN
        
        -->
          <div id="header">
              <a href="#" title="Site name home page">Sergey Guslisty</a>
              <div id="slogan">
                  Home Site</div>
          </div>
          <!--
       
        CONTENT
       
        -->
          <div id="content-container">
              <!--
          
          SIDE COLUMN
          
          -->
              <div id="content-side">
                  <ul class="link-list-vertical">
                      <li>
                          <%= Html.ActionLink("Home", "Start", "Personal") %></li>
                      <li>
                          <%= Html.ActionLink("Guest Book", "List", "Guestbook", new {pageNumber = 1}) %></a></li>
                  </ul>
              </div>
              <!--
          
          MAIN COLUMN
          
          -->
              <div id="content">
                  <a name="maincontent" id="maincontent"></a>
                  <asp:ContentPlaceHolder ID="MainHolder" runat="server">
                  </asp:ContentPlaceHolder>
                  <!--
          
        FOOTER
        
        -->
                  <div id="footer">
                      <p>
                          One Your Street Name, City State Zip Code555-1212 | (555) 555-1212 fax<br />
                          Copyright © 2005 Site Name
                      </p>
                  </div>
              </div>
              <!--
          
          SIDE 2 COLUMN
          
          -->
              <div id="content-side-2">
              </div>
          </div>
      </body>
      </html>


    Осталось изменить базовый класс для мастер страницы открываем Site.Master.cs и правим код:

    ExpandedWrap disabled
      using System;
      using System.Collections;
      using System.Configuration;
      using System.Data;
      using System.Linq;
      using System.Web;
      using System.Web.Security;
      using System.Web.UI.HtmlControls;
      using System.Web.UI.WebControls;
      using System.Web.UI.WebControls.WebParts;
      using System.Xml.Linq;
      using System.Web.UI;
      using System.Web.Mvc;
       
      namespace Personal.Views.Shared
      {
          public partial class Site : ViewMasterPage
          {
              protected void Page_Load(object sender, EventArgs e)
              {
       
              }
          }
      }


    Обратите внимание на построение ссылок в мастер странице.
    ExpandedWrap disabled
                          <%= Html.ActionLink("Home", "Start", "Personal") %>
                          <%= Html.ActionLink("Guest Book", "List", "Guestbook", new {pageNumber = 1}) %>


    Я воспользовался утилитным классом Html для генерирования ссылок, которые будут отправлять запросы к контроллерам.
    Первый парамметр имя ссылки, второй Action, третий имя контроллера без суфикса.
    При этом второй линк имеет четвертый парамметр – анонимный тип который содержит свойство которое по имени совпадает с именем параметра метода List
    в контроллере GuestbookController. ИМХО все логично :)
    MVC при обработке запроса, автоматически извлечет парамметр из URL и поместит его в соответствующий метод. При этом мы можем в контроллере пользоваться классическим разбором парамметров прям в методах, если мы обратим внимание на контроллеры, то увидим, что они унаследовали соответствующие свойства, в том числе и Request позволяющий добраться к парамметрам запроса через QueryString.
    С мастер страницей расправились.

    Далее в каталог Views добавляем каталог Personal, а в него вид новый вид Start.
    Для этого в SolutionExplorer на папке Personal из контекстного меню Add New Item.
    Выбираем MVC View Content Page даем ему имя Start.aspx. На следующим шаге не забываем указать,
    что в качестве Мастер страницы используется наш Site.

    Код вида Start (Start.aspx):

    ExpandedWrap disabled
      <%@ Page Title="" Language="C#"
      MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Start.aspx.cs" Inherits="Personal.Views.Personal.Start" %>
      <asp:Content ContentPlaceHolderID="MainHolder" runat="server">
          <h2>Personal Information</h2>
      </asp:Content>


    Если скомпилировать приложение и запустить отобразиться наш вид Start. Дальнейшее его изменение я предоставляю Вам самим.
    Нас ждут великие дела. Осталась самая интерессная часть. Добавим каталог Guestbook в папку View. Вид который мы реализуем там будет вид отображающий посты постранично и содержащий формочку для добавления сообщений.
    Добавим вид List, в папку Guestbook (делаем все аналогино виду Start).
    Изменим код для вида в файле List.aspx.cs

    ExpandedWrap disabled
      public partial class List : ViewPage<List<Message>>
          {
          }


    Как можно заметить мы наследуем от родового класса ViewPage и в качестве типа указываем List<Message>, таким откровенно нехитрым способом реализуется возможность для вида оперрировать строготипизированными данными передаваемыми из контроллера.
    Вспомним часть метода List контроллера Guesbook:

    ExpandedWrap disabled
                          ViewData["pageCount"] = count / pageSize + 1;
                          return View("List", result);


    именно List<Message>, был передан методу View в качестве второго парамметра, если бы мы не указали тип для ViewPage, то нам бы пришлось выполнять соответствующие приведение при обращении к модели в самом виде.
    Ниже приводиться код вида, который отображает соответствующие сообщения и генерирует линки для постраничного просмотра. Вид также содержит формочку для добавления новой записи.

    ExpandedWrap disabled
      <%@ Page Title="" Language="C#"
      MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true"
          CodeBehind="List.aspx.cs" Inherits="Personal.Views.Guestbook.List" %>
       
      <asp:Content ID="Content1" ContentPlaceHolderID="MainHolder" runat="server">
          <h2>
              Guest Book</h2>
          <% foreach (var message in ViewData.Model)
             { %>
          <table>
              <tr>
                  <th>
                      <%= message.Nick %>
                  </th>
                  <th>
                      <%= message.Location %>
                  </th>
                  <th>
                      <%= message.Date.ToLongDateString() %>
                  </th>
              </tr>
              <tr>
                  <td colspan="3">
                      <p>
                          <%= message.Msg %>
                      </p>
                  </td>
              </tr>
          </table>
          <hr />
          <br />
          <% } %>
          <%
              for (int i = 1; i <= Convert.ToInt32(ViewData["pageCount"]); ++i)
              { %>
          <table>
              <tr>
                  <td>
                      <%= Html.ActionLink(i.ToString(), "List", new { pageNumber = i }) %>
                  </td>
              </tr>
          </table>
          <br />
          <% } %>
          <form method="post" action="/Guestbook/Save">
          Name:
          <br />
          <input name="nickname" type="text" />
          <br />
          <br />
          Location:
          <br />
          <input name="location" type="text" />
          <br />
          <br />
          Message:
          <br />
          <textarea rows="7" cols="40" name="message"></textarea>
          <br />
          <br />
          <input type="submit" value="Save" />
          </form>
      </asp:Content>


    На что обратить внимание? Конечно на то, что самый мощный Repeater это foreach,
    а так же на то как данные от формы отправляются с помощью POST контролеру Guestbook в action Save.

    Напоминаю я выбрал самый простой способ реагирования на ошибки в программе, - отправлять пользователя на шареный вид Error.
    А потому ниже привожу код и для него. Его лучше создать в папке Shared т.к. доступ к нему в дальнейшем может понадобиться в различных контроллерах.

    Вот так не хитро:

    ExpandedWrap disabled
      <%@ Page Title="" Language="C#"
      MasterPageFile="~/Views/Shared/Site.Master"
      AutoEventWireup="true" CodeBehind="Error.aspx.cs" Inherits="Personal.Views.Shared.Error" %>
      <asp:Content ID="Content1" ContentPlaceHolderID="MainHolder" runat="server">
      <h2>Error Page</h2>
      </asp:Content>


    Что дальше? Дальше двигаемся самостоятельно. Проект представляет только общий каркас. Было бы желательно более интелектуально обработать ошибки, добавить возможность валидации данных на клиенте и сервере, а также прикрутить клиентский Ajax. Что бы лучше разобраться со всем происходящим могу предложить поставить точки останова в контроллерах и подебажить приложение самостоятельно. В доброе Вам плаванье.
      Офф. В последнее время бесит политика MS. Поэтому ASP.NET MVC заочно ненавижу и считаю актуальным писать о Castle MonoRail и т.д. и т.п. :ph34r: NHibernate.Linq жжот!
      з.ы. Тот же ayende rahien нашел кучу багов в реализации и даже API!
        Еще раз сорри. Очень не люблю политику МС в плане .NET :blink:
          M
          Цитата deil @
          считаю актуальным писать о Castle Mono

          Кто лично тебе мешает писать о Castle Mono?! Пиши!

          Цитата deil @
          Офф. В последнее время бесит политика MS.

          А мне нравится. При этом оба наши мнения частные.
            Исправления к тексту c учетом найденых неточностей и выхода в свет бета версии ASP.NET MVC:

            1. В проект необходимо добавить Reference на System.Core.dll
            2. Не требуется вносить никаких изменений в метод Page_Load в default.aspx.
            3. Изменились сигнатуры метода ActionLink у Html поэтому в мастер странице меняем генерирование линков с

            <%= Html.ActionLink("Guest Book", "List", "Guestbook", new {pageNumber = 1}) %>

            на

            <%= Html.ActionLink("Guest Book", "List", "Guestbook", new {pageNumber = 1}, null) %>
            0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
            0 пользователей:


            Рейтинг@Mail.ru
            [ Script execution time: 0,0447 ]   [ 15 queries used ]   [ Generated: 23.04.24, 20:17 GMT ]