Pull to refresh

Пример web-проекта на VS2010

Website development *
Выход VS 2010 для меня, в первую очередь, это возможность работать с .Net 4, Entity Framework 4, ASP.NET MVC 2.
Все полученные теоретические знания, на мой взгляд должны быть выражены в практическом опыте. Поэтому как только представилась возможность, я реализовал проект с использованием VS 2010. И теперь готов поделиться своими впечатлениями от новых возможностей.
Статья рассчитана на искушенных разработчиков )



Не так давно на одном из собеседований мне предложили задачу, которую я решил сделать с использованием VS 2010, т.к. в условиях не было оговорено другого. Это задание для меня было прежде всего хорошим поводом познакомиться с предлагаемыми возможностями.
Коротко о условиях: Написать web-приложение для поиска и отображения данных из таблицы Customers БД MS SQL Server NorthWind. Поиск должен осуществляться по следующим критериям: страна (country), город (city), название компании (company name).  Учитывать, что условия поиска могут быть заданы частично.

Мне больше всего нравиться создавать удобные для пользователя решения. На мой взгляд лучшим решением для пользователя в web является ajax. То есть работа сайта без перезагрузки страницы. Именно такой интерфейс я поставил своей целью создать.

Выбор деталей


Я решил выбирать то что мне больше нравиться из инструментов с которыми работаю постоянно.
Во-первых это jQuery. Потому что мне часто приходиться разрабатывать rich интерфейсы и потому что я люблю этот framework. За долго до его попадания в VS, и его популяризации. Благодаря его гибкости чувствуешь себя джедаем )
Во-вторых это Entity Framework 4. С ним знаком не так давно, с версии 3.5. MS рекомендует именно EF для разработчиков, а чтение блога разработчиков EF убедило меня в том что EF 4 будет тем что от него ждут. Наконец-то есть встроенная возможность отделить адаптеры от дата объектов. Об этом я обязательно напишу позже, я имею в виду Poco генерацию объектов на основе t4 templates. А наличие Linq to Entity добавляет этому фреймверку такого же ощущения легкости. Световой меч мне! )
В третьих, ASP.NET MVC 2  - это основа нашего приложения. Когда её не было я сделал нечто очень похожее с помощью подручных средств (rewrite путей, свои клиентские view, контроллеры в виде web services, ...) и с удовольствием все это выкинул, когда вышло решение от MS. Мне повезло использовать mvc начиная с релизов. Это ещё одна моя любовь )
И последнее, для ajax я решил использовать web services. Дело в том что я занят разработкой гетерогенных приложений, в которых web это только часть системы, а есть ещё куча другого софта. Да, я намекаю на WCF ) Т.к. наши web service обмениваются DTO, то мы можем добавить к ним data атрибуты и сделать из них сервис контракты и наше приложение смогут использовать другие. Ну разве не прелесть? )

Паззл


Что касается архитектуры, то я придерживаюсь общепринятых стандартов. Я очень рад, что MS однозначно выразила свою точку зрения на этот счет. То что достаточно помнить, для нашего проекта:

  • принцип меньшей связанности
  • каждый слой знает только о предыдущем

Под слоями подразумевается

  • View (xHtml + js)
  • (Web) Application (asp.net vew, web services)
  • Data Access Layer (Entity Framework)
  • Database (SQL Server)

между собой они общаются с помощью Data Transfer Object (DTO)
У меня есть привычка — всегда начинать с View )

Делаем view


я изменил Index.aspx
  1. <form id="customer-search-form" action="/" enctype="application/x-www-form-urlencoded" method="get">
  2.  
  3. <label>Company Name <input name="name" /></label>
  4. <label>City <input name="city" /></label>
  5. <label>Country <input name="country" /></label>
  6.  
  7. <input type="submit" value="search" />
  8. </form>
  9.  
  10. <div id="customer-search-result">
  11.  <table>
  12.   <thead>
  13.   <tr>
  14.    <th class="CustomerID">id</th>
  15.    <th class="CompanyName">name</th>
  16.    <th class="ContactTitle">contract</th>
  17.    <th class="City">city</th>
  18.    <th class="Country">country</th>
  19.    <th class="Phone">phone</th>
  20.    <th class="Fax">fax</th>
  21.   </tr>
  22.   </thead>
  23.   <tbody></tbody>
  24.  </table>
  25. </div>
* This source code was highlighted with Source Code Highlighter.

Тут наверно все понятно. Поясню только название классов в th. Они буду использоваться как теги, для отображения выборки. В проект я решил добавить несколько plugin'ов к jQuery:

  • DataTable нужен для отображения грида, page'натора, сортировки.
  • Autocomplete для удобного выбор вариантов поиска в форме.
  • Json для того чтоб корректно передать запрос web service'ам.

Добавляем в Site.Master ссылки на подключенные plugin'ы и добавляем свой js файл, я назвал его customer-list.js

JavaScript


Я знаю что такое closure, поэтому добавляем в начале и конце:

(function(){

$(function(){

//....код мы будем писать здесь...

});

})();


* This source code was highlighted with Source Code Highlighter.

заодно добавляя старт нашего кода на загрузку. Т.к. у нас одно view, то я не стал делать jQuery plugin
Настраиваем Ajax для соединения с web services:

  $.ajaxSetup({
    type:'post',
    contentType: 'application/json; charset=utf-8',
    dataType: "json"
  });


* This source code was highlighted with Source Code Highlighter.

Находим то с чем будем работать:

  var $result = $('#customer-search-result');
  var $form = $('#customer-search-form');


* This source code was highlighted with Source Code Highlighter.

Вопрос, наверно, вызывает $ в начале переменных ) PHP тут ни при чем ) Я называю так переменные, которые обернуты в jQuery, для того чтоб их было сразу видно. И понятно что от них ожидать.
Далее, я настраиваю Autocomplete. Т.к. у нас будет три поля и три соответствующих сервиса, то проще объединить настройки в один объект, а потом и пользоваться:

  var paramsAutocomplete = {
    name: '',
    queryParams: function(info){
      var res = {};

      res[this.name] = info.q;

      return $.toJSON(res);
    },
    formatItem: function(row, i, max, term) {
      return row.replace(new RegExp("(" + term + ")", "gi"), '$1');
    },
    parse: function(data) {
      var parsed = [];

      $.each(data.d, function(){
        parsed.push({data: this, value: this, result: this});
      });

      return parsed;
    }
  };


* This source code was highlighted with Source Code Highlighter.

Здесь стоит обратить внимание на функцию queryParams в стандартной поставке Autocomplete не дает изменять свои параметры так. Для того чтоб этот код работал я чуть-чуть  изменил этот plugin, конечно оставив возможность работы по умолчанию:

data: options.queryParams ? options.queryParams({
  q: lastWord(term),
  limit: options.max
}) : $.extend({
  q: lastWord(term),
  limit: options.max
}, extraParams),


* This source code was highlighted with Source Code Highlighter.

так выглядит 348 строчка в нем, там где происходит Ajax запрос. Это сделано для совместимости с web services.

Подключаем Autocomplete:

  var $name = $form.find('[name=name]')
    .autocomplete(
      'Services/Customers.asmx/CompanyNames',
      $.extend(paramsAutocomplete, {name:'name'}));

  var $city = $form.find('[name=city]')
    .autocomplete(
      'Services/Customers.asmx/Cities',
      $.extend(paramsAutocomplete, {name:'city'}));

  var $country = $form.find('[name=country]')
    .autocomplete(
      'Services/Customers.asmx/Countries',
      $.extend(paramsAutocomplete, {name:'country'}));


* This source code was highlighted with Source Code Highlighter.

Собираем поля, которые будем отображать:

  var $table = $result.find('table');

  var labels = (function(){
    var res = [];
    $table.find('thead th').each(function()
    {
      res.push($(this).attr('class'));
    });
    return res;
  })();


* This source code was highlighted with Source Code Highlighter.

Далее мы должны настроить DataTable для работы с нашим источником данных — web services.

  var lastLength = 10;

  var dataTable = $table.dataTable({
    bProcessing: true,
    bServerSide: true,
    sPaginationType: 'full_numbers',
    sAjaxSource: 'Services/Customers.asmx/Search',
    fnServerData: function ( sSource, aoData, fnCallback ) {

        // для поиска нужных параметров в aoData
        var findByLabel = function(o, name)
        {
          var res = null, find = false;
          $(o).each(function(){
            if(!find && this.name === name)
            {
              find = true;
              res = this.value;
            }
          });
          return res;
        };

        // конвертируем данные в понятный dataTable'у формат
        var convert = function(list)
        {
          var res = [];

          $(list).each(function(){
            var item = [];
            var row = this;

            $(labels).each(function()
            {
              item.push(row[this]);
            });

            res.push(item);
          });
          return res;
        };

        var onPage = aoData ? findByLabel(aoData, 'iDisplayLength') : lastLength;
        var start = aoData ? findByLabel(aoData, 'iDisplayStart') : 0;
        var sortField = aoData ? findByLabel(aoData, 'iSortCol_0') : 0;
        var sortIsAsc = aoData ? findByLabel(aoData, 'sSortDir_0') === 'asc' : true;

        lastLength = onPage;

        // загружаем данные и отправляем на прорисовку
        // забираем введенные данные с формы
        $.ajax( {
            url: sSource,
            data: $.toJSON({
            name: $name.val(),
            city: $city.val(),
            country: $country.val(),
            order: {
              Field: labels[sortField],
              Dir: sortIsAsc ? 'Ascending' : 'Descending'
            },
            start: start,
            onPage: onPage
          }),
          success: function(data){

            fnCallback({
              iTotalRecords: data.d.Count,
              iTotalDisplayRecords: data.d.Count,
              aaData: convert(data.d.List)
            });
          }
        } );
      }
    });


* This source code was highlighted with Source Code Highlighter.


Была бы моя воля, я бы этот DataTable совсем по другому написал. Слишком многих движений он требует для работы с ним, да и тяжеловат. Этот кусок кода мне не нравиться больше всего, очень громоздкий. DataTable позволяет реализовать эту же функциональность с помощью своих внутренних механизмов, но код, как я предполагаю, получится ещё менее приятным.
Добавляем обработку submit к форме

  $form.submit(function(){

    dataTable.fnDraw(true);

    return false;
  });


* This source code was highlighted with Source Code Highlighter.

С JS мы закончили. Первое на что можно обратить внимание в VS стало удобно работать с JS кодом. Больше нет прыжков с не понятными отступами, тормозов с валидацией кода. Наличие подсказок по функционалу jQuery и добавленные Lint JS делают из VS один из  лучших редакторов JavaScript кода.

Web Services


Создаем web service Customers.asmx, разкомментируем [ScriptService], добавляем using System.Web.Script.Services;
создаем методы:

  • string[] CompanyNames(string name)
  • string[] Cities(string city)
  • string[] Countries(string country)
  • Result<VS2010.Customers> Search(string name, string city, string country, Sort order, int start, int onPage)

Добавляем необходимые атрибуты:

        [WebMethod]
        [ScriptMethod(ResponseFormat = ResponseFormat.Json)]

Они необходимы для работы в формате Json, он больше подходит для передачи текстовых данных в web по многим причинам: лаконичнее, проще читается, быстрее parse'тся. 
Первые три метода наполняются однотипно, приведу пример только одного из них:

      using (var context = new NorthwindEntities())
      {
        return context.Customers
          .Where(c => c.CompanyName.Contains(name))
          .Select(c => c.CompanyName)
          .Distinct()
          .OrderBy(n => n)
          .ToArray();
      }


* This source code was highlighted with Source Code Highlighter.

Открываем контекст, находим, отдаем то что от нас ждут. Linq to Entities превратит это в select и он материализуется только когда мы вызовем ToArray. Кстати, почему я использую именно ToArray. Причина в том что так проще будет заставить этот сервис работать под WCF. Т.к. массивы могут быть описаны в контрактах.

Поиск, сложнее:

    [WebMethod]
    [ScriptMethod(ResponseFormat = ResponseFormat.Json)]
    public Result<VS2010Lab.Customers> Search(string name, string city, string country, Sort order, int start, int onPage)
    {
      // проверяем есть ли поле по которому собрались сортировать
      if (order == null || !typeof(VS2010Lab.Customers).GetProperties().Any(f => f.Name == order.Field))
        order = new Sort {Field = "CompanyName", Dir = SortDirection.Ascending};        

      using (var context = new NorthwindEntities())
      {
        // фильтруем
        var res = context.Customers
          .Where(c => string.IsNullOrEmpty(name) || c.CompanyName.Contains(name))
          .Where(c => string.IsNullOrEmpty(city) || c.City == city)
          .Where(c => string.IsNullOrEmpty(country) || c.Country == country)
        // сортируем
          .Order(order);

        // отдаем свой, специальный DTO
        return new Result<VS2010Lab.Customers>
        {
          // общее количество для pagenator'а
          Count = res.Count(),
          List = res.Paginate(
            new ListFilter
              {
                Start = start,
                Count = onPage
              }
          ).ToArray()
        };
      }
    }


* This source code was highlighted with Source Code Highlighter.

На что тут стоит обратить внимание: Order, Paginate. Их я описал как extension'ы для IQueryable:

public static IQueryable Order(this IQueryable query, Sort sort)
  {
   return sort.Dir == SortDirection.Ascending ?
    query.Order(sort.Field) :
    query.OrderDescending(sort.Field);
  }

  public static IQueryable Order(this IQueryable query, string name)
  {
   return query.ApplyOrder(name, "OrderBy");
  }

  public static IQueryable OrderDescending(this IQueryable query, string name)
  {
   return query.ApplyOrder(name, "OrderByDescending");
  }

  public static IQueryable Paginate(this IQueryable query, IListFilter filter)
  {
   return filter.Count > 0 ? query.Skip(filter.Start).Take(filter.Count) : query;
  }


* This source code was highlighted with Source Code Highlighter.


Entity Framework 4


Создаем ADO.NET Entity Data Model (edmx), добавляем туда табличку Customers. 

«Тыкаем» правой кнопкой мыши в белую область экрана нашей модели и выбираем Add Code Generation Item

Add Code Generation Item

И тут пользуемся ещё одной фичей VS 2010, загрузкой Online Template'а. открываем, выбираем ADO.NET C# POCO Entity Generator, вбиваем имя, например Northwind.tt, жмем Add )
Add ADO.NET C# POCO Entity Generator
Переходим в свойства модели (F4), выбираем:

  • Code Generaion Strategy None
  • DDL Generation Template Northwind.tt

Теперь у нас есть набор Data Object (DO) и контекст работы с ними. И они не лежат вместе, как это было в EF3.5 )

результат

Voila! )

Выводы


Не смотря на большой объем статьи я обхватил не так много, многое касается не только VS2010, но без неё этот проект был бы совсем другим )
Свою работу в VS я начал с версии 5, ещё студентом. Студия сильно с тех пор поменялась ) И я рад что внимание к улучшению качества и удобства работы программиста остается на должном уровне. А так же то что MS, надеюсь окончательно, повернулась лицом к web разработчикам. В IE9 объявлена поддержка HTML5, но это уже совсем другая история ).
То что я сознательно упустил из обзора, дабы не захламлять мелочами, можно посмотреть в самом проекте.

ps: Если Вам понравилась статья, проголосуйте за нее, она участвует в конкурсе )
Tags:
Hubs:
Total votes 87: ↑60 and ↓27 +33
Views 9.3K
Comments Comments 27