четверг, 7 августа 2008 г.

Entity Framework - использование хранимых процедур

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

Оказалось, очень просто. Ну, во-первых, надо создать саму процедуру. Во-вторых, надо обновить модель - т.е. в ModelBrowser щелкнуть правой кнопкой мыши на модели и выбрать Update Model From Database и там уже выбрать процедуру. После этого она добавляется в модель, но как-то не до конца - в коде она не появится до тех пор, пока вы ей не скажете. А для этого недо щелкнуть на процедуре (в модели) и выбрать Create Function Import. На экране появится окошко, в котором вас спросят, как назвать будущий метод, и что он будет возвращать. После этого можно обращаться к процедуре через контекст, причем обращение как к обычному методу, т.е.

context.Proc(parameters)

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

пятница, 20 июня 2008 г.

MVC - создание и использование повторно используемых компонентов

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

Способ первый. Использование вложенных представлений
Оптимальное решение, на мой взгляд - использование контролов, или встроенных представлений. Однако и оно не без греха. Но рассмотрим все по порядку.

В MS MVC существует класс под названием ComponentController. Это, по сути, обычный контроллер, использующий немного другие методы и сигнатуры. Зачем понадобилось делать дополнительный класс с почти такой же функциональностью, для меня осталась загадка. Итак, вот как используется этот класс:


public class CategoryController : ComponentController
  {
    public ActionResult Index()
    {
      // Add action logic here
      throw new NotImplementedException();
    }
    public void RenderControl(string CategoryName)
    {
      RenderView(CategoryName);
    }
  }

Т.е. видим два существенных отличия: класс наследует ComponentController, а не просто Controller, и второе - по-другому выглядят методы действия. А выглядят они точно также, как выглядели методы действия обычных контроллеров раньше - т.е. возвращают void и рендерят представление с помощью RenderView.

В данном случае имя представления передается как параметр, что позволяет вызывает вызывать представления абсолютно произвольно, используя при этом всего один метод контроллера.

Размещается контроллер вместе с остальными в папке Controllers (думаю, на самом деле эти файлы можно размещать где угодно). А вот с представлением не так все просто. Оно должно лежать в специальной папке Components/[ComponentControllerName]/Views/[ViewName]. Т.е. в корне вы создаете папку Components, в ней произвольную папку, в которой создаете папку Views, а уже сюда кладете файлы представлений.

Дальше все просто. Контроллер компонента создан, представление - тоже, остается только его вызвать. У меня это сделано так: главная страница загружает выпадающий список, в нем на onchange вызывается функция JavaScript, которая с помощью библиотеки Prototype загружает представление с соответствующими параметрами (подробности работы с Prototype смотри в другой записи блога тут). А вот это представление содержит вызов компонента.

Вызов можно осуществлять двумя способами. Во-первых, используя RenderUserControl() - тогда сам вызов будет выглядеть так:

<%= Html.RenderUserControl("~/Views/SomeView.aspx") %>


Как видите, здесь указывается непосредственно имя файла. В ряде случае это может быть совершенно неудобно. Например, когда мы на стадии этого вызова еще не знаем имя файла, или хотим передать другой параметр, а метод чтобы сам решал, какое представление ему вызывать. Для этого используем второй способ:

<% =Html.RenderComponent<CategoryController>(c => c.RenderControl("Public")) %>


Здесь мы просто указываем имя представления, а могли бы, например, передавать какой-нибудь идентификатор. Мы можем брать из модели, загруженной в объект ViewData, любой доступный член. Почему бы не передавать его в качестве параметра, например, вот так:

<% =Html.RenderComponent<CategoryController>(c => c.RenderControl(View.Model.PageId)) %>


К сожалению, подобный код не будет работать (из-за бага в MVC). Вы просто увидите ошибку следующего содержания:

Unable to cast object of type 'System.Linq.Expressions.MemberExpression' to type 'System.Linq.Expressions.ConstantExpression'.

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

protected string RenderComponent(Expression<Action<CategoryController>> action)
    {
      var controller = new CategoryController();
      controller.Context = this.ViewContext;
      var ex = action.Compile();
      ex.Invoke(controller);
      return controller.RenderedHtml;
    }

Вызов будет выглядеть соответственно:

<% =RenderComponent(c => c.RenderControl(ViewData.Model.ParentCategoryName)) %>

Все работает!

Способ второй. Подтягиваем разметку Html из базы данных
Первый способ, безусловно, хорош. Но у него есть один существенный недостаток - что, если состав вложенных контролов постоянно меняется, т.е. попросту говоря, не является детерменированным? Ничего такого, надо просто каждый раз обновлять версию dll на сервере, и все. Тоже выход, но можно ли обойтись без этого? Можно - например, если хранить код разметки в поле таблицы. Этот вариант подходит для не очень большой и сложной разметки, но зато позволяет обойтись без постоянного деплоя.

Как это сделать? Очень просто - не забываем, что мы можем передать представлению любой класс, а уже в этом классе не составит труда загрузить любое поле из таблицы. Например, так:

private string html;
    public CategoryParameters(int? ParentCategoryId)
    {
      using (DataCoreConnection context = new DataCoreConnection())
      {
        var htm = (from p in context.ItemType
             where p.Id == ParentCategoryId
             select p.ParametersHTML).ToList();
        if (((List<string>)htm).Count > 0)
          html = ((List<string>)htm)[0];
      }
    }
public string Html
    { get { return html;} }

А вот так выглядит вызов:
<%=ViewData.Model.Html %>


Третий способ. Подтягивание представления с помощью Ajax
Подробно о том, как использовать Ajax в проектах MVC, рассказано в другой моей заметке (тут), здесь я лишь покажу, как это использовать в наших целях. Вот код JavaScript, который вызывает представление:
function LoadCategory(parentCategory)
{
  var newDiv = document.createElement("div");
  newDiv.id = "ControlDiv_" + parentCategory;
  newDiv.className = "ControlBlock";
  var url = "Controls/GetCategories/" + parentCategory;
  new Ajax.Updater(newDiv, url,
    {
      method: 'get'
    }
  );
  $('Container').appendChild(newDiv);
}

Т.е. здесь мы динамически создаем блок DIV, в который загружаем содержимое представления, а сам блок помещаем в ранее созданный DIV с Id= Container.

На самой странице же достаточно сделать такой вызов:
<script language="javascript" type="text/javascript">
      LoadCategory(1);
    </script>


Полезные ссылки:
Статья Chris Sainty, посвященная RenderComponent() и RenderUserControl()
Using the ComponentController in ASP.NET MVC CTP 2 - Mike Bosch

пятница, 6 июня 2008 г.

Вышла третья версия ASP.NET MVC (Preview) - перелопачиваем проект (а лучше создаем новый)

Значит, вышла, родная. Изменений немерено!
Скачать новую версию можно здесь. Там на странице внизу список доступных файлов. Обязательно скачайте Readme - он содержит перечень всех изменений. А для тех, кто не очень дружит с английским (ну и вообще - для уяснения) перечислю эти изменения здесь.

Контроллеры
Методы действия теперь возвращают экземпляр типа ActionResult. Вернее, один из типов, наследующих ActionResult, потому что ActionResult - абстрактный класс. Вот какие это могут быть типы:

  • ViewResult - такой тип возвращает метод контроллера View (то, что раньше было RenderView), параметры остаются те же

  • EmptyResult - ничего не делает - аналогичен тому, что метод действия возвращает null

  • RedirectResult - перенаправляет на другой URL

  • RedirectToRouteResult - перенаправление по указанному маршруту

  • JsonResult - содержимое ViewData сериализуется в формат JSON

  • ContentResult - рендерит некий текст (обычно это HTML)


Объекты всех этих типов возвращают соответствующие методы контроллера:

  • View

  • Redirect - перенаправляет на новый URL

  • RedirectToAction - перенаправляет на другой метод действия

  • RedirectToRoute - перенаправляет по новому маршруту

  • Json - возвращает экземпляр типа JsonResult

  • Content - рендерит текст (обычно это HTML)


Интересный момент. Если мы пытаемся вернуть объект, не наследующий ActionResult (теоритечески это может быть что угодно), то метод неявно сериализует этот объект в строку (вызывая его ToString()), после чего оборачивает полученный текст в ContentResult и передает его клиенту.

Представления
Теперь ViewData в представлении не заменяется объектом, из которого мы подтягиваем данные. А мы получаем к ним доступ через свойство Model, т.е. вместо ViewData.CitiesForCountry пишем теперь ViewData.Model.CitiesForCountry.

Роутинг
Дефолтные значения теперь задаются проще. Вместо:
routes.Add("route-name", new Route("{locale}/{year}", new MvcRouteHandler()) {
 Defaults = new RouteValueDictionary(new {locale="en-US", year=DateTime.Now.Year.ToString()})
}

теперь чуть проще
routes.MapRoute("route-name", "{locale}/{year}", new {locale="en-US", year=DateTime.Now.Year.ToString()});


Это основные новшества. Некоторые моменты касаются констрейнтов, контролов и т.д.

Установка
Поверх не ставится. Сначала надо удалить вторую версию.

Изменения в проекте
Попробуем скомпилировать наш маленький (пока еще) проект. Ну что ж, для начала неплохо - всего 24 ошибки.
Первое, что делаем - удаляем из списка ссылок ссылки на System.Web.Abstractions, System.Web.MVC, System.Web.Routing и заново добавляем их из C:\Program Files\Microsoft ASP.NET\ASP.NET MVC Preview 3\Assemblies. Компилируем опять, уже лучше - всего 14 ошибок.
Далее по тексту файла Readme меняем код. Все компилируется отлично, но одна маленькая проблема - полностью перестает работать роутинг. Т.е. вообще полностью - хотя маршруты и сохраняются в коллекции, однако перехода к методам контролов не осуществляется. Поэтому мне не осталось ничего другого, как создать новый проект, в котором это работает. Мне, к сожалению, так и не удалось найти "5 отличий" между измененным проектом и новым. Если кто-нибудь знает, как это пофиксить, сообщите пожалуйста.
Update Обнаружилась еще одна неприятная фича (которая, конечно, скорее бага). Если у вас модельная часть с edmx находится в отдельном проекте, то из проекта MVC не удалется получить к ней доступ. Т.е. компилируется проект нормально, но при обращении к странице, которая дергает данные из модели, вываливаются непонятные ошибки вроде "включите ссылку на namespace System.Data.Entity". После того, как на страницу импортируешь это пространство имен, начинает кричать, что он не знает, что такое System.Data.Objects. Как только модель была перенесена в проект MVC, подобные ошибки сразу же исчезли. Кто знает, как лечить по-другому, сообщайте.

воскресенье, 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");

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

воскресенье, 27 апреля 2008 г.

Разбираемся с ADO.NET Entity Framework

Итак, в этом проекте мы не будем использовать никаких посторонных систем ORM (про ORM здесь), а попробуем построить слой DAL на основе новой технологии ADO.NET Entity Framework, предложенной Microsoft. Говорят, что это не просто ORM, а нечто большее. Что ж, попробуем понять, что именно. Честно говоря, не совсем понимаю, почему вокруг этой ORM столько шума. Мне, например, очень нравится ORM LLBLGen, я думаю, что эта система ни в чем не уступает майкрософтовской, за исключением, разве что тем, что в ней нельзя использовать LINQ (хотя может быть, и можно).

Entity Data Model
В основе ADO.NET EF лежит модель сущностей, т.е. Entity Data Model. Как и предполагалось, основные объекты модели - это сущности и связи. Все объекты и их связи представлены в виде XML-схемы - в файле с расширением .edmx. Таблицы представлены объектами EntitySet, поля - свойствами Property (обычные поля) и NavigationProperty (поля, участвующие в ассоциациях - ключи, foreign keys).

Служебные объекты (Object Services)
В ADO.NET EF уществует несколько полезных объектов, с помощью которых можно получать доступ к соединению с БД или, например, к выборке из базы, представленной в виде типизированной коллекции.
ObjectContext позволяет получить доступ к соединению, метаданным и еще кое-каким службам.
ObjectQuery - это, по сути, запрос, который возвращает коллекцию типизированных сущностей.
ObjectStateManager кэширует запросы, а также отслеживает изменения в сущностях (с целью дальнейшей синхронизацией с БД).

Mapping
Это то, как полученные сущности связаны с реальными объектами базы данных.

Как все это выглядит
Добавим в наш солюшн еще один проект, который назовем DataCore типа ClassLibrary. Затем в проект добавим новый айтем типа ADO.NET Entity Data model. Сразу же на экране возникает визард, предлагающий нам выбрать, будет ли наша модель построена на основе базы данных, или она будет пустой. Конечно, выбираем Generate from database. Дальше все просто. Визард автоматически подключается к серверу, вернее, сразу к самой базе, после чего мы выбираем таблицы и другие объекты, которые хотим включить в модель. В нашем проекте мы включим следующие таблицы:

  • aspnet_Applications

  • aspnet_Membership

  • aspnet_Users

  • Category

  • Comment

  • Question

  • UserDetails


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


Если теперь выбрать файл DataModel.edmx в SolutionExplorer, а затем Open with... XML Editor, то мы увидим, как в действительности выглядит этот файл. Это обычный XML, точнее схема, где описаны все элементы: сущности идут здесь как EntitySet, связи между таблицами (ассоциации) как AssociationSet, а отдельные поля - это Properties.

Помимо этой схемы, студия генерирует еще три файла DataModel.csdl, DataModel.msl и DataModel.ssdl. Файл CSDL представляет собой концептуальную схему - это XML файл, в котором представлены объекты на уровне сущностей. Файл SSDL - схема объектов (таблиц и пр.), как они представлены в базе данных.Файл MSL - схема сопоставления - содержит данные о том, как связаны между собой концептуальная схема и схема объектов БД.

Теперь вернемся в Solution Explorer и рассмотрим файл DataModel.Designer.cs. Это самый важный для нас файл, потому что здесь сосредоточены все сгенерированные объекты. Если мы откроем наш первый проект (MVC), то в любом месте сможем, например, написать (не забудьте добавить ссылку на проект):

using (DataCoreConnection context = new DataCoreConnection())
{
Category cat = new FAQEngineModel.Category();
cat.Name = "Web";
context.AddObject("Category",cat);
context.SaveChanges();
}

Итак, что же делает этот код?
В первую очередь он создает объект контекста (DataCoreConnection), который будет уничтожен после выхода за пределы фигурных скобок. Этот объект идет первым в нашем файле DataModel.Designer.cs и является по сути связующим звеном между нашим кодом (объектами) и базой данных. С его помощью выполняются все стандартные операции с данными - выборка, вставка, обновление и удаление.

Затем мы создаем новый объект категории, присваиваем ей имя Web, добавляем объект в контекст и сохраняем изменения. Это, конечно, всего лишь пример кода, поскольку настоящий код должен обязательно содержать конструкцию try-catch.

Однако в этом коротком примере кроется одна проблема, которая, как я надеюсь, будет в дальнейшем решена разработчиками студии. Дело в том, что все проходит гладко, когда мы имеем один проект, из которого вызываем наш контекст. Но как только мы выделяем обработку с данными в отдельный проект, а вызываем код из другого проекта, начинаются проблемы. Во-первых, поскольку вызывающий проект (у нас это веб-проект) имеет свой файл конфигурации, в котором содержатся свои строки подключения к БД, он в упор не видит файла конфигурации App.config проекта DataCore, и следовательно, не может подключиться к базе. Как результат, мы видим на экране сообщение EntityConnection error: The specified named connection is either not found in the configuration. Это исправляется легко - достаточно скопировать строку подключения из файла App.config в файл Web.config нашего веб-проекта.

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

<add name="DataCoreConnection" connectionString="metadata=.\FAQEngineModel.csdl|.\FAQEngineModel.ssdl|.\FAQEngineModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=.\SQLEXPRESS;Initial Catalog=FAQEngine;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />


Обратите внимание, на какие файлы метаданных она ссылается. Все эти файлы лежат в каталоге bin/Debug проекта DataCore (.\) - т.е. в каталоге, куда помещается dll. Но поскольку мы вызываем эту dll из другого проекта, то получается, что мы ищем эти файлы там, где их нет, и получаем новую ошибку: "The specified metadata path is not valid. A valid path must be either an existing directory, an existing file with extension '.csdl', '.ssdl', or '.msl', or a URI that identifies an embedded resource.". Нам остается два выхода - либо исправить строку подключения ,либо каждый раз копировать нужные файлы в какой-либо каталог веб-проекта.

Попробуем сначала первый способ. Лучше всего, если бы можно было указать относительный путь, например, с использованием какого-нибудь макроса. Единственное, что мне удается найти - это DataDirectory. Но, поскольку мы вызываем файлы из веб-приложения, этот макрос будет заменен каталогом App_Data, который нам вовсе ни к чему. Поэтому можно просто указать абсолютный путь к файлам на диске. Но потом, после деплоймента, эту строку придется менять. Поэтому этот способ мне не нравится.

Второй способ - копирование файлов - тоже не слишком хорош. Получается, что каждый раз, как только я изменяю модель, мне надо куда-то еще копировать сгенерированные файлы? Получается, что так... ладно, пусть это делает за меня студия. Открываем свойства проекта DataCore, идем в Post-Build event и пишем следующее:


copy $(TargetDir)FAQEngineModel.csdl $(SolutionDir)FAQEngine\App_Data\
copy $(TargetDir)FAQEngineModel.msl $(SolutionDir)FAQEngine\App_Data\
copy $(TargetDir)FAQEngineModel.ssdl $(SolutionDir)FAQEngine\App_Data\


Здесь мы просто копируем нужные файлы в каталог App_Data нашего веб-приложения. Теперь остается чуть подправить строку подключения (в файле Web.config):

<add name="DataCoreConnection" connectionString="metadata=|DataDirectory|\FAQEngineModel.csdl||DataDirectory|\FAQEngineModel.ssdl||DataDirectory|\FAQEngineModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=.\SQLEXPRESS;Initial Catalog=FAQEngine;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />

Все работает!
Решение данной проблемы описано на форуме ADO.NET Entity Framework.


Ссылки по теме:
Обзор ADO.NET Entity Framework
Блог Sergey Rozovik - посты, посвященные EF
Работа с макросами в Pre- и Post-Build events

суббота, 12 апреля 2008 г.

Microsoft MVC Framework - роутинг

Правила
В основе роутинга в MS MVC Framework лежит следующий принцип - "мы не обращаемся к файлу, мы обращаемся к классу контроллера, и делаем это через паттерны". Что здесь имеется в виду? А то, что мы действительно не вызываем никакие файлы, т.е. мы пишем в адресной строке не mysite/products/details.aspx?productid=6, а вместо этого указываем "дружественный" URL - mysite/products/details/6. Второй момент - это, конечно, никакой не URL. Т.е. слэши здесь играют роль разделения не папок (как в настоящем URL), а частей (элементов) паттерна. Суть в том, что запрос на сервере разбирается, и каждая его часть (отделенная слэшем) интерпретируется так, как вы задали в паттерне.

Паттерн (или маршрут) это последовательность элементов-заполнителей (placeholders), в соответствие с которыми производится разбор строки запроса. Паттерн может содержать не только некоторые элементы-переменные, но и константные значения. Каждый элемент заключается в фигурные скобки { и }. Элементы отделяются друг от друга слэшем или точкой. Все, что не заключено в фигурные скобки, воспринимается как константа. Типичный пример паттерна:

{controller}/{action}/{id}

А вот строка, соответствующая этому паттерну:

Products/Details/55

Для того, чтобы приложение знало о том, как обрабатывать запрос, паттерн надо зарегистрировать. Лучше всего его регистрировать в событии Application_Start в файле Global.asax. Если вы хотите, чтобы регистрация паттерна была доступна и в проекте, предназначенном для тестирования, то метод регистрации должен быть статическим и принимать в качестве параметра RouteCollection.
Например, регистрация маршрута может выглядеть так:

protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route
(
"Category/{action}/{categoryName}"
, new CategoryRouteHandler()
));
}


Какие запросы могут подходить под этот паттерн? Например, localhost/Category/View/Yachts. А вот запрос localhost/Category/View не подходит, поскольку во-первых, в строке не хватает третьего элемента, а во-вторых, потому, что паттерн не предусматривает дефолтного значения для него (об этом чуть ниже).
Следует учесть, что в процессе регистрации все паттерны складываются в коллекцию, из которой извлекаются в порядке очереди. Это значит, что размещать паттерны надо по степени убывания специализации, как кэтчи в блоке try-catch, и дефолтный паттерн должен идти последним.

Теперь посмотрим, как задаются дефолтные значения для элемента (они подставляются в случае отсутствия элемента в строке запроса):


void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route
(
"Category/{action}/{categoryName}"
new CategoryRouteHandler()
)
{
Defaults = new RouteValueDictionary
{{"categoryName", "food"}, {"action", "view"}}
}
);
}


Как видим, все достаточно просто - если отсутствует имя категории, подставляем food, если нет action, подставляем view.

Если мы не знаем заранее точно, сколько элементов может содержать строка запроса, но хотим все же как-то обрабатывать запрос, то можно записать паттерн следующим образом:
"Category/{action}/{*titles}" . Звездочка указывает на то, что начиная с этого элемента число последующих элементов может быть любым. Но при этом все они будут трактоваться как третий элемент. Например, для строки запроса localhost/Category/View/Maxi/55 Category - это Category, action - это View, а titles - это Maxi/55. Что делать с таким параметром, мы уже решаем сами.

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

void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}

public static void RegisterRoutes(RouteCollection routes)
{
routes.Add(new Route
(
"{locale}/{year}"
, new ReportRouteHandler()
)
{
Constraints = new RouteValueDictionary
{{"locale", "{a-z}{2}-{A-Z}{2}"},{"year", @"\d{4}"}}
});
}

Мы видим, что элемент year должен содержать 4 цифры, только в этом случае он будет распознан.

Итак, главное, что надо уяснить с роутингом - это то, что строка запроса всегда разбивается ровно на три логические части - controller, action и id. Если вам нужно передать контроллеру (вернее, его методу) параметров больше одного, то вы просто удлиняете запрос, как будто добавляете еще одну папку, а уже в методе разбиваете параметр на нужное количество.

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


Кстати, если при попытке отладки через сервер у вас начнут вылезать странные ошибки вроде Failed to access IIS metabase или Unable to start debugging on the web server. The web server is not configured correctly., просто снесите asp.net 2 и установите его заново - aspnet_regiis.exe -u, потом aspnet_regiis.exe -i .


Да, речь идет только об IIS6. Потому что IIS7 уже умеет работать с приложениями, основанными на MVC Frm, и понимает запросы без расширений файлов, а шестой еще нет. Поэтому надо ему помочь. Для этого надо в код регистрации паттерна включить версию для IIS6. Но для начала в конфиге пропишем явно, на какой версии IIS запускаем наше приложение. Поскольку этот параметр может быть изменен (например, после деплоймента на рабочий сервер), заносим этот момент в наш список TODO. Вот примерный код:

<add key="IISVersion" value="6" />

Теперь в код Global.asax добавляем код, с помощью которого сначала производим выбор версии IIS:

protected void Application_Start(object sender, EventArgs e)
{
RegisterRoutes(RouteTable.Routes);
}
public static void RegisterRoutes(RouteCollection routes)
{
int iisVersion = Convert.ToInt32(ConfigurationManager.AppSettings["IISVersion"], System.Globalization.CultureInfo.InvariantCulture);

if (iisVersion >= 7)
{
RegisterRoutesForIIS6(routes);
}
else
{
RegisterRoutesForIIS7(routes);
}
}


Затем мы в соответствующих методах регистрируем наши паттерны. Код в методах отличается только наличием расширения .mvc для IIS6. Мне не очень нравится подобное дублирование, но ничего лучше мне придумать не удалось (если придумаете, напишите мне, а я расскажу здесь о вашей идее - с сохранением всех копирайтов, разумеется). Итак, получился примерно такой код:


private static void RegisterRoutesForIIS7(ICollection routes)
{
routes.Add(new Route("{controler}/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});
...
}
private static void RegisterRoutesForIIS6(ICollection routes)
{
routes.Add(new Route("{controler}.mvc/{action}/{id}", new MvcRouteHandler())
{
Defaults = new RouteValueDictionary(new { controller = "Home", action = "Index", id = "" }),
});
...
}

Неудобство использования IIS6 проявляется и в том, что нормальные адреса (как предполагалось, без расширений) все равно не получаются. Более того, скажу честно, мне так и не удалось заставить всю эту кухню работать под IIS6. На встроенном в студию сервере - пожалуйста, а под IIS6 - ни в какую. Однако не будем терять надежды. Во-первых, может быть, это всего лишь баг, и его в релизе исправят, а во-вторых, может, к тому времени на серверах отомрут IISы-6...

Хозяйке на заметку

С контроллерами мы будем разбираться позже, сейчас же буквально два слова о том, каким образом MVC Frm узнает о том, что и кому передавать. В основе всего лежит объект System.Web.Routing.UrlRoutingModule, который реализует ничто иное, как интерфейс IHttpModule. Http-модуль характеризуется тем, что вставивается в конвейер обработки запросов. Данный модуль перехватывает запросы и интерпретирует их по-своему. Этот модуль, как полагается, зарегистрирован в файле конфига:

<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<httpModules>


А Http-хандлером является MvcHandler, он решает, какой контроллер вызывать и, соответственно, какой метод. Надо знать, что если в строке запроса указан контроллер, например, Products, то реально сам контроллер (это вообще класс) должен называться ProductController. Имя метода же должно точно соответствовать элементу action запроса.

Полезные ссылки:
Kigg - первый стартер-кит, построенный на MS MVC Framework (клон Digg'a)
Еще кое-что о роутинге

Куда дальше?
В следующей части я рассмотрю архитектуру приложения.

пятница, 11 апреля 2008 г.

Microsoft MVC Framework - первый взгляд

Итак, что же это такое - Microsoft MVC Framework ?
Многие (а может быть и все) из нас знают, что совет "разделяй и властвуй" как нельзя лучше подходит к программированию. Веб-программирование проделало долгий путь, постепенно приближаясь к воплощению данного совета в жизнь. Вспомним классический ASP, в котором текст страницы перемежался с кодом, или файлы Perl, в которых, наоборот, код перемежается раздражающими вставками разметки. С появлением ASP.NET и модели "code-behind" кажется, мы приблизились к заветному разделению - код в одном файле, разметка в другом. И все же нет в мире совершенства... лично меня периодически раздражал тот факт, что в коде, обрабатывающем какой-нибудь Postback, надо обращаться к базе (или к слою DAL), и тут же - к элементам интерфейса, и все перемешивается в одну кучу...

Кажется, MVC призвана решить именно эту проблему. Во-первых, что такое MVC? Это такой принцип разработки (он же паттерн), при котором все разделено: интерфейс (View), управление (Controller) и внутренняя логика и данные (Model). Подробнее описано в википедии. Там же, кстати, есть ссылки на более подробные статьи на эту тему.

Microsoft MVC Framework решает данную задачу по-своему, и сейчас мы попробуем разобраться, как именно. В контексте этого фреймворка Model - это некий набор классов, позволяющих манипулировать данными и логикой. Например, это могут быть классы, представляющие некоторые сущности (entities). View - это, естественно, интерфейс - т.е. файл страницы с разметкой. Наконец, Controller - это классы, с помощью которых осуществляется взаимодействие с пользователем, обработка пользовательских данных. Пока не очень понятно, что за всем этим стоит (кроме представления).

Обработка урлов в MVC идет по-особенному, т.е. сервер обращается не к конкретной странице, как это было в ASP.NET, а к классу контроллера, и вот этот-то контроллер и обрабатывает запрос пользователя, а также пользовательский ввод. Кроме того, он, с одной стороны, обращается к модели (т.е. использует логику и данные), с другой стороны, вызывает уже определенный класс представления, т.е., попросту говоря, файл .aspx. С этими процессами связан один интереснейший момент, а именно routing, позволяющий в адресной строке браузера указывать не имена файлов, а путь к некоторому вложенному каталогу (на самом деле это не каталоги, но об этом позже). Т.е. вместо Products/Product.aspx?id=5 мы указываем Products/Details/5. Очень удобно и красиво, а самое главное, просто! Если кто-то пытался сделать подобное в классическом ASP.NET, он сталкивался с невероятными трудностями, о преодолении которых можно почитать в MSDN (URL Rewriting in ASP.NET). Организацию роутинга мы подробно разберем позже, а сейчас продолжим исследование MVC.

Еще одна очень любопытная деталь - в MVC полностью отказались от постбэков. Это означает две вещи: во-первых, поскольку каждый раз обращение идет к определенному классу контрола, он вызывает конкретную страницу, и следовательно, эта страница загружается всегда заново; а во-вторых, как следствие, здесь не существует такого понятия, как жизненный цикл страницы, вернее, он существует, но нам совершенно не интересен - мы не можем использовать Viewstate и события, связанные с жизненным циклом...


Полезные ссылки:
дневник ScottGu по теме
Создание веб-приложений без форм
ASP.NET Model View Controller Applications

четверг, 10 апреля 2008 г.

MVC, Entity Framework и Membership provider. Подготовка.

Чтобы в дальнейшем работать с пользователями - вход, регистрация, выход, администрирование и т.д. - необходимо использовать какой-то интерфейс. Самым удобным мне кажется Membership provider. Но чтобы заставить работать и его, надо проделать некоторую предварительную работу, о которой я и расскажу.
Всякие установки
Начнем с того, что установим VS 2008, который достался совершенно бесплатно на конференции разработчиков. В пакет также входит SQL Server 2005, а SQL Server Management Studio Express можно также скачать бесплатно.
Теперь надо установить необходимые дополнения - MVC Framework (здесь) и ADO.NET Entity Framework (здесь). После установки при создании нового приложения выбираем Web -> ASP.NET MVC Web Application. Все, студия создала для нас заготовку приложения!
Двигаемся дальше.

База данных
Создаем в студии SQL Server новую базу. После этого в Web.config нашего приложения добавляем строку подсоединения к базе, например:

<add name="Main.ConnectionString" connectionString="Data Source=5D734033\SQLEXPRESS;Initial Catalog=DB;Integrated Security=True;User Instance=True" providerName="System.Data.SqlClient" />

Строку эту, естественно, добавляем в раздел connectionStrings.

Membership
Теперь добавляем настройки для нашего провайдера. Например, такие:

<membership defaultProvider="FProvider" userIsOnlineTimeWindow="1">
<providers>
<clear />
<add name="FProvider"
applicationName="FEngine"
connectionStringName="Main.ConnectionString"
type="System.Web.Security.SqlMembershipProvider"
enablePasswordRetrieval="false"
enablePasswordReset="true"
minRequiredPasswordLength="6"
minRequiredNonalphanumericCharacters="0"
passwordFormat="Hashed"
requiresQuestionAndAnswer="false"
requiresUniqueEmail="true" />
</providers>
</membership>

Далее создаем настройки для провайдера ролей. Например, так:

<roleManager enabled="true"
defaultProvider="rprovider"
cookieName="RC"
cookieTimeout="30"
cookieSlidingExpiration="true"
cookieProtection="All"
cacheRolesInCookie="false"
>
<providers>
<add name="rprovider"
type="System.Web.Security.SqlRoleProvider"
applicationName="FEngine"
connectionStringName="Main.ConnectionString"/>
</providers>
</roleManager>
Все эти настройки нужны для того, чтобы Membership API знал, откуда брать все данные. Если этого не сделать, то он создаст свою собственную базу данных, но нам ведь не это надо, верно?

Заодно не забываем установить authentication mode равным Forms.
Теперь выполним важный шаг, о котором я практически всегда забываю. Надо сгенерировать все необходимые таблицы - для этого находим утилиту aspnet_reqsql.exe и запускаем ее. Эта утилита лежит всегда в одном месте - C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727 .
В результате откроется вполне нормальное окошко Windows, где мы сначала нажимаем Next, потом выбираем Configure SQL Server for application services, снова нажимаем Next. Здесь нас просят выбрать сервер базы, тип аутентификации (обычно Windows, особенно на локальном компьютере), имя базы. Сервер у меня определился неправильно, поэтому сначала выскочило сообщение об ошибке. После введение в поля правильного имени в выпадающем списке появились все базы. Выбираем базу, жмем Next, потом опять Next, потом Finish.

Проверим, все ли получилось. Зайдем на сервер, выберем нашу базу данных и посмотрим ее содержимое. Если все в порядке, то в базе должна появиться куча новых таблиц, названия которых начинаются с aspnet_, например, aspnet_Roles.
Вот теперь можно проверить, работает ли наш мембершип провайдер. Для этого возвращаемся в студию, компилируем проект, затем выбираем Project ->ASP.NET Configuration. В результате студия сгенерирует и запустит специальный проект, с помощью которого можно делать некоторые административные вещи, например, устанавливать роли, создавать пользователей. Не думаю, что кто-нибудь пользуется им для реальной работы, но для проверки и установки начальных параметров вполне сойдет.
Итак, после некоторой паузы наконец открывается окно браузера с этим проектом. Внешне все выглядит нормально, но зайдем на вкладку Security. Так и есть! Сообщение об ошибке:

There is a problem with your selected data store. This can be caused by an invalid server name or credentials, or by insufficient permission. It can also be caused by the role manager feature not being enabled. Click the button below to be redirected to a page where you can choose a new data store.

The following message may help in diagnosing the problem: Cannot open database "FEngine" requested by the login. The login failed. Login failed for user 'S-5D7E4033\S'.

Итак, делаю две вещи:
Во-первых, изменяю в свойствах сервера тип аутентификации с Windows на смешанный.
Во-вторых, убираю из строки соединения в файле web.config параметр User Instance=True. Нам это совсем не нужно (насколько я знаю, этот параметр позволяет запускать локальную версию сервера).

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

Идем на вкладку Provider и щелкаем ссылку Select a different provider for each feature (advanced). В результате оказываемся на странице, которая показывает нам отмеченными оба провайдера. Значит, все в порядке.

Идем на вкладку Sequrity и создаем необходимые роли. Я пока вижу только две роли: admin и member. Создаем обе роли, затем пару пользователей - один пусть будет админ, другой - member, т.е. зарегистрированный пользователь.
На этом подготовительную работу можно считать сделанной.

О чем этот блог

Этот блог посвящен программированию, точнее, одному проекту, который я в данное время и разрабатываю. Проект этот делается на основе достаточно новых технологий - таких, как Microsoft MVC Framework и ADO.NET Entity Framework. По мере разработки я буду писать в блог - описывать очередной этап и проблемы, с ним связанные.
Надеюсь, он (т.е. блог) поможет вам не наступать на те же самые грабли.
Поехали...