Здравствуйте, уважаемые читатели!
Сегодня мы продолжим изучать ASP MVC и наконец-то напишем свой первый код в реализации такого нелегкого проекта. Всех заинтересовавшихся и всех ожидавших вторую часть прошу под кат.
Навигация
- Изучаем ASP .NET MVC: пишем свой Хабрахабр с инвайтами и кармой. Часть 1
- Изучаем ASP .NET MVC: пишем свой Хабрахабр с инвайтами и кармой. Часть 2
Планируем
Первое, что мы реализуем – это пользователи. Так как весь остальной функционал, так или иначе, завязан на пользователях. Определим, что у нас будет у пользователей:
- Профиль пользователя. Здесь изобретать ничего не будем и скопируем все основные поля с хабра.
- Личные сообщения. Лично мне не удобна реализация сообщений в хабре, будем делать в виде диалогов как в ВК.
- Избранное. Возможность просматривать добавленные в избранное топики/комментарии/вопросы. Функционал для добавления в избранное будет реализовываться непосредственно при написании кода для топиков и т.д.
- Возможность зафрендить кого либо, а также подтвердить или отвергнуть запрос на дружбу
Приступаем
В общем виде спланировали. Теперь приступим. Создадим ASP .NET MVC 3 Web Application, выберем Internet Application, а также поставим галочку для создания юнит тестов. VS создаст нам стандартный работающий проект с дефолтными настройками. Что в него включено:
- Два контроллера: один контроллер для отображения приветственной страницы, второй для авторизации/регистрации пользователей.
- Отображения для каждого действия контроллеров с общим шаблоном.
- Подключен и настроен membership.
Теперь обо всем поподробнее.
Membership
Что есть мембершип? Это механизм для работы с пользователями, предоставляющий удобный интерфейс. Как это выглядит на практике:
- Надо узнать, авторизован ли пользователь? Пожалуйста, Request.IsAuthenticate вернёт булевое значение.
- Узнать, состоит ли пользователь в определенной привилегированной группе, например “administrators”? Легко, User.IsInRole(«administrators») (или как вариант Roles.IsUserInRole(«administrators»))
- Или добавить пользователя в административную группу: Roles.AddUserToRole(«user», «administrators»);
Это поверхностный функционал для примера. А где же все это храниться? При первой инициализации создается БД ASPNETDB.MDF в каталоге App_Data. Изначально этот файл не включен в проект, для отображения не включенных в проект файлов в VS надо нажать эту кнопку:
Но перед этим надо инициализировать БД. Как это сделать. Для этого достаточно открыть конфигуратор веб-приложения:
И на вкладке «Безопасность» включить «Роли». Также здесь можно создать/отредактировать пользователей, правила доступа и некоторые другие параметры приложения. После этого будет создана БД с нужными таблицами и связами. Рекомендую открыть и посмотреть на её структуру. А откуда берутся параметры для membership’a? Вся конфигурация описана в файле Web.config проекта, который мы откроем и посмотрим (там же хранятся параметры для всего приложения, а не только для membership’a, остальные параметры будем рассматривать в процессе изучения остального материала):
Как видим, это файл в формате XML. Что нам будет интересно сейчас:
- connectionStrings – здесь задаем параметры для подключения к БД. У нас создана одна строка для подключения с именем «ApplicationServices», в ней указан физический путь к файлу БД, а так же драйвер, используемый для подключения.
- authentication – задаем какой режим аутентификации использовать. Также здесь задана страница для входа. Для чего она используется: например на страничку, расположенную по адресу localhost/profile/edit могут зайти только авторизованные пользователи. При попытке зайти неавторизованного пользователя его перебросит на страницу, указанную в эти настройках. В дополнительных настройках можно задать время жизни кукисов и требование ssl. Полный список параметров можно посмотреть здесь: страница в msdn
- membership – секция для непосредственной настройки membership’a. Имеет много настроек, детально о которых можно прочитать на msdn’e . Настройка минимальной длины пароля, секретного вопроса, количества неудачных попыток входа настраиваться именно здесь.
Controllers
Контроллеры в ASP. MVC располагаться в папке Controllers (Как очевидно, да ?). Имена файлов контроллеров имеют вид ИмяController.cs. Сейчас у нас имеется два контроллера:
- AccountController.cs
- HomeController.cs
Откроем файл HomeController.cs и посмотрим на его структуру. Внутри мы найдем класс контроллера, имеющий имя в таком же формате, что и имя файла, и он должен быть обязательно унаследован от базового класса Controller. Методы этого класса являются действиями, которые вызывает роутер. Например: при запросе адреса localhost/Home/Test роутер запустить контроллер с именем HomeController и вызовет у него метод Test. Если действие не указано, у контроллера вызывается метод Index
Пару слов о том, что возвращают действия контроллеров. Лично мне такая реализация нравиться намного больше, чем в тех же php фреймворках. Возвращаемый тип: ActionResult, является базовым классом для следующих типов классов:
- RedirectResult – предоставляет перенаправление пользователя. В параметрах принимает либо внешний URL, либо имя контроллера и действия для перенаправления
- JsonResult – возвращает ответ в формате JSON
- ViewResult – возвращает результат в виде отображения, используется наиболее часто.
Полный список и описание доступно здесь . Небольшой кусок кода для того, чтобы показать, как это используется:
public class HomeController : Controller
{
public ActionResult ViewRes()
{
return View("ViewName");
}
public ActionResult RedirectRes()
{
return Redirect("http://google.com");
}
public ActionResult JsonRes()
{
return new JsonResult();
}
}
Ну и наконец-то создадим свой первый контроллер. Назовем его UserController. VS создаст файл с нужным классом и со следующим кодом:
public class UserController : Controller
{
public ActionResult Index()
{
return View();
}
}
Сейчас View будет отображаться красным, т.к. для данного контроллера и действия не найден вид. Это мы исправим чуть позже. Наш контроллер и действие Index сработает, если перейти по адресу localhost/user/index/. На данной странице нам нужно отобразить информацию отдельного пользователя. Для этого еще дополнительным параметром в строке запроса будем передавать его ID: localhost/user/index/31337/. Теперь нам надо принять идентификатор пользователя в нашем действии. Для этого модифицируем его:
public ActionResult Index(long? id)
{
return View();
}
Теперь в переменной id будет находиться значение «31337». Замечу, что здесь строго указан тип передаваемого значения. Если мы попробуем передать, например, строку, то в id передастся null. Как вы уже заметили, тип идентификатора я задал long?, т.е. он может помимо числа принять еще значение null. Сейчас поясню, для чего это сделано. Если задать тип long id, то при переходе по адресам вида: localhost/user/index/ и localhost/user/index/313string37/ мы получим исключение что параметр id не может принимать значения null, т.к. при несовпадении формата запроса с типом параметра (задан long, а пытаемся передать string) в этот параметр передается null. Поэтому я использовал такой тип. Добавим дополнительный код для проверки:
public ActionResult Index(long? id)
{
if(id == null)
throw new HttpException(404, "User not found!");
return View();
}
Теперь у нас будет выбрасываться удобное исключение, которое можно будет правильно обработать и показать пользователю 404 страницу.
Добавим представление для нашего действия. Для этого достаточно щелкнуть ПКМ по View() и выбрать в меню Add View. VS покажет диалоговое окно добавления представления:
После нажатия “OK” будет создан файл представления, имеющий путь: Views/User/Index.cshtml. Как можно заметить, путь строиться из названия контроллера и действия контроллера.
Давайте теперь выведем в представлении идентификатор пользователя, профиль которого мы просматриваем. Для этого, добавим в представление следующую строку @ViewData[«userid»] и получим в итоге:
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Index</title>
</head>
<body>
<div>
Hi, user id is @ViewData["userid"]
</div>
</body>
</html>
ViewData – это ассоциативный массив, который служит для передачи данных из контроллера в представление. Символ “@” служит указанием движку представления, что дальше идет некая конструкция из C#. Более детально это можно посмотреть здесь, там же показано много интересных примеров.
Теперь модифицируем наш контроллер для передачи данных. Добавим следующий код:
public ActionResult Index(long? id)
{
if(id == null)
throw new HttpException(404, "User not found!");
ViewData["userid"] = id;
return View();
}
Запустим и посмотрим, что у нас получилось:
Работает.
Небольшой хинт: если что то не работает в самом неожиданном месте – пересоберите проект по сочетанию Ctrl+Shift+B. Часто, из-за того, что не пересобрался какой-то компонент приходиться долго искать, откуда лезут очень интересные баги
Models
Создадим в нашем проекте новую БД, назовем её habrahabr и поместим в папку App_Data. Создадим первую таблицу profiles для хранения пользовательских профилей, добавим в неё следующие поля:
- UserId – тип uniqueidentifier . Этот тип является GUID’ом и используется для идентификации пользователей в membership’е, и вот именно к пользователям из membership’a мы будем привязывать. Сделаем его первичным ключом и уникальным полем.
- login — поле, в которое при создании пользователя продублируется логин из membersip’a, что бы нам каждый раз не лазить в другую бд за ним.
- firstname, lastname, about, skype – не нуждаются в пояснении. Вообще идеально было бы использовать для контактов
- karma, rating – тип int, у нас будет карма и рейтинг без дробных частей, не люблю я их.
Создадим таблицу messages.
Столбец id является автоинкрементным первичным ключом.
И добавим новую схему:
Добавим к проекту ADO .NET EDM, в каталог Models. Нам будет предложено создать модель из существующей БД или пустую модель. Выбираем из существующей БД:
Выбираем нашу БД habarahabr.mdf:
Указываем для каких таблиц создать классы моделей:
В результате будут созданы классы сущностей:
Как видим, внешним связям даны не совсем правильные имена. Поправим их названия, что бы в дальнейшем при работе не путаться самим. Какие имена необходимо дать: у каждого пользователя может быть список входящих и исходящих сообщений; у каждого сообщения есть две ссылки на профили: на профиль отправителя и профиль получателя. В результате получим:
Пишем код
«Ну, наконец-то» — воскликнут самые нетерпеливые. Начнем с реализации контроллера для регистрации/авторизации пользователей. Инвайты и разделение прав доступа прикрутим чуть позже.
Создаем новый пустой контроллер «Auth». Добавим новое действие «Logon». В действии «Index» поставим редирект на действие «Logon», что бы при переходе по ссылке localhost/Auth/ нас перенаправляло на форму авторизации по адресу localhost/Auth/Logon. Саму форму создадим чуть позже. Сейчас добавим еще одно действие «Register». Данное действие будет отображать форму для регистрации. Для защиты от самых любопытных пользователей добавим проверку: если пользователь авторизован, перенаправляем его на главную страницу.
public class AuthController : Controller
{
public ActionResult Index()
{
return RedirectToAction("Logon", "Auth");
}
public ActionResult Logon()
{
return View();
}
public ActionResult Register()
{
if (Request.IsAuthenticated)
return RedirectToAction("Index", "Home"); // Пусть это будет пока такой маршрут
return View();
}
}
Перед добавлением отображения для регистрации создадим модель. В каталоге Models создадим новый класс RegisterUserModel. Какие поля нам нужны будут на данном этапе от пользователя: имя пользователя, e-mail и пароль. Добавим в наш класс четыре этих свойства:
public class RegisterUserModel
{
public string Login { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
}
Теперь создадим отображение для нашего действия. При добавлении отображения укажем класс модели, для которой будет создано отображение:
Будет создано стандартное отображение. Добавим в него следующий код:
@model Habrahabr.Models.RegisterUserModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Register</title>
</head>
<body>
<div>
@using(Html.BeginForm())
{
<table>
<tr>
<td>@Html.LabelFor( m => m.Login )</td>
<td>@Html.EditorFor( m => m.Login )</td>
<td>@Html.ValidationMessageFor( m => m.Login )</td>
</tr>
<tr>
<td>@Html.LabelFor( m => m.Email )</td>
<td>@Html.EditorFor( m => m.Email)</td>
<td>@Html.ValidationMessageFor( m => m.Email)</td>
</tr>
</table>
<button type="submit">Register</button>
}
</div>
</body>
</html>
Рассмотрим, что мы здесь создали. Класс Html является html-хелпером, т.е. с помощью его методов можно создавать определенные html элементы на странице. Первый хелпер Html.BeginForm создает элемент . Он заключен в using для того, чтобы по окончанию создался закрывающий тег . Без использованию using данный код можно переписать иначе: @Html.BeginForm() @{ Html.EndForm(); }, результат будет одинаковый. Следующие хелперы создают поля, связанные с полями модели, указанной в заголовке отображения. Запустим браузер и посмотрим, что у нас получилось:
Как видим, создалось два текстовых блока и два блока для редактирования. Если мы сейчас нажмем кнопку «Register», все данные без проверки отправятся на сервер (сейчас они там никак не обработаются, но нам этого пока не надо). Хотелось бы добавить красивую валидацию с удобным взаимодействием из модели (а это именно то, из за чего я и полюбил ASP MVC). Откроем файл с классом нашей модели и добавим следующий код:
public class RegisterUserModel
{
[Required( ErrorMessage = "Обязательное поле!")]
[Display( Name = "Ваш логин")]
public string Login { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
}
Также в отображение, в секцию head добавим следующие строки, подключающие нужные JS библиотеки:
<head>
<title>Register</title>
<script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
</head>
Пересоберем проект и опять откроем страничку регистрации. Первое, что бросается в глаза, отображается новое название у поля «Логин». Но это не все, если нажать кнопку «Register», произойдет чудо клиентской валидации:
Разберем подробнее, как такое произошло. В модели к свойству Login мы указали дополнительные атрибуты, а именно: Required — что свойство обязательно для заполнения, Display — задает параметры отображения свойства. При рендере отображения, при использовании HTML-хелперов для генерации форм, фреймворк генерирует JS код для валидации полей формы в соответствии с заданными атрибутами для свойств. Посмотрим какие еще бывают атрибуты, добавив их к нашей модели:
public class RegisterUserModel
{
[Required( ErrorMessage = "Обязательное поле!")]
[Display( Name = "Ваш логин" )]
[StringLength(256, ErrorMessage = "Длина логина должна быть более {2} и менее {1} символов!",MinimumLength = 5)]
public string Login { get; set; }
[Required(ErrorMessage = "Обязательное поле!")]
[Display(Name = "E-mail")]
[RegularExpression(@"w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*", ErrorMessage = "Неправильный email адрес!")]
[DataType(DataType.EmailAddress)]
public string Email { get; set; }
[Required(ErrorMessage = "Обязательное поле!")]
[Display(Name = "Пароль")]
[DataType(DataType.Password)]
[StringLength(100, ErrorMessage = "Пароль должен быть более {2} и менее {1} символов!", MinimumLength = 6)]
public string Password { get; set; }
[Display(Name = "Подтвердите пароль")]
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = "Пароли не совпадают!")]
public string ConfirmPassword { get; set; }
}
И добавим недостающие поля в отображение:
<body>
<div>
@using(Html.BeginForm())
{
<h4 style="color:red;">@Html.ValidationSummary(true)</h4>
<table>
<tr>
<td>@Html.LabelFor( m => m.Login )</td>
<td>@Html.EditorFor( m => m.Login )</td>
<td>@Html.ValidationMessageFor( m => m.Login )</td>
</tr>
<tr>
<td>@Html.LabelFor( m => m.Email )</td>
<td>@Html.EditorFor( m => m.Email)</td>
<td>@Html.ValidationMessageFor( m => m.Email)</td>
</tr>
<tr>
<td>@Html.LabelFor( m => m.Password )</td>
<td>@Html.EditorFor(m => m.Password)</td>
<td>@Html.ValidationMessageFor(m => m.Password)</td>
</tr>
<tr>
<td>@Html.LabelFor( m => m.ConfirmPassword )</td>
<td>@Html.EditorFor(m => m.ConfirmPassword)</td>
<td>@Html.ValidationMessageFor(m => m.ConfirmPassword)</td>
</tr>
</table>
<button type="submit">Register</button>
}
</div>
</body>
Хелпер Html.ValidationSummary используется для отображения ошибок, переданных непосредственно из контроллера. О его использовании чуть ниже. Посмотрим, что у нас получилось:
Отображение для регистрации пользователя готово. Теперь добавим логику регистрации в контроллер:
private habrahabrEntities model = new habrahabrEntities();
public ActionResult Register()
{
if (Request.IsAuthenticated)
return RedirectToAction("Index", "Home"); // Пусть это будет пока такой маршрут
return View();
}
[HttpPost]
public ActionResult Register(RegisterUserModel user)
{
if (ModelState.IsValid) // Проверяем модель на валидность
{
// Статус регистрации
MembershipCreateStatus createStatus;
//Создаем пользователя
var newUser = Membership.CreateUser(user.Login, user.Password, user.Email, null, null, true, null, out createStatus);
// Проверяем результат регистрации
if (createStatus == MembershipCreateStatus.Success)
{
// Если успешно, авторизуем его
FormsAuthentication.SetAuthCookie(user.Login, false);
// И создаем нашему пользователю пустой профиль в БД
model.profiles.AddObject(
new profiles()
{
userId = (Guid)newUser.ProviderUserKey,
login = user.Login,
firstName = "",
lastName = "",
about = "",
skype = ""
});
//Фиксируем изменения в БД
model.SaveChanges();
// Если все прошло успешно, перенаправляем его на редактирование профиля
return RedirectToAction("Edit", "User");
}
else
{
// Если результать регистрации отрицателен, отправляем пользователю сообщение об ошибке
ModelState.AddModelError("", "При регистрации возникла ошибка, попробуйте позже!");
}
}
// Передаем в отображение нашего пользователя, иначе поля окажутся пустыми
return View(user);
}
Рассмотрим этот код. Первое: у нас теперь два действия с одинаковым именем. Но перед вторым мы можем заметить указанный атрибут HttpPost. Этот атрибут указывает, что действие будет реагировать только на POST запросы. Как работает: когда мы открываем localhost/Auth/Register срабатывает первое действие, отображающее нам форму регистрации. После заполнения формы и нажатия кнопки регистрации на сервер отправиться POST запрос, содержащий в себе данные формы. Вот сейчас и сработает второе действие. При этом из данных POST запроса будет создан объект RegisterUserModel и передан в метод. Далее в методе переданная модель еще раз проверяется (т.к. пользовательскую валидацию легко обойти) и с помощью membership’а создаем нового пользователя. В случае удачного создания пользователя, создаем ему профиль в таблице и перенаправляем на редактирование профиля. В случае неудачи создаем ошибку и показываем пользователю. Пару слов о ModelState.AddModelError. Данный метод позволяет добавить ошибку к модели и отправить её пользователю. В отображении хелпер Html.ValidationSummary отвечает за вывод этих сообщений. Вот так это будет выглядеть:
Также в ModelState.AddModelError в качестве первого параметра передать имя свойства, к которому относиться ошибка, например:
ModelState.AddModelError("Login", "Это ошибка для поля Login!");
Получит такой результат:
Немного расширим количество отображаемых пользователю сообщений об ошибках:
else
{
switch (createStatus)
{
case MembershipCreateStatus.InvalidEmail:
ModelState.AddModelError("Email", "Неправильный email адрес!");
break;
case MembershipCreateStatus.DuplicateUserName:
ModelState.AddModelError("Login", "Данный логин занят!");
break;
case MembershipCreateStatus.DuplicateEmail:
ModelState.AddModelError("Email", "Пользователь с таким email уже зарегестрирован!");
break;
default:
ModelState.AddModelError("", "При регистрации возникла ошибка, попробуйте позже!");
break;
}
}
Замечу, что это не полный список возможных результатов. Посмотрим, как теперь будет работать:
Безопасность.
Возможно, вы заметили что отсутствует код для проверки вводимых пользовательский данных на зловредные. Как этот механизм работает в ASP MVC: попробуем зарегистрировать пользователя с ником <script></script> и увидим следующее:
Как видим из описания, по умолчанию все входные данные проверяются. Также не забываем о том, что в модели можно строго задать разрешенные символы (например, a-Z, 0-9) и о том, в отображении при выводе всех пользовательских данный они автоматически экранируются. Тем самым количество «детских» ошибок в плане безопасности, которые можно допустить, значительно снижено.
Теперь реализуем возможность логиниться. Создадим новую модель LogonUserModel:
public class LogonUserModel
{
[Required(ErrorMessage = "Обязательное поле!")]
[Display(Name = "Логин")]
[StringLength(256, ErrorMessage = "Длина логина должна быть более {2} и менее {1} символов!", MinimumLength = 5)]
public string Login { get; set; }
[Required(ErrorMessage = "Обязательное поле!")]
[Display(Name = "Пароль")]
[DataType(DataType.Password)]
[StringLength(100, ErrorMessage = "Пароль должен быть более {2} и менее {1} символов!", MinimumLength = 6)]
public string Password { get; set; }
[Display(Name = "Запомнить?")]
public bool Remember { get; set; }
}
Добавим логику в контроллер:
public ActionResult Logon()
{
// Если пользователь уже авторизован перенаправляем его
if(Request.IsAuthenticated)
RedirectToAction("Index", "Home");
return View();
}
[HttpPost]
public ActionResult Logon(LogonUserModel user)
{
if(ModelState.IsValid)
{
// Проверяем логин и пароль пользователя
if(Membership.ValidateUser(user.Login, user.Password))
{
// Если все верно, ставим ему куки
FormsAuthentication.SetAuthCookie(
user.Login,
user.Remember
);
return RedirectToAction("Index", "Home");
}
else
{
// Иначе показываем ошибку
ModelState.AddModelError("", "Имя пользователя или пароль не верны!");
}
}
return View(user);
}
И создадим отображение для данного действия:
@model Habrahabr.Models.LogonUserModel
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<title>Logon</title>
</head>
<body>
<div>
@using(Html.BeginForm())
{
<h4 style="color:red;">@Html.ValidationSummary(true)</h4>
<table>
<tr>
<td>@Html.LabelFor( m => m.Login )</td>
<td>@Html.EditorFor( m => m.Login )</td>
<td>@Html.ValidationMessageFor( m => m.Login )</td>
</tr>
<tr>
<td>@Html.LabelFor( m => m.Password )</td>
<td>@Html.EditorFor(m => m.Password)</td>
<td>@Html.ValidationMessageFor(m => m.Password)</td>
</tr>
<tr>
<td>@Html.LabelFor( m => m.Remember )</td>
<td>@Html.EditorFor(m => m.Remember)</td>
</tr>
</table>
<button type="submit">Login</button>
}
</div>
</body>
</html>
И посмотрим на результат:
Итог
Пока что мы реализовали только самую малую часть от задуманного, в следующих заметках теории уже будет уделяться меньше внимания, будем больше кодировать. В 3 части более подробно рассмотрим работу с отображениями и их структуру, приведем уже созданные в нормальный вид (для этого я планирую использовать Twitter Bootstrap, предложения по поводу других css фреймворков приму на рассмотрение) и реализовать весь функционал для работы с пользователями.
Автор: vyacheslav_ka
спасибо! Это реально очень полезно! Жду следующих уроков с нетерпением!!
прикольная статейка, использовал исходники, спасибо!