ASP.NET MVC: История одного проекта "UI - всё для пользователя" (часть 5)

создано: 5/14/2012 | опубликовано: 5/14/2012 | обновлено: 12/11/2017 | просмотров: 12819

В этой части займемся пользовательским интерфейсом (UI). Моё, сугубо личное мнение, что пользовательский интерфейс (имеется в виду функциональные возможности, а не картинки и разноцветный текст) является одним из самых важных критериев качественного сайта.

Содержание

ASP.NET MVC: История одного проекта "Готовимся к старту" (часть 1)
ASP.NET MVC: История одного проекта "Всё ради данных" (часть 2)
ASP.NET MVC: История одного проекта "Шаблоны и внешний вид" (часть 3)
ASP.NET MVC: История одного проекта "Еще немного классов" (часть 4)
ASP.NET MVC: История одного проекта "UI - всё для пользователя" (часть 5)
ASP.NET MVC: История одного проекта "UI - Добавление экспоната" (часть 6)
ASP.NET MVC: История одного проекта "UI - Редактирование экспоната" (часть 7)
ASP.NET MVC: История одного проекта "Обработка ошибок" (часть 8)
ASP.NET MVC: История одного проекта "Фильтрация" (часть 9)
ASP.NET MVC: История одного проекта "Поиск" (часть 10)
ASP.NET MVC: История одного проекта "Облако тегов" (часть 11)
ASP.NET MVC: История одного проекта "Главная страница" (часть 12)

Что имеем и к чему стремиться

В предыдущей части были подготовлены шаблоны, классы, репозитории и контроллеры. Теперь пришло время привести внешний вид представлений (Views) экспонатов музея к адекватному виду (про ленту говорить в этой статье не  буду). Я обещал, что к моменту написания следующей (этой) части, перенесу данные и пользователей в одну новую базу, и все последующие изменения в структуре данных будут происходить уже по средствам EF миграций (Migrations). Показывать как я проделал операцию переноса не буду, скажу что после экспорта данных в новую базу я запустил aspnet_regsql.exe (об этой утилите было рассказно в первой части), потом завел одного пользователя и добавил ему роль "Администратор". Еще надо в web.config поправить строку подключения к базе с пользователями.

<connectionStrings>
  <add name="ApplicationServices" 
    connectionString="Data Source=(local);
      Initial Catalog=museumDb;Integrated Security=True" 
    providerName="System.Data.SqlClient" />
  <add name="MuseumContext" 
    connectionString="Data Source=(local);
      Initial Catalog=museumDb;Integrated Security=True" 
    providerName="System.Data.SqlClient" />
</connectionStrings>

Обратите внимание на значения параметров Initial Catalog для обоих строк - теперь они одинаковые.

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

А вот так выгдядит страница детальная информации:

Ужасный вид, не правда ли? А вот так выглядит форма добавление нового экспоната:

А так форма редактирования:

Душераздирающее зрелище! Особенно не радует тот факт, что я совершенно забыл разграничить доступ. То есть на страницы, которые должны заходить только администраторы, меня сайт запустил без проверки моей учетной записи и, соответстветнно, моих ролей. Сделаем это сейчас. Для этого надо в контроллере Museum на методами Edit и Create поставить атрибут (AuthorizeAttribute), который потребует от пользователя наличие роли "Administrator":

[Authorize(Roles="Administrator")]
public ActionResult Create() {
  /// ...
}
[HttpPost]
[Authorize(Roles = "Administrator")]
public ActionResult Create(Exhibit exhibit) {
  //...
}

и для методов редактирования:

[Authorize(Roles="Administrator")]
public ActionResult Edit() {
  /// ...
}
[HttpPost]
[Authorize(Roles = "Administrator")]
public ActionResult Edit(Exhibit exhibit) {
  //...
}

Теперь, если попытаться открыть страницу с формой добавления или редактирования какого-нибудь экспоната, сайт перенаправит меня на страницу входа. Именно такое поведение я и планировал.

Полезная оптимизация №1

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

Потом создаю новую строку ресурса:

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

public class AdminOnlyAttribute : AuthorizeAttribute
{
  protected override bool AuthorizeCore(System.Web.HttpContextBase httpContext)
  {
    if (httpContext == null) throw new ArgumentNullException("httpContenxt");
    if (!httpContext.User.Identity.IsAuthenticated) return false;
    bool authorize = httpContext.User.IsInRole(Resource.AdminRoleName);
    return authorize;
  }
}

При таком подходе, я могу, просто изменив (например, при переносе в другую систему функционал связанный с авторизацией) название роли в файле ресурсов, скопировать сборку по работе с авторизованными пользователями. И раз теперь я имею такой атрибут, применим его в контроллере:

[AdminOnly]
public ActionResult Create() {
  /// ...
}
[HttpPost]
[AdminOnly]
public ActionResult Create(Exhibit exhibit) {
  //...
}

и для методов редактирования:

[AdminOnly]
public ActionResult Edit() {
  /// ...
}
[HttpPost]
[AdminOnly]
public ActionResult Edit(Exhibit exhibit) {
  //...
}

Вот такое небольшое отступление от темы получилось. Надеюсь? оно было полезным (пишите в комментариях, нужные ли они в статье или можно без них). Теперь дальше пройдемся по MVC framework'у.

Лирическое отступление на тему "главная страница"

Чтобы привести главную страницу к нормальному виду, я для начала сделаю очередное отступление. Дело в том, что я взял за привычку не "вытаскивать" модели на представления (Views). Каюсь, что не показал как это делается во второй части "истории одного проекта" (далее ИОП) при отрисовке меню сайта (Hall), хотя мне и надо было всего лишь проверить, что контейнер работает правильно. На данный момент у меня меню сайта, также отображается с использованием ViewModel:

public class HallViewModel : Humor {
  public HallViewModel(Hall hall) {
    this.Id = hall.Id;
    this.Name = hall.Name;
  }
  /// <summary>
  /// Наименование
  /// </summary>
  [Display(Name = "Наименование")]
  [Required(ErrorMessage = "Наименование - обязательно поле")]
  [StringLength(50, ErrorMessage = "Наименование не может длиннее 50 символов")]
  public string Name { get; set; }
}

И теперь, чтобы быть последовательным, я постараюсь подробно рассказать, как я буду делать это для сущности Exhibit так, как будто у меня это первая сущность, в этом проекте, которая должна превращаться во ViewModel и обратно. Итак поехали...

Model -> ViewModel -> View

Для начала создаю в папке Models еще одну папку, называю ее ViewModels. Теперь в этой папке создаю новый класс ExhibitViewModel:

namespace Calabonga.Mvc.Humor.Models {
  /// <summary>
  /// ViewModel для сущности Exhibit
  /// </summary>
  public class ExhibitViewModel : Humor {
    public ExhibitViewModel(Exhibit exhibit) {
      this.Id = exhibit.Id;
      this.Content = exhibit.Content;
      this.CreatedAt = exhibit.CreatedAt;
      this.HallName = exhibit.Hall.Name;
      this.HallId = exhibit.HallId;
      this.Tags = exhibit.Tags;
      this.Title = exhibit.Title;
      this.TotalViewCount = exhibit.TotalViewCount;
      this.UpdatedAt = exhibit.UpdatedAt;
      this.PeriodViewCount = exhibit.PeriodViewCount;
    }

    /// <summary>
    /// Содержание
    /// </summary>
    [Display(Name = "Содержание")]
    [Required(ErrorMessage = "Содержание - обязательно поле")]
    [DataType(DataType.MultilineText)]
    public string Content { get; set; }

    public DateTime CreatedAt { get; set; }

    /// <summary>
    /// Идентификтор зала
    /// </summary>
    [Display(Name = "Зал экспозиции")]
    public string HallName { get; set; }

    /// <summary>
    /// Идентификтор зала
    /// </summary>
    [Required(ErrorMessage = "Номер зала - обязательное поле")]
    [Display(Name = "Номер зала")]
    public int HallId { get; set; }

    /// <summary>
    /// Навигационное свойство для связи с метками
    /// </summary>
    public ICollection<Tag> Tags { get; set; }

    /// <summary>
    /// Заголовок
    /// </summary>
    [Display(Name = "Заголовок")]
    [Required(ErrorMessage = "Заголовок - обязательно поле")]
    [StringLength(255, ErrorMessage = "Заголовок не может длиннее 255 символов")]
    public string Title { get; set; }

    /// <summary>
    /// Дата обновления
    /// </summary>
    [Display(Name = "Дата обновления")]
    [DataType(DataType.Date)]
    public DateTime? UpdatedAt { get; set; }

    /// <summary>
    /// Общее количество просмотров
    /// </summary>
    [Display(Name = "Общее количество просмотров")]
    public int TotalViewCount { get; set; }

    /// <summary>
    /// Общее количество просмотров за период
    /// </summary>
    [Display(Name = "Общее количество просмотров за перид")]
    public int PeriodViewCount { get; set; }
  }
}

Могу догадаться, что в какой-то момент времени, у вас может возникнуть вопрос: "Зачем это ViewModel нужен, если можно показать на предствалении (View) просто модель и не париться?". На что готов ответить следующее. Бывают моменты в процессе разработки, когда приходится менять структуру данных так сильно (речь идет о серьезных, больших проектах... например, на основании требования заказчика), что применение изменений для представлений, репозиториев и контроллеров под это обновление, если не в корне "убивает" всю проделанную до этого работу, то ужасно сильно затягивает процесс введения обновлений и замедляет разработку. Сие утверждение проверено на собственном опыте. Даже если вы больше никогда не будете изменять проект (структуру), хуже от того, что вместо моделей на представлении появятся их "заменители" (ViewModels) точно не будет. Единственное что вы потеряете - немного времени, зато в будующем оно (затраченное время) к вам вернется с лихвой, если придется расширить или обновить уже существующий функционал (структуру данных). Так что мой вам совет: всегда создавайте ViewModel для Model чтобы отобразить ее на представлении (View).

Итак, ViewModel есть, теперь немного расширений. Я создаю файл расширений в папке Engine, под названием ModelsExtensions (namespace я оставил Calabonga.Mvc.Humor), в который напишу расширение для класса Exhibit (и для Hall тоже).

public static class ModelsExtensions {
  #region Exhibit
  public static IEnumerable<ExhibitViewModel> ToViewModels(this IEnumerable<Exhibit> source) {
    if (source == null) { return null; }
    List<ExhibitViewModel> result = new List<ExhibitViewModel>();
    Parallel.ForEach(source, x => result.Add(new ExhibitViewModel(x)));
    return result;
  } 
  #endregion
  #region Hall
  public static IEnumerable<HallViewModel> ToViewModels(this IEnumerable<Hall> source) {
    if (source == null) { return null; }
    List<HallViewModel> result = new List<HallViewModel>();
    Parallel.ForEach(source, x => result.Add(new HallViewModel(x)));
    return result;
  } 
  #endregion
}

Контроллер тоже надо "поправить" изменив метод Index, который получает данные для главной страницы. Главное изменение выделено жирным:

public ViewResult Index(int? id) {
  var model = exhibitRepository
    .AllIncluding(exhibit => exhibit.Hall,
      exhibit => exhibit.Tags)
    <strong>.ToViewModels();
</strong>  return View(model.ToPagedList(id ?? 1));
}

А вот теперь пришло время поговорить о представлении (View) главной страницы музея юмора. Я немного поколдовал над внешиним видом: теперь страница выглядит так:

Главное отличие от предыдущего вида в том, что это теперь не <table>, а <div>. Я не случайно выделил на картинке место желтым маркером, тут должны быть метки (Tags), Хотя нет... об этом позже. Немного кода разметки. Так выглядит код главной страницы:

@model PagedList<Calabonga.Mvc.Humor.Models.ExhibitViewModel>
@{
  ViewBag.Title = "Экспонаты музея юмора";
}
@section header{
  <h2>
    Экспонаты музея юмора</h2>
}
@if (Model != null && Model.Count > 0) {

  foreach (Calabonga.Mvc.Humor.Models.ExhibitViewModel item in Model) {
    <strong>@Html.Partial("Templates/ExhibitTemplate", item);</strong>
  }  

  @Html.PagerForPagedList(Model.PageIndex, Model, "index")
}

Обратите внимание, на жирную строку. Эта строка вызывает отрисовку шаблона для каждого объекта типа Exhibit. Код шаблона для Exhibit:

@model Calabonga.Mvc.Humor.Models.ExhibitViewModel
<div class="item">
  <div class="title">@Html.ActionLink(Model.Title, "show", new {id = Model.Id}, null)</div>
  <div class="info">@Html.ActionLink(@Model.HallName, "index", new { hall = Model.HallId })
    @Html.DisplayFor(x => Model.CreatedAt)
  </div>
  <div class="content">@Html.Raw(Model.Content)</div>
  <div class="tags">@Html.DisplayFor(x => Model.Tags)</div>
</div>

Вот только Tags указаны в разметке, но не отображаются правильно, это как раз то самое место, которое я выделил на картинке. Надо исправить данный казус. Я буду отрисовывать на странице метки при помощи специльного представления для отображения, который помещается в папку Shared/DisplayTemplates. Потому для редактирования меток к записям будет использоваться другой шаблон в другой папке Shared/EditorTemplates. Создаю папку DisplayTemplates и новое в ней представление (View) с именем Tag.cshtml.

Содержимое этой View:

@model Calabonga.Mvc.Humor.Models.Tag
<span class="tag">
  @Html.DisplayFor(modelItem => Model.Name)
</span>

Теперь мне осталось просто запустить проект. MVC 3 Framework на столько "умный", что сам поймет, что при отображение коллекции меток (Tags) нужно будет использовать именно этот шаблон для отрисовки каждой метки. Я пока просто показываю названия меток, в дальнейшем они должны стать кликабельны.

Я сейчас заметил, что короткие экспонаты отображаются корректно, а вот длинные, занимают очень много места. Надо бы устранить сию оплошность и сделать "обрезание" лишнего текста, тем более, что всё равно любой из экспонатов можно посмотреть в отдельном виде (метод контроллера Details я вместе с представлением переименовал в Show). Я создал класс-помощник, который будет в дальнейшем наращивать свои амбиции и возможности. Пока в нем единственный метод CutLongText.

public static string CutLongText(string text, int maxLength, string appendText) {
  //...
}

Осталось правильно его применить на правильном представлении... Но в каком?!... Да вы совершенно правы! Нужно еще одно преставление (View). Создаю новое представление для сущности Exhibit в папке Templates, назову его ExhibitBriefTemplate:

@model Calabonga.Mvc.Humor.Models.ExhibitViewModel
<div class="item">
  <div class="title">@Html.ActionLink(Model.Title, "show", new {id = Model.Id}, null)</div>
  <div class="info">@Html.ActionLink(@Model.HallName, "index", new { hall = Model.HallId })
    @Html.DisplayFor(x => Model.CreatedAt)
  </div>
  <strong><div class="content">@Html.Raw(Calabonga.Mvc.Humor.Engine.MuseumHelpers.CutLongText(Model.Content, 280, "..."))</div>
</strong>  <div class="tags">@Html.DisplayFor(x => Model.Tags)</div>
</div>

    Ииивавы

И, соответственно, я этот шаблон буду использовать в главном представлении главной странице:

@model PagedList<Calabonga.Mvc.Humor.Models.ExhibitViewModel>
@{
  ViewBag.Title = "Экспонаты музея юмора";
}
@section header{
  <h2>
    Экспонаты музея юмора</h2>
}
@if (Model != null && Model.Count > 0) {

  foreach (Calabonga.Mvc.Humor.Models.ExhibitViewModel item in Model) {<strong>
    @Html.Partial("Templates/ExhibitBriefTemplate", item);</strong>
  }  

  @Html.PagerForPagedList(Model.PageIndex, Model, "index")
}

Теперь код теперь в самом шаблоне ExhibitBriefTemplate:

@model Calabonga.Mvc.Humor.Models.ExhibitViewModel
<div class="item">
  <div class="title">@Html.ActionLink(Model.Title, "show", new {id = Model.Id}, null)</div>
  <div class="info">@Html.ActionLink(@Model.HallName, "index", new { hall = Model.HallId })
    @Html.DisplayFor(x => Model.CreatedAt)
  </div>
  <div class="content">@Html.Raw(
        Calabonga.Mvc.Humor.Engine.MuseumHelpers.CutLongText(
            Model.Content, 280, 
            string.Concat(" ", 
              @Html.ActionLink("[дальше...]", 
            "show", 
            new { id = Model.Id }, 
            null).ToString())))</div>
  <div class="tags">@Html.DisplayFor(x => Model.Tags)</div>
</div>

Соответственно, теперь сам внешний вид главной страницы музея принял другой вид (экспонаты с длинным текстом отмечены желтым маркером)

Заключение или что дальше.

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

P.S.: Я могу не описывать детально, как я делал перечисленные в заключении представления. Если всё всем понятно, можно будет заняться чем-нибудь более полезным в следующих статьях, а представления я сделаю приватно (без написания статьи). Пожелания, замечания, предложения и конструктивная критика принимаются в комментариях.

Спасибо читателям

Ознакомившись с комментариями, я сразу же выложу правильный код:

public static class ModelsExtensions {
  #region Exhibit
  public static IEnumerable<ExhibitViewModel> ToViewModels(this IEnumerable<Exhibit> source) {
    if (source == null) { return null; }
    List<ExhibitViewModel> result = new List<ExhibitViewModel>();
    foreach (Exhibit item in source) {
      result.Add(new ExhibitViewModel(item));
    }
    return result;
  }
  #endregion

  #region Hall
  public static IEnumerable<HallViewModel> ToViewModels(this IEnumerable<Hall> source) {
    if (source == null) { return null; }
    List<HallViewModel> result = new List<HallViewModel>();
    foreach (Hall item in source) {
      result.Add(new HallViewModel(item));
    }
    return result;
  }
  #endregion
}

Комментарии к статье (8)

3/20/2012 3:00:30 PM Artyom Krivokrisenko|20.05.2012 15:00:30

1) Совершенно лишний абстрактный класс Humor. Наследование ViewModel'ов от этого класса излишне, так как эта генерализация все равно никак не используется в проекте, только добавляется сильная связанность между классами.

2) Вы создаете отдельные ViewModel на случай если очень сильно прийдется менять модель данных. Это правильно. View "отвязывается" от конкретной модели данных. И при этом сразу же завязываете сам ViewModel на модель данных (я так понимаю, именно объект EF передается в конструктор классов ViewModel).

При этом ViewModel не имеет пустого публичного конструктора, т.е. проинициализировать его можно только имея на руках объект EF. Если бы публичный конструктор был, можно было бы создать экземпляр ViewModel, проинициализировать налету какими-то тестовыми данными, и тестировать работу отдельных компонентов системы.

Есть смысл в вашем Helper классе создать статические методы, которые принимают объект EF, а возвращают объект ViewModel. По аналогии с теми методами Helper класса, которые работают с наборами объектов.

3) Calabonga.Mvc.Humor.Engine.MuseumHelpers.CutLongText(Model.Content, 280, "...")

Что помешало сделать Extension метод и вызывать его как

Model.Content.CutLongText(280, "...")

?

4) Parallel.ForEach(source, x => result.Add(new ExhibitViewModel(x)));

Дональд Кнут говорил, что преждевременная оптимизация - прямая дорога в ад. Эта фраза подтверждается вашим примером. Преждевременно "заоптимизировав", вы: добавили тормоза и 2 нестабильных сложновоспроизводимых ошибки.

Вы вообще проводили тесты и пришли к выводу что в этом месте тормоза и нужно распараллелить обработку? Я полагаю что нет. Советую провести, и убедиться что на небольших наборах данных TPL будет работать медленнее даже чем обычный LinQ, но при этом потреблять больше ресурсов (расходы на синхронизацию потоков и т.п.).

Это первое. Второе - класс List<T> не является потокобезопасным, следовательно, данный код периодически будет работать некорректно (падать с ошибками, будут пропадать элементы и т.п.). Доступ к нему нужно синхронизировать, что добавит еще больше тормозов. В 4.0 появились потокобезопасные контейнеры, но скорость их работы будет тоже ниже по сравнению с однопоточным режимом.

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

5/20/2012 3:00:49 PM Никита

А зачем в кажом методе контроллера прописывать роль, если это можно сделать на самом контроллере единожды?

5/20/2012 10:54:17 PM Artyom Krivokrisenko

В предыдущем комментарии в пункте 2 я ошибся, я имел в виду что "ViewModel не имеет пубилчного конструктора без параметров".

5/21/2012 1:22:48 AM Calabonga

Никита, в этом контроллере есть и другие методы, которые должны видель посетители сайта. Пока разделения к функционала мне не требуется. Всё будет по ходу.

5/21/2012 2:30:21 AM Calabonga

Во-первых, Artyom Krivokrisenko позвольте выразить вам огромную благодарность за столь полные и развернутые комментарии. Нет, правда, большое спасибо!

Во-вторых, относительно непараметризированного конструктора... У меня еще не возникало потребности в его наличии на данный момент, а ... "Дональд Кнут говорил, что преждевременная оптимизация - прямая дорога в ад.". По поводу  Calabonga.Mvc.Humor.Engine.MuseumHelpers.CutLongText() отвечу так. Этот метод будет не только контент у экпонатов обрезать, а будет использоваться еще как минимум 5-6 местах проекта. Поэтому писать на каждый класс одно и тоже расширение не счел правильным. Но вы безусловно правы, расширение (Extension) в данной версии (незаконченного) проекта было бы оптимальным. Что же касается, четвертого пункта, тут с вами вынужден согласиться. Тем более, что это параллелизация была в этом методе применина в целях тестирования, а в окончательной (опубликованной на сайте) версии статьи я просто забыл про этот тест и оставил случайно. А вот что касается List<T>, я наверное действительно что-то упустил из вида, в особенности то, что безопасность в потоках отсутствует для List<T> (хотя на странице MSDN указано обратное, может есть у вас другое объяснение написанному на странице MSDN ?), но падения с ошибками и пропадания элементов еще ни разу не видел (может поэтому и упустил этот момент).

Еще раз спасибо, за дельные замечания и дополнения. Всё что вы говорите - правильно! Единственное, что я просил бы вас учесть на будущее так это то, что проект в разработке, а значит это не финальная (оптимизированная и оттестирования) версия.

5/21/2012 4:55:50 AM Ustas

Artyom Krivokrisenko,
1. Здесь вижу скорее недочет. Конечно же ViewModel не должен наследоваться от базового класса модели.
2. ViewModel так или иначе строится под какую-то сущность модели, почему тогда конструктор ViewModel не должен принимать на входе объект сущности из EF? С обязательном наличием пустого конструктора во ViewModel согласен с Вами. Собственно в рабочем проекте он есть всегда. Но используется только при тестировании, ибо объект сущности по умолчанию создается в BALе, поэтому ViewModel всегда инициализируется моделью.
 

5/21/2012 4:14:44 PM Artyom Krivokrisenko

По поводу  Calabonga.Mvc.Humor.Engine.MuseumHelpers.CutLongText() отвечу так. Этот метод будет не только контент у экпонатов обрезать, а будет использоваться еще как минимум 5-6 местах проекта. Поэтому писать на каждый класс одно и тоже расширение не счел правильным.

 Я предлагаю сделать Extension метод для типа String, и тогда его можно будет использовать везде где нужно обрезать кусок текста, при условии что это текст лежит в String (странно если он будет лежать в чем-то другом)

А вот что касается List<T>, я наверное действительно что-то упустил из вида, в особенности то, что безопасность в потоках отсутствует для List<T> (хотя на странице MSDN указано обратное, может есть у вас другое объяснение написанному на странице MSDN ?),

В MSDN четко сказано что List корректно работает в многопоточной среде только когда все потоки выполняют его чтение (с только чтением как правило мало какой класс будет иметь проблемы). И далее сказано: To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization. То что у вас никаких ошибок не падало - объясняется тем что проблемы многопоточности редко себя проявляют, сложно диагностируются и воспроизводятся. Также они по разному проявляются в режимах Release и Debug.

Вот пример

            List<int> items = new List<int>();

            Parallel.For(0, 5000, i => items.Add(i));

            Console.WriteLine(items.Count);

Несколько запусков подряд в режиме Debug, в консоль вместо 5000 каждый раз выводится разные значения Count (от 3800 до 4900, но никак не 5000). Что происходит - элемент добавился в массив, но увеличить счетчик элементов не успел, в это время в эту же ячейку массива добавляет элемент другой поток. Учитывая что потоков 8 (по кол-ву ядер), такая ситуация происходит часто.

В один из тестов просто упало исключение

System.IndexOutOfRangeException was unhandled by user code
  HResult=-2146233080
  Message=Index was outside the bounds of the array.
  Source=mscorlib
  StackTrace:
       at System.Collections.Generic.List`1.Add(T item)
       at ListThreadTest.Program.<>c__DisplayClass1.<Main>b__0(Int32 i)
       at System.Threading.Tasks.Parallel.<>c__DisplayClassf`1.<ForWorker>b__c()
  InnerException:

Собственно о чем я и говорил - пропадают элементы и падают неопределенные ошибки.

Что касается конструктора ViewModel

Конструктор без параметров здесь должен быть. Для того чтоб можно было инициализировать объект, не имея на руках объект EF. По большому счету для ViewModel не нужен никакой объект EF, ему нужны данные, а то, откуда они будут взяты - из EF, или из какого-то веб-сервиса, или сгенерированы в тестовом методе - не имеет значения.

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

Делать конструктор который принимает объект EF - это, конечно, вопрос менее принципиальный. Я считаю что не стоит - поскольку нарушается Single responsibility principle. В MVC класс ViewModel может являться обычным DTO без какого-либо поведения или логики, его ответственность - просто хранить в себе определенные данные. Преобразование данных из одной структуры (объекта EF) в другую (объекта ViewModel) - это другая ответственность и она может быть вынесена в другой класс (мне нравится вариант со статических helper'ом).

5/22/2012 1:26:32 AM Calabonga

Уважаемый Artyom Krivokrisenko, очень жаль что вы меня не услашали, а если услышали, но не поняли, то еще хуже. Я же вроде бы "черным по белому" сказал, что идея цикла статей не показать готовое решение, а продемонстрировать процесс разработки. Я прекрасно знаю, что нужно сделать конструктор без параметров, потому что при создании объекта из ViewModel будет ошибка: "No parameterless constructor defined for this object.", потому что MVC не сможет сериализовать такой объект, но я хочу показать своим читателям не только "как надо", но "зачем и почему", а ложка, как вы наверное знаете, к обеду дорога. Комментарии ваши обоснованы и по делу, но вы, уважаемый, забегаете вперед, впрочем, как и с "Преобразование данных из одной структуры" в хелпере. И про Extension для String - это естественно правильной подход, хотя тут у каждого свои методы. Но и это, по большому случаю, "преждевременная оптимизация", о которой вы говорили.

Что же касается List<T>, то я уже покаялся, что прочитал на MSDN русскую инскрукцию, в которой написано: "Делая выбор между классами List<T> и ArrayList, предлагающими сходные функциональные возможности, следует помнить, что класс List<T> в большинстве случаев обрабатывается быстрее и является потокобезопасным.". Именно это меня и ввело в заблуждение.

Но, в любом случае, большое вам спасибо, за участие и за комментарии.