Введение
Наконец, переходим к одному из самых важных уроков, в котором будет рассказано про создание записей. Любое действие на сайте, от сложных, когда мы заполняем регистрационную анкету, до простых, когда ставим лайк, – происходит следующим образом:
- Post\get запрос на сайт
- Авторизация и аутентификация
- Проверка введенных данных (валидация) на правильность
- Если проверка введенных данных показала, что введенные данные неверны, то в заполняемую форму выводится предупреждение.
- Если проверка введенных данных показала, что эти данные верны, то они сохраняются в БД и выводится страница с подтверждением.
Регистрация
Сделаем форму для регистрации пользователя. При регистрации, пользователь должен распознать капчу и повторить ввод пароля. Но начнем без этого. Создадим метод Register в контроллере UserController и View.
public ActionResult Register() { var newUser = new User(); return View(newUser); }
Создаем и передаем во View новый объект User. Так как полей у нас пока только два, для заполнения создаем View:
@using (Html.BeginForm("Register", "User", FormMethod.Post, new { @class = "form-horizontal" })) { <fieldset> <div class="control-group"> <label class="control-label" for="Email"> Email </label> <div class="controls"> @Html.ValidationMessage("Email") @Html.TextBox("Email", Model.Email) </div> </div> <div class="control-group"> <label class="control-label" for="FirstName"> Password </label> <div class="controls"> @Html.ValidationMessage("Password") @Html.Password("Password", Model.Password) </div> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary"> Register </button> @Html.ActionLink("Cancel", "Index", null, null, new { @class = "btn" }) </div> </fieldset> }
Все эти дивы, fieldset’ы и button’ы сделаны по подобию, как это описано в фреймворке bootstrap (далее будем изучать).
Изучим основные Html-вставки:
Html.BeginForm("Register", "User", FormMethod.Post, new { @class = "form-horizontal" })
— формирует тег <form action=”/User/Register” method=”post” class=”form-horizontal”>
и закрывает его после вызова Dispose()
(закрытие кавычек using() {}
)
@Html.TextBox("Email", Model.Email)
— формирует тег <input type=”text” name=”Email” value=”@Model.Email”>
(т.е. в значение тега записывается значение Email переданного объекта)
@Html.ValidationMessage("Password")
— выводит тег ошибки если такая есть
@Html.Password("Password", Model.Password)
— выводит тег <input type=”password” name=”Password” value=”@Model.Password”>
После нажатия на кнопку Register идет Http-запрос типа POST (так как FormMethod.Post
и передает данные Email=&Password=.
Создадим метод Register, принимающий в качестве параметра тип User, и пометим его атрибутом HttpPost, а предыдущий — атрибутом HttpGet. Контроллер различает, какой из типов запроса сейчас происходит и перенаправляет на тот, который необходим:
[HttpGet] public ActionResult Register() { var newUser = new User(); return View(newUser); } [HttpPost] public ActionResult Register(User user) { return View(user); }
Сделаем точку останова на втором методе Register и проверим, какой объект приходит к нам:
Видим, что поля Email и Password заполнены, остальные остались нулевыми или по умолчанию (default).
Так как мы должны принять еще 2 поля (повтор пароля и капчу), то добавим эти поля в наш User partial class:
public partial class User { public static string GetActivateUrl() { return Guid.NewGuid().ToString("N"); } public string ConfirmPassword { get; set; } public string Captcha { get; set; } }
Добавим поля во View:
<div class="control-group"> <label class="control-label" for="FirstName"> Confirm Password </label> <div class="controls"> @Html.ValidationMessage("ConfirmPassword") @Html.Password("ConfirmPassword", Model.ConfirmPassword) </div> </div> <div class="control-group"> <label class="control-label" for="FirstName"> Captcha </label> </div> <div class="control-group"> <label class="control-label" for="FirstName"> Тут картинка 1234 </label> <div class="controls"> @Html.ValidationMessage("Captcha") @Html.TextBox("Captcha", Model.Captcha) </div> </div>
Капчу пока не будем делать, просто она будет равна 1234.
Валидация
Условия для правильности данных:
- Поле email не нулевое
- Email – это корректно введенный адрес почты, т.е. с собачкой
- Email добавляемый в БД — уникальный
- Пароль не нулевой
- Пароли совпадают
- Капча равна 1234
Если какое-то из этих условий не соблюдается, то выдается ошибка.
IValidatableObject
Так как у нас класс User — partial, то мы можем реализовать для него IValidatableObject интерфейс, для этого, правда, придется добавить в проект System.Component.DataAnnotation. Это не очень хорошо, так как эта сборка необходима для валидации, а валидация – это прерогатива контроллеров в MVC. Так что мы тут немного нарушаем принцип.
Класс User:
public partial class User : IValidatableObject { public static string GetActivateUrl() { return Guid.NewGuid().ToString("N"); } public string ConfirmPassword { get; set; } public string Captcha { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { //Не нулевой Email if (string.IsNullOrWhiteSpace(Email)) { yield return new ValidationResult("Введите email", new string[] {"Email"}); } //корректный Email var regex = new Regex(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.Compiled); var match = regex.Match(Email); if (!(match.Success && match.Length == Email.Length)) { yield return new ValidationResult("Введите корректный email", new string[] { "Email" }); } //пароль не нулевой if (string.IsNullOrWhiteSpace(Password)) { yield return new ValidationResult("Введите пароль", new string[] { "Password" }); } //пароли совпадают if (Password != ConfirmPassword) { yield return new ValidationResult("Пароли не совпадают", new string[] { "ConfirmPassword" }); } } }
Мы смогли сделать проверку 4 из 6 правил валидации, но оставим пока так, а остальные добавим непосредственно в контроллере.
Выполняем форму, получаем:
Видим, что обе наши ошибки были отловлены.
Есть два стандартных метода вывести ошибку: это Html.ValidationMessage(“ErrorField”) и Html.ValidationSummary(). Первый выводит ошибку, связанную с конкретным неверновведенным полем, а второе — выведет все (или все оставшиеся) ошибки.
Добавляем в контроллер проверку на капчу и проверку на существование Email в БД (/Areas/Default/UserController.cs:Register):
if (user.Captcha != "1234") { ModelState.AddModelError("Captcha", "Текст с картинки введен неверно"); } var anyUser = Repository.Users.Any(p => string.Compare(p.Email, user.Email) == 0); if (anyUser) { ModelState.AddModelError("Email", "Пользователь с таким email уже зарегистрирован"); }
И результат:
Что ж, с задачей мы справились, но в дальнейшем, используя такой способ, мы получим несколько проблем:
- Класс User всегда будет содержать проверку на необходимость введения пароля и идентичность паролей, а, например, при изменении данных в личном кабинете, мы вообще не должны вводить пароль. Т.е. необходимо будет вводить другие поля, которые будут обозначать: это регистрация, это смена пароля, это изменение данных.
- Валидацию мы сделали частично в Model-части и частично в Controller-части – это не совсем хрестоматийно.
Но есть решение, мы создаем класс, который является представлением класса User, организующим валидацию. Мы назовем его UserView и создадим в папке Models/ViewModels:
public class UserView { public int ID { get; set; } public string Email { get; set; } public string Password { get; set; } public string ConfirmPassword { get; set; } public string Captcha { get; set; } public string AvatarPath { get; set; } }
Automapping
Прежде чем приступить к использованию этого класса, стоит заметить, что это не совсем удобно. Мы создали совершенно другой класс, но добавлять в БД мы должны класс User, а это означает, что в каком-то месте программы мы должны передавать от объекта UserView в User поля, так и наоборот. А при большом количестве объектов и полей – это рутинно, к тому же, подобное у нас уже есть в функции Update[Table] в репозитории. Для решения этой задачи существуют так называемые мапперы object-to-object.
Одним из самых популярных, является automapper (http://automapper.org/). Собственно, эта библиотека берет на себя работу по переводу одного объекта в другой, и, как мы дальше увидим, там еще есть много других вкусных плюшек.
Устанавливаем Automapper:
Install-Package AutoMapper
Так как при разработке программы мы избегаем сильную связность, то организуем интерфейс + реализацию и зарегистрируем это в Ninject, после чего выведем использование в контроллер.
Создаем в /Mappers:
public interface IMapper { object Map(object source, Type sourceType, Type destinationType); }
Реализация:
public class CommonMapper : IMapper { static CommonMapper() { Mapper.CreateMap<User, UserView>(); Mapper.CreateMap<UserView, User>(); } public object Map(object source, Type sourceType, Type destinationType) { return Mapper.Map(source, sourceType, destinationType); } }
Регистрация (пусть будет как объект-одиночка) (/App_Start/NinjectWebCommon.cs):
kernel.Bind<IMapper>().To<CommonMapper>().InSingletonScope();
В BaseController (/Controllers/BaseController.cs):
public abstract class BaseController : Controller { [Inject] public IRepository Repository { get; set; } [Inject] public IMapper ModelMapper { get; set; } }
Теперь изменим UserController (и View) с использованием UserView:
[HttpGet] public ActionResult Register() { var newUserView = new UserView(); return View(newUserView); } [HttpPost] public ActionResult Register(UserView userView) { if (userView.Captcha != "1234") { ModelState.AddModelError("Captcha", "Текст с картинки введен неверно"); } var anyUser = Repository.Users.Any(p => string.Compare(p.Email, userView.Email) == 0); if (anyUser) { ModelState.AddModelError("Email", "Пользователь с таким email уже зарегистрирован"); } if (ModelState.IsValid) { var user = (User)ModelMapper.Map(userView, typeof(UserView), typeof(User)); //TODO: Сохранить } return View(userView); }
И в Register.cshtml изменится первая строка:
@model LessonProject.Models.ViewModels.UserView
Атрибуты
Для UserView будем использовать для валидации атрибуты.
Добавим сборку:
using System.ComponentModel.DataAnnotations; public class UserView { public int ID { get; set; } [Required(ErrorMessage="Введите email")] public string Email { get; set; } [Required(ErrorMessage="Введите пароль")] public string Password { get; set; } [Compare("Password", ErrorMessage="Пароли должны совпадать")] public string ConfirmPassword { get; set; } public string Captcha { get; set; } public string AvatarPath { get; set; } }
Проверяем:
Мы смогли описать тут 5 из 6 правил валидации. Правила, касающегося верного введенного email – нет. Напишем для этого свой класс-атрибут, проверяющий корректность введенного email:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] public class ValidEmailAttribute : ValidationAttribute { public override bool IsValid(object value) { if (value == null) { return true; } if (!(value is string)) { return true; } var source = value as string; if (string.IsNullOrWhiteSpace(source)) { return true; } var regex = new Regex(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*", RegexOptions.Compiled); var match = regex.Match(source); return (match.Success && match.Length == source.Length); } }
Вначале проверяем, что полученный объект есть строка, и строка не пустая, иначе возвращаем значение «истина» в проверке. Тут срабатывает правило, что «мы у инопланетян документы не проверяем», т.е. пока нет достаточных условий для проверки – мы не проверяем, а проверять будут другие атрибуты. Потом же, с помощью регулярного выражения, проверяем. При желании, в интернете можно найти более полную проверку регулярным выражением с использованием всех доменов первого уровня.
Примечание: Можно подключить DataAnnotationsExtensions, чтобы не писать самому нужные атрибуты (http://dataannotationsextensions.org/)
Добавим пользователю поле дня рождения. Да, прямо сейчас, и посмотрим, как можно реализовать выбор даты.
- Добавляем поле в БД. Birthdate datetime null.
Примечание: возможно, надо будет снять эту галочку, чтобы спокойно изменять структуру БД:
- В данных выставим всем записям значения 2012-1-1
- Изменим поле Birthdate на datetime not null
- Удаляем из
LessonProjectDb.dbml
таблицу User и заново переносим из Server Explorer - В SqlRepository/User.cs добавляем строку в UpdateUser():
public bool UpdateUser(User instance) { User cache = Db.Users.Where(p => p.ID == instance.ID).FirstOrDefault(); if (cache != null) { cache.Birthdate = instance.Birthdate; cache.AvatarPath = instance.AvatarPath; cache.Email = instance.Email; Db.Users.Context.SubmitChanges(); return true; } return false; }
- В UserView у нас будет совершенно другое представление о поле Bithdate. И об этом чуть подробнее отдельно.
Выбор дня рождения у нас будет таким:
Тут надо решить несколько задач. Первая из них – создание и организация выпадающего списока. В Html (который мы еще позже рассмотрим подробнее) есть DropDownList, который реализует выпадающий список.
Параметры такие:
@Html.DropDownList(string name, IEnumerable<SelectListItem> selectList)
Смотрим SelectListItem:
public class SelectListItem { public SelectListItem(); public bool Selected { get; set; } public string Text { get; set; } public string Value { get; set; } }
Для выбора, например, из 1 — apple, 2 – orange (выбран), 3 — banana мы должны написать следующий код:
public IEnumerable<SelectListItem> SelectFruit { get { yield return new SelectListItem() { Value = "1", Text = "apple", Selected = false }; yield return new SelectListItem() { Value = "2", Text = "orange", Selected = true }; yield return new SelectListItem() { Value = "3", Text = "banana", Selected = false }; } }
И передать в DropDownList()
вторым параметром, первый параметр – name, которому присвоится значение Value при подтверждении (сабмите) формы.
Cоздадим реализацию для выбора дня рождения:
public int BirthdateDay { get; set; } public int BirthdateMonth { get; set; } public int BirthdateYear { get; set; } public IEnumerable<SelectListItem> BirthdateDaySelectList { get { for (int i = 1; i < 32; i++) { yield return new SelectListItem { Value = i.ToString(), Text = i.ToString(), Selected = BirthdateDay == i }; } } } public IEnumerable<SelectListItem> BirthdateMonthSelectList { get { for (int i = 1; i < 13; i++) { yield return new SelectListItem { Value = i.ToString(), Text = new DateTime(2000, i, 1).ToString("MMMM"), Selected = BirthdateMonth == i }; } } } public IEnumerable<SelectListItem> BirthdateYearSelectList { get { for (int i = 1910; i < DateTime.Now.Year; i++) { yield return new SelectListItem { Value = i.ToString(), Text = i.ToString(), Selected = BirthdateYear == i }; } } }
И во View:
<div class="control-group"> <label class="control-label" for="FirstName"> Birth date </label> <div class="controls"> @Html.DropDownList("BirthdateDay", Model.BirthdateDaySelectList) @Html.DropDownList("BirthdateMonth", Model.BirthdateMonthSelectList) @Html.DropDownList("BirthdateYear", Model.BirthdateYearSelectList) </div> </div>
Запустим приложение и поставим брейк-поинт point на приеме данных. Проверим, как мы получаем данные для полей даты рождения для объекта UserView:
Теперь осталось правильно передать их в объект User. Опишем эту передачу в описании маппинга (/Mappers/CommonMapper.cs):
Mapper.CreateMap<User, UserView>() .ForMember(dest => dest.BirthdateDay, opt => opt.MapFrom(src => src.Birthdate.Day)) .ForMember(dest => dest.BirthdateMonth, opt => opt.MapFrom(src => src.Birthdate.Month)) .ForMember(dest => dest.BirthdateYear, opt => opt.MapFrom(src =>src.Birthdate.Year)); Mapper.CreateMap<UserView, User>() .ForMember(dest => dest.Birthdate, opt => opt.MapFrom(src => new DateTime(src.BirthdateYear, src.BirthdateMonth, src.BirthdateDay)));
Здесь мы задаем правила однозначного перевода из свойств BirthdateDay, BirthdateMonth, BirthdateYear в Birthdate и обратно.
Captcha
Для создания капчи, мы используем отдельный класс, который создаст нам картинку с цифрами и выведет как картинку. Сами цифры будут сохранены в сессионные данные. Про сессию мы дальше еще поговорим. Сейчас надо знать только, что сессия однозначно определяет пользователя.
/// <summary> /// Генерация капчи /// </summary> public class CaptchaImage { public const string CaptchaValueKey = "CaptchaImageText"; public string Text { get { return text; } } public Bitmap Image { get { return image; } } public int Width { get { return width; } } public int Height { get { return height; } } // Internal properties. private string text; private int width; private int height; private string familyName; private Bitmap image; // For generating random numbers. private Random random = new Random(); public CaptchaImage(string s, int width, int height) { text = s; SetDimensions(width, height); GenerateImage(); } public CaptchaImage(string s, int width, int height, string familyName) { text = s; SetDimensions(width, height); SetFamilyName(familyName); GenerateImage(); } // ==================================================================== // This member overrides Object.Finalize. // ==================================================================== ~CaptchaImage() { Dispose(false); } // ==================================================================== // Releases all resources used by this object. // ==================================================================== public void Dispose() { GC.SuppressFinalize(this); Dispose(true); } // ==================================================================== // Custom Dispose method to clean up unmanaged resources. // ==================================================================== protected virtual void Dispose(bool disposing) { if (disposing) // Dispose of the bitmap. image.Dispose(); } // ==================================================================== // Sets the image aWidth and aHeight. // ==================================================================== private void SetDimensions(int aWidth, int aHeight) { // Check the aWidth and aHeight. if (aWidth <= 0) throw new ArgumentOutOfRangeException("aWidth", aWidth, "Argument out of range, must be greater than zero."); if (aHeight <= 0) throw new ArgumentOutOfRangeException("aHeight", aHeight, "Argument out of range, must be greater than zero."); width = aWidth; height = aHeight; } // ==================================================================== // Sets the font used for the image text. // ==================================================================== private void SetFamilyName(string aFamilyName) { // If the named font is not installed, default to a system font. try { Font font = new Font(aFamilyName, 12F); familyName = aFamilyName; font.Dispose(); } catch (Exception) { familyName = FontFamily.GenericSerif.Name; } } // ==================================================================== // Creates the bitmap image. // ==================================================================== private void GenerateImage() { // Create a new 32-bit bitmap image. Bitmap bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb); // Create a graphics object for drawing. Graphics g = Graphics.FromImage(bitmap); g.SmoothingMode = SmoothingMode.AntiAlias; Rectangle rect = new Rectangle(0, 0, width, height); // Fill in the background. HatchBrush hatchBrush = new HatchBrush(HatchStyle.SmallConfetti, Color.LightGray, Color.White); g.FillRectangle(hatchBrush, rect); // Set up the text font. SizeF size; float fontSize = rect.Height + 1; Font font; // Adjust the font size until the text fits within the image. do { fontSize--; font = new Font(familyName, fontSize, FontStyle.Bold); size = g.MeasureString(text, font); } while (size.Width > rect.Width); // Set up the text format. StringFormat format = new StringFormat(); format.Alignment = StringAlignment.Center; format.LineAlignment = StringAlignment.Center; // Create a path using the text and warp it randomly. GraphicsPath path = new GraphicsPath(); path.AddString(text, font.FontFamily, (int)font.Style, font.Size, rect, format); float v = 4F; PointF[] points = { new PointF(random.Next(rect.Width) / v, random.Next(rect.Height) / v), new PointF(rect.Width - random.Next(rect.Width) / v, random.Next(rect.Height) / v), new PointF(random.Next(rect.Width) / v, rect.Height - random.Next(rect.Height) / v), new PointF(rect.Width - random.Next(rect.Width) / v, rect.Height - random.Next(rect.Height) / v) }; Matrix matrix = new Matrix(); matrix.Translate(0F, 0F); path.Warp(points, rect, matrix, WarpMode.Perspective, 0F); // Draw the text. hatchBrush = new HatchBrush(HatchStyle.LargeConfetti, Color.LightGray, Color.DarkGray); g.FillPath(hatchBrush, path); // Add some random noise. int m = Math.Max(rect.Width, rect.Height); for (int i = 0; i < (int)(rect.Width * rect.Height / 30F); i++) { int x = random.Next(rect.Width); int y = random.Next(rect.Height); int w = random.Next(m / 50); int h = random.Next(m / 50); g.FillEllipse(hatchBrush, x, y, w, h); } // Clean up. font.Dispose(); hatchBrush.Dispose(); g.Dispose(); // Set the image. image = bitmap; } }
Суть такова, что в свойство Image генерируется картинка, состоящая из цифр (которые как бы сложно распознать) методом GenerateImage().
Теперь сделаем метод вывода UserController.Captcha():
public ActionResult Captcha() { Session[CaptchaImage.CaptchaValueKey] = new Random(DateTime.Now.Millisecond).Next(1111, 9999).ToString(); var ci = new CaptchaImage(Session[CaptchaImage.CaptchaValueKey].ToString(), 211, 50, "Arial"); // Change the response headers to output a JPEG image. this.Response.Clear(); this.Response.ContentType = "image/jpeg"; // Write the image to the response stream in JPEG format. ci.Image.Save(this.Response.OutputStream, ImageFormat.Jpeg); // Dispose of the CAPTCHA image object. ci.Dispose(); return null; }
Что здесь происходит:
- В сессии создаем случайное число от 1111 до 9999.
- Создаем в ci объект CatchaImage
- Очищаем поток вывода
- Задаем header для mime-типа этого http-ответа будет “image/jpeg” т.е. картинка формата jpeg.
- Сохраняем bitmap в выходной поток с форматом ImageFormat.Jpeg
- Освобождаем ресурсы Bitmap
- Возвращаем null, так как основная информация уже передана в поток вывода
Запрашиваем картинку из Register.cshtml (/Areas/Default/View/User/Register.cshtml):
<label class="control-label" for="FirstName"> <img src="@Url.Action("Captcha", "User")" alt="captcha" /> </label>
Проверка (/Areas/Default/Controllers/UserController.cs):
if (userView.Captcha != (string)Session[CaptchaImage.CaptchaValueKey]) { ModelState.AddModelError("Captcha", "Текст с картинки введен неверно"); }
Вот и всё, закончили. Добавляем создание записи и проверяем, как она работает:
if (ModelState.IsValid) { var user = (User)ModelMapper.Map(userView, typeof(UserView), typeof(User)); Repository.CreateUser(user); return RedirectToAction("Index"); }
Все исходники находятся по адресу https://bitbucket.org/chernikov/lessons
ссылка на оригинал статьи http://habrahabr.ru/post/176023/
Добавить комментарий