воскресенье, 18 мая 2008 г.

MVC и Ajax

Создавая проект на основе MVC, вы можете использовать Ajax, причем сделать это совсем несложно. Кто-то захочет использовать самописный код, а может быть, библиотеку Microsoft Ajax, я же предпочитаю использовать простую и удобную библиотеку Prototype. Единственный видимый ее недостаток - размер (123 К), который устраняется сжатием. Например, можно воспользоваться вот этим онлайновым компрессором, после которого файл уменьшается до 50К, что вполне приемлемо.

Задача
Мне требуется сделать следующую, довольно простую штуку: есть два комбо, в одном перечислены страны, а в другом надо обновлять список городов для данной страны. Т.е. выбрали мы, например, Russia в первом комбо, во втором должны появиться Moscow, SPb и так далее. И естественно, без перезагрузки страницы.

План
Задача на первый взгляд простая и даже тривиальная, и решается она действительно просто. Но давайте в учебных целях подробно рассмотрим план решения.
1. БД - в базе должны были страны и города
2. Entity Framework - интерфейс для доступа к данным.
3. Модель - должен обеспечивать как выдачу списка доступных стран, так и списка городов для страны.
4. Контроллер - должен иметь методы действий для отображения всей страницы (на которой размещаются комбо) и части (обновление второго комбо).
5. Представления - отображение данных.
6. Ajax (prototype) - код для подтягивания данных по событию комбо.

1. База данных
Заведем две таблицы - города и страны. Я не очень доверяю числовым идентификаторам, т.е. я им доверяю, но связывать такие справочные таблицы предпочитаю с помощью более долговечных вещей. Поэтому я ввожу поле кода для страны, а в таблице городов делаю ссылку на это поле (FK т.е.).

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

2. Entity Framework
После создания таблиц надо сгенерировать код для доступа к ним. Можно просто апдейтить нашу модель, но у меня, например, студия сглюкнула и пришлось удалять модель полностью, а затем создавать заново. Чтобы обновить модель, надо перейти в режим Model browser (дважды щелкнуть на файле .edmx - мне такой способ не очень нравится, но другого я не знаю). Затем выбрать корневой элемент в дереве и выполнить команду Update model from database. Далее вы просто выбираете новые таблицы и жмете Finish. Если все пройдет нормально, вы получите обновленную модель и обновленный код доступа к данным.

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

  public class StaticData
  {
    public List<Country> CountryList
    {
      get
      {
        return GetAllCountries();
      }
    }
    private static List<Country> GetAllCountries()
    {
      List<Country> countries = null;
      using (DataCoreConnection context = new DataCoreConnection())
      {
        countries = (from c in context.Country
               select c).ToList();
      }
      return countries;
    }
    
  }



Код простейший, добавлю лишь, что DataCoreConnection - это класс типа System.Data.Objects.ObjectContext из файла, сгенерированного при построении модели.
Второй класс аналогичен первому, за исключением конструктора, в который передается код страны:
public class Cities
  {
    private string countryCode;
    public Cities(string CountryCode)
    {
      countryCode = CountryCode;
    }
    public List<City> CitiesForCountry
    {
      get
      {
        return GetCitiesForCountry(countryCode);
      }
    }
    private List<City> GetCitiesForCountry(string CountryCode)
    {
      List<City> list = null;
      using (DataCoreConnection context = new DataCoreConnection())
      {
        list = (from c in context.City
                where c.Country.Code == CountryCode
                select c).ToList();
      }
      return list;
    }
  }



4. Контроллер
Контроллер у нас уже есть - HomeController (поскольку наши комбобоксы располагаются на главной странице). Сюда мы добавим один-единственный метод:
public void UpdateCities(string code)
    {
      Cities cities = new Cities(code);
      RenderView("SelectCity", cities);
    }



Здесь мы всего лишь получаем наши города и передаем экземпляр объекта представлению SelectCity.
Не забываем о роутинге - в файле Global.asax добавим строку:
      routes.Add(new Route("Home/UpdateCities/{code}", new RouteValueDictionary(new { controller = "Home", action = "UpdateCities" }),
        new MvcRouteHandler()));


Обратите внимание на последний элемент - {code} - это код города, который передается контроллеру. Контроллер принимает его как string code.

5. Представления - отображение данных.
Контролы помещаются на странице (т.е. представлении) Index.aspx. Поскольку страница с момента загрузки должна содержать некоторые статические данные, это должна быть не просто ViewPage, а в нашем случае ViewPage<StaticData>. Сначала отобразим комбобокс со списком доступных стран:



Как видим, мы получаем данные напрямую из ViewData, как будто ViewData - это объект типа StaticData (так оно и есть). Очень удобно. Кого-то может смутить тот факт, что разметка перемешивается с кодом. Как же так? Ведь обещали полное отделение кода! Мммм... ну.... в-общем, ничего страшного. Кода все-таки очень мало, и мне он кажется логическим дополнением к разметке.
Обращаю внимание на объявление обработчика:

onchange="UpdateCitiesList();"


Это функция JavaScript, которая будет подтягивать список городов при изменении значения комбобокса со странами (эту функцию рассмотрим чуть позже).
А пока займемся представлением, обновляющим второй комбобокс. Да-да, комбобокс будет обновлен целым представлением! Добавляем новое представление SelectCity, которое, конечно, объявляем так:

  public partial class SelectCity : ViewPage<Cities>
  {
  }



А вот код, генерирующий содержимое комбобокса:



6. Ajax
Ну а теперь посмотрим, какая функция подтягивает список городов (вернемся к Index.aspx):

  1: <script type="text/javascript" language="javascript">
  2:   function UpdateCitiesList()
  3:   {
  4:     if ($('selCountry').value == "0")
  5:       return;
  6:       var url = '/Home/UpdateCities/' + $('selCountry').value;
  7:     new Ajax.Updater('selCities', url,
  8:     {
  9:       method: 'get'
 10:     }
 11:         );
 12:   }
 13: </script>



В строке 4 мы проверяем, не выбран ли нулевой элемент выпадающего списка (тот, который содержит текст "Select ..."). В этом случае просто покидаем функцию. Далее формируем url, к которому будем обращаться - это обычный адрес в MVC, нам надо вызвать контроллер Home, метод UpdateCities, и передать код страны. В строке 7 мы вызываем очень удобный метод Ajax.Updater - он обновляет содержимое нужного нам контрола. Сигнатура метода проста:
new Ajax.Updater(container, url[, options])

Container - это контрол, который требуется обновить; url - вызываемый url; options - мы указали только метод передачи запроса (полный список параметров см. на сайте Prototype).

Как только мы выбираем страну, срабатывает событие onchange выпадающего списка, и Ajax.Updater обновляет комбобокс городов. Все! По-моему, все выглядит просто и красиво.

суббота, 17 мая 2008 г.

MVC. Представления

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

Вообще обычные веб-страницы MVC, т.е. представления являются наследниками класса ViewPagе, а тот, в свою очередь, наследует обычный Page и, кроме того, реализует интерфейс IViewDataContainer. По умолчанию экземпляр ViewPage не включает тэг <form>, поэтому, если вы захотите добавить серверные контролы, не забудьте добавить и форму.
Следует отметить, что представление должно служить только для отображения данных, оно не должно содержать никакой логики или кода доступа к данным. Просто взять данные и отобразить их.

Передача данных
Главное, ради чего создаются веб-страницы (и вообще приложение) - это данные, которые должны быть переданы форме (в нашем случае представлению). В MVC существует несколько способов передать странице данные. Рассмотрим их по порядку.
1. Использование словаря ViewData
Класс Controller содержит член ViewData, он объявлен следующим образом:

public IDictionary<string, object> ViewData { get; }

Как видим, это простая дженерик коллекция, доступ к ней получаем из самого контроллера:

ViewData["CategoryName"] = Category.Name;
ViewData["Categories"] = categories; //categories - это <List>Category

Этот же словарь доступен из представления, поэтому мы можем обратиться прямо на странице к нему так:

<h2><% = ViewData["CategoryName"] %></h2>

Для отображения повторяющихся частей страницы (как в Repeater) можно использовать цикл foreach:

<select>
<%foreach (var cat in ((<List>)ViewData["Categories"]) ){ %>
<option value='<%=cat.id %>'> <%= cat.Name %> </option>
</select>

Обращаю ваше внимание на две вещи:
1. Несмотря на то, что cat имеет "неопределенный" тип var, мы можем обращаться к полям его настоящего типа, поскольку к нему мы привели объект ViewData["Categories"].
2. Мы использовали встроенный код как для содержимого контейнера (для option), так и для определения атрибута тэга (value) - во втором случае кавычки обязательны (даже несмотря на то, что в обычных тэгах мы можем их и не ставить).

2. Использование типизированных объектов
Вы можете создавать слой DAL самостоятельно, я пытаюсь использовать MS EntityFramework, в любом случае у нас есть доступ к типизированным объектам и коллекциям, которые мы прекрасно можем передать странице. Делается это с помощью перегруженного метода RenderView, который в качестве второго параметра принимает любой объект:

protected void RenderView(string viewName, object viewData);

Вот туда-то мы и можем передать, например, список категорий:

RenderView("Category", categories); //categories - это <List>Category

На страницу этот параметр приходит в виде ViewData, поэтому далее мы работаем с ним также, как было показано выше.

Но есть еще более эффективный способ использовать типизированные объекты на странице (уже без всякого приведения). Дело в том, что в MS MVC существует еще один класс, наследующий ViewPage, определенный следующим образом:

public class ViewPage<TViewData> : System.Web.Mvc.ViewPage
Member of System.Web.Mvc

TViewData - это шаблон, вместо него вы подставляете имя класса, содержащего необходимые данные.
Все, что надо сделать - это наследовать представление от ViewPage<TViewData>, например:

public partial class Index : ViewPage<Category>
{
}

После чего прямо на странице можно легко обращаться к ViewData, объекту, который в нашем примере имеет тип Category:

<%=ViewData.Subcategory %>

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

Ссылки по теме:
ScottGu о контроллерах (и представлениях)
ASP.NET Model View Controller Applications > Views and Rendering Helpers

четверг, 15 мая 2008 г.

MVC. Контроллеры.

Итак, добрались мы наконец до самого, на мой взгляд, "вкусного" в этом новом фреймворке - до контроллеров. Это, можно сказать, сердце вашего проекта, построенного на основе MVC. Надо сказать, что концепция контроллеров не кажется сложной, но содержит некоторые нюансы, без знания которых сложно создать что-то стоящее.
1. Как работают контроллеры
Контроллеры - это специальные классы, в которых вы определяете внутреннюю логику работы приложения - откуда взять данные, куда их передать, и какую страницу показать.
Например, если у вас должна быть страница, которая показывает список категорий (чего-нибудь), а также отдельно содержимое категории, то логично создать контроллер Category, в котором определить методы ShowAll и Show, принимающий в качестве параметра id категории.
Подобные методы носят название action methods (методы действия). Вызов метода производится пользователем, когда он кликает ссылку или набирает в адресной строке /Category/Show/5 - показать содержимое категории с id=5. Методы вызываются в соответствии с правилами, определенными в файле Global.asax.

О чем надо помнить. Имя самого класса контроллера должно заканчивать словом controller, а при вызове вы указываете непосредственно имя контроллера, без этого постфикса. Т.е. если вызов /Category/Show/5, то класс контроллера должен называться CategoryСontroller.

Если контроллер содержит публичный метод, который не является методом действия, пометьте его атрибутом [NonActionAttribute].
2. Параметры метода действия
Все параметры, переданные контроллеру, находятся в объекте Request. Кроме значений параметров, оттуда можно извлечь куки и строку запроса. Извлекать параметры очень просто:

int id = Convert.ToInt32(Request["id"]);

Также доступен объект Response. В отличие от стандартных объетов Response и Request ASP.NET (которые являются запечатанными объектами) эти реализуют соответствующие интерфейсы System.Web.IHttpResponse и System.Web.IHttpRequest. Это обстоятельно позволяет создавать псевдообъекты для тестирования контроллеров.
Есть еще более удобный способ извлекать параметры. Для этого метод контроллера должен просто принимать параметр соответствующего типа.

Важно! Не забывайте простую вещь, что именно эти правила определяют роутинг. Если у вас в правилах по дефолту идентификатор передается как id, то в методы вы должны определить параметр точно с таким именем (id, а не CategoryId, например). Если вам нужно передавать именно CategoryId, добавляйте соответствующее правило.


Второй важный момент - тип параметра. Если метод принимает int, а вы передаете ему "hello", возникнет исключение.

Если параметр не совпадает с шаблоном роутинга, или если он вообще не передан, его значение в методе будет null.
Вызывать контроллер можно двумя способами: обычным для MVC (Category/Show/5) и обычным для ASP.NET (Category/Show?id=5).
В конце кода метода действия обычно идет метод RenderView("..."), в качестве параметра указывается имя представления (т.е. страницы, попросту говоря, которая должна быть отображена в браузера при обращении к данному методу).
Кстати, если вызывается метод, которого на самом деле нет, то естественно, возникает ошибка 404. Можно переопределить метод HandleUnknownAction(string actionName) и перенаправить запрос к другому методу действия, используя RedirectToAction (этот метод позволяет вызвать не только другой метод действия в данном контроллере, но и метод любого доступного контроллера).


Ссылки по теме:
ScottGu о контроллерах
ASP.NET Model View Controller Applications > Controllers and Controller Actions

VS 2008 Service Pack 1. Решает одни проблемы и добавляет другие

Итак, вышли два новеньких сервис-пака, призванные устранить немеренное количество багов и добавить новые. Качать здесь. Прошу учесть, что эти сервис паки - тоже беты, что позволяет их разработчикам выпускать бесчисленное множество последующих версий.
Итак, какие баги, видимые невооруженным глазом, устраняет VS SP1 ?
1. Устранена белиберда с копированием файлов Entity Framework (я имею в виду, что теперь, если ваша модель edmx размещена в одном проекте, а вызываете вы ее в другом, то теперь не надо копировать файлы ssdl, csdl и msl в этот проект). Но копировать строку подключения надо по-прежнему (а то получите сообщение об ошибке Unable to load the specified metadata resource).
2. Устранен дурацкий баг с закрытием файла конфига после его сохранения.
3. Диаграммы EF стали еще красивее... (все изменения EF приведены самими разработчиками в соответствующем блоге).
Больше пока не вспомню.
А вот что появилось:
1. Перестал работать роутинг для страницы Default.aspx. Т.е. если раньше вы спокойно задавали

routes.Add(new Route("Default.aspx", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});

то действительно, после набора в строке localhost:1111/ вы переходили по адресу Home/Index. Теперь браузер упорно зависает на пустой странице default.aspx и ни с места. Пока я эту проблему решаю так: добавляю в default.aspx.cs обработчик Page_Load, где делаю явную переадресацию

Response.Redirect("~/Home");

Топорно, но, по крайней мере, работает.