Спрашивали? Отвечаем - Школа MODX

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

Что это?

Авторизация - это процесс предоставления доступа к контроллерам и методам действий для определенных пользователей, как правило, находящихся в определенных ролях (например, допуск к админке должны иметь только администраторы).

Зачем нужно использовать?

Без авторизации вы сможете различать только две категории пользователей: прошедшие и не прошедшие аутентификацию. Большинство приложений имеют список ролей, таких как: пользователь, модератор, администратор и т. д.

Как использовать в рамках MVC?

Добавление поддержки ролей

ASP.NET Identity содержит строго типизированный базовый класс для доступа и управления ролями, который называется RoleManager , где T является реализацией интерфейса IRole , описывающего механизм хранения данных, используемых для представления ролей. Entity Framework использует класс IdentityRole , являющийся реализацией интерфейса IRole и содержит следующие свойства:

Мы не будем использовать напрямую объекты IdentityRole в нашем приложении, вместо этого добавьте файл класса AppRole.cs в папку Models со следующим содержимым:

Using Microsoft.AspNet.Identity.EntityFramework; namespace Users.Models { public class AppRole: IdentityRole { public AppRole() : base() { } public AppRole(string name) : base(name) { } } }

Класс RoleManager работает с экземплярами IRole с помощью методов и свойств, перечисленных в таблице ниже:

Эти базовые методы реализуют тот же базовый шаблон, который использует класс UserManager для управления пользователями. Добавьте файл AppRoleManager.cs в папку Infrastructure со следующим содержимым:

Using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using System; using Users.Models; namespace Users.Infrastructure { public class AppRoleManager: RoleManager, IDisposable { public AppRoleManager(RoleStore store) : base(store) { } public static AppRoleManager Create(IdentityFactoryOptions options, IOwinContext context) { return new AppRoleManager(new RoleStore(context.Get())); } } }

Этот класс определяет статический метод Create(), который позволит OWIN создавать экземпляры класса AppRoleManager для всех запросов, где требуются данные Identity, не раскрывая информации о том, как данные о ролях хранятся в приложении. Чтобы зарегистрировать класс управления ролями в OWIN, необходимо отредактировать файл IdentityConfig.cs, как показано в примере ниже:

// ... namespace Users { public class IdentityConfig { public void Configuration(IAppBuilder app) { // ... app.CreatePerOwinContext(AppRoleManager.Create); // ... } } }

Это гарантирует, что экземпляры класса AppRoleManager используют тот же контекст базы данных Entity Framework, что и экземпляры AppUserManager.

Создание и удаление ролей

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

Using System.Web; using System.Web.Mvc; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using System.ComponentModel.DataAnnotations; using Users.Infrastructure; using Users.Models; namespace Users.Controllers { public class RoleAdminController: Controller { private AppUserManager UserManager { get { return HttpContext.GetOwinContext().GetUserManager(); } } private AppRoleManager RoleManager { get { return HttpContext.GetOwinContext().GetUserManager(); } } public ActionResult Index() { return View(RoleManager.Roles); } public ActionResult Create() { return View(); } public async Task Create(string name) { if (ModelState.IsValid) { IdentityResult result = await RoleManager.CreateAsync(new AppRole(name)); if (result.Succeeded) { return RedirectToAction("Index"); } else { AddErrorsFromResult(result); } } return View(name); } public async Task Delete(string id) { AppRole role = await RoleManager.FindByIdAsync(id); if (role != null) { IdentityResult result = await RoleManager.DeleteAsync(role); if (result.Succeeded) { return RedirectToAction("Index"); } else { return View("Error", result.Errors); } } else { return View("Error", new string { "Роль не найдена" }); } } private void AddErrorsFromResult(IdentityResult result) { foreach (string error in result.Errors) { ModelState.AddModelError("", error); } } } }

Здесь мы применили многие из тех приемов, что использовали в контроллере Admin, в том числе добавили свойства UserManager и RoleManager для более быстрого запроса объектов AppRoleManager и AppUserManager. Также мы добавили аналогичный метод AddErrorsFromResult(), который обрабатывает ошибки в объекте IdentityResult и добавляет их в метаданные модели.

Представления для контроллера RoleAdmin содержат простую HTML-разметку и операторы Razor. Нам необходимо отобразить не только список ролей, но и имена всех пользователей, входящих в каждую роль. Класс IdentityRole определяет свойство Users, которое возвращает коллекцию объектов IdentityUserRole , описывающих пользователей роли. Каждый объект IdentityUserRole имеет свойство UserId, которое возвращает уникальный идентификатор пользователя, с помощью которого мы будем получать имя пользователя.

Добавьте файл класса IdentityHelpers.cs в папку Infrastructure со следующим содержимым:

Using System.Web; using System.Web.Mvc; using Microsoft.AspNet.Identity.Owin; namespace Users.Infrastructure { public static class IdentityHelpers { public static MvcHtmlString GetUserName(this HtmlHelper html, string id) { AppUserManager mgr = HttpContext.Current .GetOwinContext().GetUserManager(); return new MvcHtmlString(mgr.FindByIdAsync(id).Result.UserName); } } }

Этот код содержит определение вспомогательного метода HTML , определенного как расширение класса HtmlHelper. Метод GetUserName() принимает строковый аргумент, содержащий идентификатор пользователя, получает экземпляр класса AppUserManager с помощью метода GetOwinContext().GetUserManager() (где метод GetOwinContext является расширяющим HttpContext), использует метод FindByIdAsync(), чтобы найти экземпляр AppUser, связанный с идентификатором и возвращает значение свойства UserName.

Следующий пример показывает содержимое файла Index.cshtml, находящегося в папке /Views/RoleAdmin:

@using Users.Models @using Users.Infrastructure @model IEnumerable @{ ViewBag.Title = "Роли"; }

Roles
@if (Model.Count() == 0) { } else { foreach (AppRole role in Model) { } }
ID Название Пользователи
Нет ролей
@role.Id @role.Name @if (role.Users == null || role.Users.Count == 0) { @: Нет пользователей в этой роли } else {

@string.Join(", ", role.Users.Select(x => Html.GetUserName(x.UserId)))

}
@using (Html.BeginForm("Delete", "RoleAdmin", new { id = role.Id })) { @Html.ActionLink("Изменить", "Edit", new { id = role.Id }, new { @class = "btn btn-primary btn-xs", style= "float:left; margin-right:5px" }) }
@Html.ActionLink("Создать", "Create", null, new { @class = "btn btn-primary" })

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

Следующий пример содержит представление Create.cshtml в той же папке, которое используется для создания новых ролей:

@model string @{ ViewBag.Title = "Создание роли"; }

Создать роль

@Html.ValidationSummary(false) @using (Html.BeginForm()) {

Единственная информация, которая требуется для создания новой роли - ее название. Поэтому мы добавили один стандартный элемент и кнопку отправки формы POST-методу действия Create.

Чтобы протестировать функционал создания ролей, запустите приложение и перейдите по адресу /RoleAdmin/Index в окне браузера. Чтобы создать новую роль нажмите кнопку «Создать», введите имя в поле ввода в появившейся форме и нажмите вторую кнопку «Создать». Новое представление будет отображать список ролей, сохраненных в базе данных:

Вы можете также удалить роль из приложения нажав кнопку «Удалить».

Редактирование ролей

Для авторизации пользователей недостаточно просто создавать и удалять роли. Мы также должны уметь управлять ролями, назначать и удалять пользователей из роли. Это не сложный процесс, но для его реализации нам необходимо загружать данные о ролях с помощью класса AppRoleManager, а затем вызывать методы, определенные в классе AppUserManager на объектах, связанных с определенной ролью.

Давайте начнем с добавления новых классов модели-представления (view-model) в файл UserViewModels.cs:

Using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace Users.Models { public class CreateModel { // ... } public class LoginViewModel { // ... } public class RoleEditModel { public AppRole Role { get; set; } public IEnumerable Members { get; set; } public IEnumerable NonMembers { get; set; } } public class RoleModificationModel { public string RoleName { get; set; } public string IdsToAdd { get; set; } public string IdsToDelete { get; set; } } }

Класс RoleEditModel содержит информацию о роли и определяет список пользователей в роли в виде коллекции объектов AppUser. Благодаря этому, мы сможем извлечь ID и имя каждого пользователя в роли. Класс RoleModificationModel будет получать данные от системы привязки модели во время редактирования данных пользователя. Он содержит массив идентификаторов пользователей, а не объектов AppUser, для замены ролей.

Определившись с классами моделей, давайте добавим методы редактирования ролей Edit в контроллер RoleAdmin:

Using System.Web; using System.Web.Mvc; using System.Threading.Tasks; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; using System.ComponentModel.DataAnnotations; using Users.Infrastructure; using Users.Models; using System.Linq; using System.Collections.Generic; namespace Users.Controllers { public class RoleAdminController: Controller { // ... public async Task Edit(string id) { AppRole role = await RoleManager.FindByIdAsync(id); string memberIDs = role.Users.Select(x => x.UserId).ToArray(); IEnumerable members = UserManager.Users.Where(x => memberIDs.Any(y => y == x.Id)); IEnumerable nonMembers = UserManager.Users.Except(members); return View(new RoleEditModel { Role = role, Members = members, NonMembers = nonMembers }); } public async Task Edit(RoleModificationModel model) { IdentityResult result; if (ModelState.IsValid) { foreach (string userId in model.IdsToAdd ?? new string { }) { result = await UserManager.AddToRoleAsync(userId, model.RoleName); if (!result.Succeeded) { return View("Error", result.Errors); } } foreach (string userId in model.IdsToDelete ?? new string { }) { result = await UserManager.RemoveFromRoleAsync(userId, model.RoleName); if (!result.Succeeded) { return View("Error", result.Errors); } } return RedirectToAction("Index"); } return View("Error", new string { "Роль не найдена" }); } } }

Большая часть кода в GET-версии метода Edit отвечает за формирование списков пользователей входящих и не входящих в роль и реализуется с помощью методов LINQ. После группировки пользователей возвращается представление, которому передается объект RoleEditModel.

POST-версия метода Edit отвечает за добавление и удаление пользователей из ролей. Класс AppUserManager наследует ряд вспомогательных методов для работы с ролями из класса UserManager. Эти методы перечислены в таблице ниже:

Странность этих методов заключается в том, что они работают с идентификатором пользователя и именем роли, хотя каждая роль также имеет свой уникальный идентификатор. Именно поэтому класс RoleModificationModel содержит строковое свойство RoleName.

В примере ниже показан код представления Edit.cshtml, находящегося в папке /Views/RoleAdmin.cshtml:

@using Users.Models @model RoleEditModel @{ ViewBag.Title = "Изменить роль"; }

Изменить роль

@Html.ValidationSummary() @using (Html.BeginForm()) {
Добавить в роль @Model.Role.Name
@if (Model.NonMembers.Count() == 0) { } else { foreach (AppUser user in Model.NonMembers) { } }
Все пользователи в роли
User ID Добавить в роль
@user.UserName
Удалить из роли @Model.Role.Name
@if (Model.Members.Count() == 0) { } else { foreach (AppUser user in Model.Members) { } }
Нет пользователей в роли
User ID Удалить из роли
@user.UserName
@Html.ActionLink("Отмена", "Index", null, new { @class = "btn btn-default" }) }

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

Давайте протестируем функциональность редактирования ролей. Добавление класса AppRoleManager в архитектуру OWIN заставит Entity Framework удалить базу данных и воссоздать новую схему. Это означает, что пользователи, которых мы создали ранее исчезнут. Поэтому после запуска приложения перейдите по адресу /Admin/Index и создайте нескольких пользователей.

Чтобы проверить редактирование ролей, перейдите по адресу /RoleAdmin/Index и создайте несколько ролей, затем отредактируйте эти роли, добавив в них нескольких пользователей. На рисунке ниже показан пример приложения (я создал роль Users):

Использование ролей для авторизации

Теперь, когда у нас есть возможность управления ролями, мы можем использовать их в качестве основы для авторизации через атрибут Authorize. Чтобы проще было тестировать процесс авторизации, давайте добавим метод действия для выхода пользователя из системы в контроллер Account, как показано в примере ниже:

Public class AccountController: Controller { // ... public ActionResult Logout() { AuthManager.SignOut(); return RedirectToAction("Index", "Home"); } // ... }

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

Using System.Collections.Generic; using System.Web.Mvc; namespace Users.Controllers { public class HomeController: Controller { public ActionResult Index() { return View(GetData("Index")); } public ActionResult OtherAction() { return View("Index", GetData("OtherAction")); } private Dictionary GetData(string actionName) { Dictionary dict = new Dictionary(); dict.Add("Action", actionName); dict.Add("Пользователь", HttpContext.User.Identity.Name); dict.Add("Аутентифицирован?", HttpContext.User.Identity.IsAuthenticated); dict.Add("Тип аутентификации", HttpContext.User.Identity.AuthenticationType); dict.Add("В роли Users?", HttpContext.User.IsInRole("Users")); return dict; } } }

В этом примере мы оставили атрибут Authorize для метода действия Index без изменений, но добавили этот атрибут к методу OtherAction, задав при этом свойство Roles, ограничивающее доступ к этому методу только для пользователей, являющихся членами роли Users. Мы также добавили метод GetData(), который добавляет некоторую базовую информацию о пользователе, используя свойства, доступные через объект HttpContext.

В заключение, нам необходимо добавить кнопку выхода из приложения в представление Index.cshtml из папки /Views/Home:

... @Html.ActionLink("Выйти", "Logout", "Account", null, new {@class = "btn btn-primary"})

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

Для тестирования системы авторизации, запустите приложение и перейдите по адресу /Home/Index. Браузер будет перенаправлен на страницу входа в приложение, где вы должны будете ввести данные существующей учетной записи. Метод действия Index является доступным для любого авторизованного пользователя. Однако если вы перейдете по адресу /Index/OtherAction, доступ будет открыт только тем пользователям, которые являются членами роли Users.

Если вы попытаетесь войти под пользователем, находящимся в другой роли, то браузер перенаправит вас снова на форму входа в приложение. Перенаправление уже аутентифицированных пользователей на страницу входа является малополезным решением, поэтому давайте отредактируем контроллер Account и добавим возможность перенаправления аутентифированных пользователей, не прошедших авторизацию, на страницу ошибки:

Public class AccountController: Controller { public ActionResult Login(string returnUrl) { if (HttpContext.User.Identity.IsAuthenticated) { return View("Error", new string { "В доступе отказано" }); } ViewBag.returnUrl = returnUrl; return View(); } // ... }

На рисунке ниже наглядно показано поведение нашего приложения, когда пользователю отказано в доступе:

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

Что такое событие?

Событием в языке C# называется сущность, предоставляющая две возможности: для класса - сообщать об изменениях, а для его пользователей - реагировать на них.
Пример объявления события:

Public event EventHandler Changed;
Рассмотрим, из чего состоит объявление. Сначала идут модификаторы события, затем ключевое слово event, после него - тип события, который обязательно должен быть типом-делегатом, и идентификатор события, то есть его имя. Ключевое слово event сообщает компилятору о том, что это не публичное поле, а специальным образом раскрывающаяся конструкция, скрывающая от программиста детали реализации механизма событий. Для того, чтобы понять, как работает этот механизм, необходимо изучить принципы работы делегатов.

Основа работы событий - делегаты

Можно сказать, что делегат в.NET - некий аналог ссылки на функцию в C++. Вместе с тем, такое определение неточно, т.к. каждый делегат может ссылаться не на один, а на произвольное количество методов, которые хранятся в списке вызовов делегата (invocation list). Тип делегата описывает сигнатуру метода, на который он может ссылаться, экземпляры этого типа имеют свои методы, свойства и операторы. При вызове метода Invoke() выполняется последовательный вызов каждого из методов списка. Делегат можно вызывать как функцию, компилятор транслирует такой вызов в вызов Invoke().
В C# для делегатов имеются операторы + и -, которые не существуют в среде.NET и являются синтаксическим сахаром языка, раскрываясь в вызов методов Delegate.Combine и Delegate.Remove соответственно. Эти методы позволяют добавлять и удалять методы в списке вызовов. Разумеется, форма операторов с присваиванием (+= и -=) также применима к операторам делегата, как и к определенным в среде.NET операторам + и - для других типов. Если при вычитании из делегата его список вызовов оказывается пуст, то ему присваивается null.
Рассмотрим простой пример:

Action a = () => Console.Write("A"); //Action объявлен как public delegate void Action(); Action b = a; Action c = a + b; Action d = a - b; a(); //выведет A b(); //выведет A c(); //выведет AA d(); //произойдет исключение NullReferenceException, т.к. d == null

События - реализация по умолчанию

События в языке C# могут быть определены двумя способами:
  1. Неявная реализация события (field-like event).
  2. Явная реализация события.
Уточню, что слова “явная” и “неявная” в данном случае не являются терминами, определенными в спецификации, а просто описывают способ реализации по смыслу.

Рассмотрим наиболее часто используемую реализацию событий - неявную. Пусть имеется следующий исходный код на языке C# 4 (это важно, для более ранних версий генерируется несколько иной код, о чем будет рассказано далее):

Class Class { public event EventHandler Changed; }
Эти строчки будут транслированы компилятором в код, аналогичный следующему:

Class Class { EventHandler сhanged; public event EventHandler Changed { add { EventHandler eventHandler = this.changed; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange(ref this.changed, comparand + value, comparand); } while(eventHandler != comparand); } remove { EventHandler eventHandler = this.changed; EventHandler comparand; do { comparand = eventHandler; eventHandler = Interlocked.CompareExchange(ref this.changed, comparand - value, comparand); } while (eventHandler != comparand); } } }
Блок add вызывается при подписке на событие, блок remove - при отписке. Эти блоки компилируются в отдельные методы с уникальными именами. Оба этих метода принимают один параметр - делегат типа, соответствующего типу события и не имеют возвращаемого значения. Имя параметра всегда ”value”, попытка объявить локальную переменную с таким именем приведет к ошибке компиляции. Область видимости, указанная слева от ключевого слова event определяет область видимости этих методов. Также создается делегат с именем события, который всегда приватный. Именно поэтому мы не можем вызвать событие, реализованное неявным способом, из наследника класса.

Interlocked.CompareExchange выполняет сравнение первого аргумента с третьим и если они равны, заменяет первый аргумент на второй. Это действие потокобезопасно. Цикл используется для случая, когда после присвоения переменной comparand делегата события и до выполнения сравнения другой поток изменяет этот делегат. В таком случае Interlocked.CompareExchange не производит замены, граничное условие цикла не выполняется и происходит следующая попытка.

Объявление с указанием add и remove

При явной реализации события программист объявляет делегат-поле для события и вручную добавляет или удаляет подписчиков через блоки add/remove, оба из которых должны присутствовать. Такое объявление часто используется для создания своего механизма событий с сохранением удобств языка C# в работе с ними.
Например, одна из типичных реализаций заключается в отдельном хранении словаря делегатов событий, в котором присутствуют только те делегаты, на события которых была осуществлена подписка. Доступ к словарю осуществляется по ключам, которыми обычно являются статические поля типа object, используемые только для сравнения их ссылок. Это делается для того, чтобы уменьшить количество памяти, занимаемое экземпляром класса (в случае, если он содержит большое количество нестатических событий). Эта реализация применяется в WinForms.

Как происходит подписка на событие и его вызов?

Все действия по подписке и отписке (обозначаются как += и -=, можно легко спутать с операторами делегатов) компилируются в вызовы методов add и remove. Вызовы внутри класса, отличные от вышеуказанных, компилируются в простую работу с делегатом. Следует заметить, что при неявной (и при правильной явной) реализации события невозможно получить доступ к делегату извне класса, работать можно лишь с событием как с абстракцией - подписываясь и отписываясь. Так как нет способа определить, подписались ли мы на какое-либо событие (если не использовать рефлексию), то кажется логичным, что отписка от него никогда не вызовет ошибок - можно смело отписываться, даже если делегат события пуст.

Модификаторы событий

Для событий могут использоваться модификаторы области видимости (public, protected, private, internal), они могут быть перекрыты (virtual, override, sealed) или не реализованы (abstract, extern). Событие может перекрывать событие с таким же именем из базового класса (new) или быть членом класса (static). Если событие объявлено и с модификатором override и с модификатором abstract одновременно, то наследники класса должны будут переопределить его (равно как и методы или свойства с этими двумя модификаторами).

Какие типы событий бывают?

Как уже было отмечено, тип события всегда должен быть типом делегата. Стандартными типами для событий являются типы EventHandler и EventHandler где TEventArgs - наследник EventArgs. Тип EventHandler используется когда аргументов события не предусмотрено, а тип EventHandler - когда аргументы события есть, тогда для них создается отдельный класс - наследник от EventArgs. Также можно использовать любые другие типы делегатов, но применение типизированного EventHandler выглядит более логичным и красивым.

Как все обстоит в C# 3?

Реализация field-like события, которая описана выше, соответствует языку C# 4 (.NET 4.0). Для более ранних версий существуют весьма существенные отличия.
Неявная реализация использует lock(this) для обеспечения потокобезопасности вместо Interlocked.CompareExchange с циклом. Для статических событий используется lock(typeof(Class)). Вот код, аналогичный раскрытому компилятором неявному определению события в C# 3:

Class Class { EventHandler changed; public event EventHandler Changed { add { lock(this) { changed = changed + value; } } remove { lock(this) { changed = changed - value; } } } }
Помимо этого, работа с событием внутри класса ведется как с делегатом, т.е. += и -= вызывают Delegate.Combine и Delegate.Remove напрямую, в обход методов add/remove. Это изменение может привести к невозможности сборки проекта на языке C# 4! В C# 3 результатом += и -= был делегат, т.к. результатом присвоения переменной всегда является присвоенное значение. В C# 4 результатом является void, т.к. методы add/remove не возвращают значения.

Помимо изменений в работе на разных версиях языка есть еще несколько особенностей.

Особенность №1 - продление времени жизни подписчика

При подписке на событие мы добавляем в список вызовов делегата события ссылку на метод, который будет вызван при вызове события. Таким образом, память, занимаемая объектом, подписавшимся на событие, не будет освобождена до его отписки от события или до уничтожения объекта, заключающего в себе событие. Эта особенность является одной из часто встречаемых причин утечек памяти в приложениях.
Для исправления этого недостатка часто используются weak events, слабые события. Эта тема уже .

Особенность №2 - явная реализация интерфейса

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

EventHandler changed; event EventHandler ISomeInterface.Changed { add { changed += value; } remove { changed -= value; } }

Особенность №3 - безопасный вызов

События перед вызовом следует проверять на null, что следует из описанной выше работы делегатов. От этого разрастается код, для избежания чего существует как минимум два способа. Первый способ описан Джоном Скитом (Jon Skeet) в его книге C# in depth :

Public event EventHandler Changed = delegate { };
Коротко и лаконично. Мы инициализируем делегат события пустым методом, поэтому он никогда не будет null. Вычесть из делегата этот метод невозможно, т.к. он определен при инициализации делегата и у него нет ни имени, ни ссылки на него из любого места программы.

Второй способ заключается в написании метода, содержащего в себе необходимую проверку на null. Этот прием особенно хорошо работает в.NET 3.5 и выше, где доступны методы расширений (extension methods). Так как при вызове метода расширений объект, на котором он вызывается, является всего лишь параметром этого метода, то этот объект может быть пустой ссылкой, что и используется в данном случае.

Public static class EventHandlerExtensions { public static void SafeRaise(this EventHandler handler, object sender, EventArgs e) { if(handler != null) handler(sender, e); } public static void SafeRaise(this EventHandler handler, object sender, TEventArgs e) where TEventArgs: EventArgs { if(handler != null) handler(sender, e); } }
Таким образом, мы можем вызывать события как Changed.SafeRaise(this, EventArgs.Empty), что экономит нам строчки кода. Также можно определить третий вариант метода расширений для случая, когда у нас EventArgs.Empty, чтобы не передавать их явно. Тогда код сократится до Changed.SafeRaise(this), но я не буду рекомендовать такой подход, т.к. для других членов вашей команды это может быть не так явно, как передача пустого аргумента.

Тонкость №4 - что не так со стандартной реализацией?

Если у вас стоит ReSharper, то вы могли наблюдать следующее его сообщение . Команда решарпера правильно считает, что не все ваши пользователи достаточно осведомлены в работе событий\делегатов в плане отписки\вычитания, но тем не менее ваши события должны работать предсказуемо не для ваших пользователей, а с точки зрения событий в.NET, а т.к. там такая особенность есть, то и в вашем коде она должна остаться.

Бонус: попытка Microsoft сделать контравариантные события

В первой бете C# 4 Microsoft попытались добавить контравариантности событиям. Это позволяло подписываться на событие EventHandler методами, имеющими сигнатуру EventHandler и все работало до тех пор, пока в делегат события не добавлялось несколько методов с разной (но подходящей) сигнатурой. Такой код компилировался, но падал с ошибкой времени выполнения. По всей видимости, обойти это так и не смогли и в релизе C# 4 контравариантность для EventHandler была отключена.
Это не так заметно, если опускать явное создание делегата при подписке, например следующий код отлично скомпилируется:

Public class Tests { public event EventHandler Changed; public void Test() { Changed += ChangedMyEventArgs; Changed += ChangedEventArgs; } void ChangedMyEventArgs(object sender, MyEventArgs e) { } void ChangedEventArgs(object sender, EventArgs e) { } }
Это происходит потому, что компилятор сам подставит new EventHandler(...) к обеим подпискам. Если же хотя бы в одном из мест использовать new EventHandler(...), то компилятор сообщит об ошибке - невозможно сконвертировать тип EventHandler в EventHandler (здесь Events - пространство имен моего тестового проекта).

Источники

Далее приведен список источников, часть материала из которых была использована при составлении статьи. Рекомендую к прочтению книгу Джона Скита (Jon Skeet), в которой в деталях описаны не только делегаты, но и многие другие средства языка.

Используйте события , чтобы узнавать, как пользователи взаимодействуют с вашим контентом.

Событие – это взаимодействие пользователя с контентом, которое можно отслеживать независимо от просмотров страниц или экранов. Скачивания файлов, клики по мобильным объявлениям, использование гаджетов, элементов Flash и AJAX, воспроизведение видео – все эти действия можно отслеживать в качестве событий.

Содержание

Просмотр данных о событиях

Чтобы просматривать данные в отчетах о событиях, нужно добавить код отслеживания на сайт или в приложение.

Чтобы просмотреть отчеты о событиях, выполните следующие действия:

  1. Выберите нужное представление .
  2. Откройте Отчеты .
  3. Выберите Поведение > События .

Компоненты событий

Событие состоит из следующих компонентов. Если событие произошло, для каждого из компонентов регистрируется значение, которое появляется в отчетах:

  • Категория
  • Действие
  • Ярлык (необязательно, но рекомендуется)
  • Значение (необязательно)

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

Категории нужны, чтобы объединять схожие типы событий. Обычно одна и та же категория многократно используется для связанных элементов пользовательского интерфейса, чтобы сгруппировать их.

Предположим, что вам также нужно отслеживать, сколько раз скачали ваше видео (см. пример выше). Можно использовать следующий набор компонентов:

В этом случае в отчетах будет только одна категория – Videos (Видеозаписи) – и вы увидите сводные показатели взаимодействия пользователей со всеми элементами этого видеообъекта.

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

С другой стороны, можно создать отдельные категории для видеозаписей разных типов: одну – для кинофильмов, другую – для музыкальных видеоклипов, и т. д. Можно также создать отдельную категорию для скачивания видео:

  • Видео – фильмы
  • Видео – музыка
  • Скачивание видео

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

Объектная модель отслеживания событий обеспечивает максимальную гибкость, но все же мы рекомендуем сначала продумать структуру отчетов, а затем выбирать названия категорий. Если вы будете повторно использовать название категории, не используйте другие варианты названия. Например, если для отслеживания видео вы добавите категорию Video, а затем в названии по ошибке используете множественное число (Videos), то у вас будет две разных категории для отслеживания видео. Кроме того, если вы измените название категории объекта, который уже отслеживается, данные по исходной категории не будут изменены. Поэтому в отчетах показатели одного элемента страницы будут представлены в двух разных категориях.

Действие

Обычно параметр "Действие" используется для обозначения отслеживаемого события или взаимодействия, связанного с веб-объектом. Например, с помощью этого параметра можно отслеживать в одной категории Videos (Видеозаписи) несколько событий:

  • Время завершения загрузки видео
  • Нажатия кнопки "Воспроизведение"
  • Нажатия кнопки "Стоп"
  • Нажатия кнопки "Пауза"
  • Все действия перечисляются независимо от своих родительских категорий. Это удобно для сегментирования данных событий в отчетах.
  • Уникальное событие определяется уникальным именем действия. В разных категориях можно использовать одинаковые названия действий, но это может повлиять на расчет количества уникальных событий. Подробнее об этом можно узнать в разделах "Рекомендации" и "Неявный подсчет" ниже.

Ярлыки

Ярлыки позволяют предоставить дополнительную информацию для отслеживаемых событий, например название фильма в приведенных выше примерах видеозаписей или имя файла при отслеживании скачивания.

Эти компоненты событий, подобно категориям и действиям, можно просматривать в отдельном отчете, где собраны все созданные вами ярлыки. Ярлык можно рассматривать как дополнительный параметр взаимодействия пользователя с объектами страницы. Предположим, вам нужно отслеживать взаимодействие с пятью проигрывателями видео на странице сайта. Каждый из этих проигрывателей может использовать категорию Videos (Видеозаписи) с действием Play (Воспроизведение), но для каждого можно также создать отдельный ярлык (например, с названием фильма), и они будут представлены в отчете как отдельные элементы.

Чтобы отчеты были максимально эффективны, следуйте приведенным ниже рекомендациям.

  • Названия действий должны соответствовать данным отчета. При отслеживании событий показатели для действий с одним названием из двух разных категорий объединяются. Например, если действие под названием Click (Клик) используется в категории Downloads (Скачивание) и в категории Videos (Видеозаписи), то показатели для него в отчете "Основные действия" отображаются по всем взаимодействиям с этим названием. Подробные данные для действия Click (Клик) по категориям можно просмотреть на следующем уровне отчета. Однако неосмотрительное использование действия Click (Клик) в отслеживании различных событий снижает эффективность работы с отчетами. Если вы отслеживаете большое количество событий на сайте, выбирайте для действий имена, которые отражают категории отслеживаемых данных. Например, название Click (Клик) можно использовать для взаимодействия с гаджетами, а для действий с проигрывателем – названия Play (Воспроизведение), Pause (Пауза) и Stop (Стоп).
  • Используйте глобальные названия действий, чтобы получать сводные и раздельные данные по взаимодействиям пользователей. Например, можно использовать название Play (Воспроизведение) в категории Videos (Видеозаписи) для всех видеозаписей на сайте. Тогда отчет "Основные действия" будет содержать сводные данные о действии "Воспроизведение", и вы сможете сравнить это событие с другими событиями в видеозаписях, например "Пауза" или "Стоп".

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

  • "Действие" не всегда означает действие. В качестве значения этого компонента можно использовать любую строку. В некоторых случаях название события или действия не очень информативно, поэтому для отслеживания других элементов можно использовать параметр действия. Например, если требуется отслеживать скачивание файлов на странице, в качестве параметра действия для события скачивания можно указать тип файла документа. В этом случае отчеты по категории Downloads (Скачивание) будут разбиты по типам файлов (PDF, DOC, XLS).
  • Количество уникальных событий увеличивается только за счет уникальных действий. Каждое взаимодействие пользователя с объектом, помеченным определенным названием действия, регистрируется как одно уникальное событие, связанное с этим названием. Любое дополнительное взаимодействие с тем же триггером действия в рамках одного сеанса не влияет на подсчет уникальных событий, связанных с этим действием. Даже если пользователь выполнит действие с этим названием на другом объекте, количество уникальных событий не изменится.

    Поэтому при работе с отчетами нужно учитывать два важных фактора. Во-первых, предположим, что пользователь выполняет действие Play (Воспроизведение) в двух разных проигрывателях, отнесенных к разным категориям. В отчетах "Основные действия" для Play (Воспроизведение) будет указано одно уникальное событие, хотя пользователь взаимодействовал с двумя уникальными проигрывателями. Во-вторых, в отчете по действиям из каждой категории будет указано одно уникальное действие, потому что фактически на каждую пару "категория-действие" приходится одно уникальное событие. Подробнее об этом можно узнать в разделе "Неявный подсчет" ниже.

Значения

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

Значение – это численная величина. Значения всех событий суммируются в отчете (см. раздел "Неявный подсчет" ниже). В отчете также определяется среднее значение для категории. В примере выше событие вызывается для действия "Время загрузки видео" сразу после завершения загрузки видеозаписи. Название видео указывается в ярлыке, а значение увеличивается с каждой загрузкой. После этого можно определить среднее время загрузки для всех действий "Время загрузки видео" из категории "Видеозаписи". Предположим, что у вас 5 уникальных загрузок видеозаписей с сайта со следующими значениями времени загрузки в секундах:

В этом случае среднее время загрузки в отчете подсчитывается так:
  • Сеансы с событиями: 5.
  • Значение: 53.
  • Среднее значение: 10,6.

Отрицательные значения не поддерживаются.

События без взаимодействия

Термин "без взаимодействия" относится к необязательному логическому параметру, который может быть передан методу, отправляющему событие. С помощью этого параметра вы можете задать, как должен определяться показатель отказов для страниц на сайте, на которых используется отслеживание событий. Предположим, у вас на главной странице есть встроенное видео и вы хотите узнать показатель отказов для этой страницы. Что следует считать отказом? Важно ли для вас взаимодействие с роликом на главной странице? Если да, то такое взаимодействие должно учитываться при подсчете показателя отказов. Сеансы, включающие только главную страницу с нажатием на видео, не должны считаться отказами. Если же вы хотите узнать показатель отказов для главной страницы независимо от взаимодействия с видео, тогда при подсчете отказов вам нужно исключить взаимодействия с видео.

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

Неявный подсчет

При отслеживании событий каждое взаимодействие с отслеживаемым объектом страницы учитывается и связывается с сеансом пользователя. В отчетах показатель "Всего событий" рассчитывается как общее количество взаимодействий с таким объектом.

Например, если один пользователь 5 раз нажмет одну и ту же кнопку на видео, то общее количество событий, связанных с этим видео, будет равно 5, а количество уникальных событий будет равно 1.

В таблице ниже показано, как в интерфейсе отчетов объединяются данные по определенной категории событий. В этом примере одно название категории используется в двух разных проигрывателях, каждому из которых присвоен свой ярлык. Здесь используются одинаковые действия Play (Воспроизведение) и Stop (Стоп), запрограммированные во Flash-интерфейсе проигрывателей.

Отслеживание событий для категории Videos (Видеозаписи)

Действие Ярлык: Gone With the Wind (Унесенные ветром) Ярлык: Mr Smith Goes to Washington (Мистер Смит едет в Вашингтон) Всего
Play (Воспроизведение)

22 клика в 10 сеансах с событием

7 кликов в 5 сеансах с событием

29 событий всего

15 уникальных событий Play (Воспроизведение)

Pause (Пауза)

3 клика в 2 сеансах с событием

16 кликов в 8 сеансах с событием

19 событий всего

10 уникальных событий Pause (Пауза)

Stop (Стоп)

2 клика в 2 сеансах с событием

4 клика в 3 сеансах с событием

6 событий всего

5 уникальных событий Stop (Стоп)

Всего

27 событий всего

14 уникальных событий для Gone With the Wind

27 событий всего

16 уникальных событий для Mr Smith Goes to Washington

54 события всего

30 уникальных событий для категории Videos

О чем важно помнить

Прежде чем настраивать отслеживание событий, учтите факторы, перечисленные ниже.

Влияние на показатель отказов

Обычно отказом считается сеанс, во время которого пользователь посетил только одну страницу сайта. В Google Аналитике отказом считается сеанс, в ходе которого был выполнен только один запрос GIF (например, если пользователь открыл одну страницу сайта и покинул его, не отправив других запросов к серверу Google Аналитики в рамках этого сеанса). Однако для страниц, на которых используется отслеживание событий, показатель отказов может изменяться. Это объясняется тем, что отслеживание событий, как и отслеживание страниц, классифицируется как запрос взаимодействия.

Предположим, что у вас есть страница с проигрывателем видео, для которой показатель отказов всегда был высоким, и вы не отслеживаете события на ней. Если вы настроите отслеживание событий для этого проигрывателя, то показатель отказов для страницы может уменьшиться, потому что Google Аналитика будет фиксировать взаимодействия пользователя с проигрывателем и отправлять дополнительный запрос к серверу для таких взаимодействий. Таким образом, доля посетителей, которые покидают сайт, не просматривая другие страницы, может оставаться прежней, но в результате их взаимодействия с проигрывателем запускаются вызовы отслеживания событий, и такие посещения не рассматриваются как отказы.

Поэтому для страниц с отслеживанием событий отказом считается посещение одной страницы сайта без взаимодействия с отслеживаемыми событиями.

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

Ограничение по количеству событий за сеанс

ga.js

Первые 10 событий, отправленных в Google Аналитику, обрабатываются сразу. Затем выполняется обработка не более одного события в секунду. За сеанс может быть обработано не более 500 событий.

analytics.js и gtag.js

Первые 20 событий, отправленных в Google Аналитику, обрабатываются сразу. Затем выполняется обработка не более двух событий в секунду. За сеанс может быть обработано не более 500 событий. Это ограничение распространяется на все обращения, за исключением типов item и transaction (данные по товарам и транзакциям).

Чтобы число обращений оставалось в пределах установленных ограничений, не рекомендуется делать следующее:

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

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

  • Заранее определите, для каких элементов вам может понадобиться отслеживать данные, даже если сейчас вас интересует только один объект на сайте. Это поможет вам разработать структуру отчетности, в рамках которой вы позднее сможете отслеживать сколько угодно событий любого типа.
  • Планируя отчеты по отслеживанию событий, обсудите все детали с теми, для кого эти отчеты предназначены. Четко понимая, что должно быть представлено в отчетах, вы сможете правильно реализовать отслеживание событий. Например, если в отчетах нужно отразить только взаимодействие с интерфейсом просмотра видео, структура категорий будет значительно отличаться от ситуации, когда требуется отслеживать другие элементы интерфейса на базе Flash, например меню, встроенные гаджеты и время загрузки. Кроме того, советуем обсудить с пользователями отчетов разные возможности отслеживания событий и выбрать наиболее подходящий вариант. Например, им может быть интересно не только взаимодействие пользователей с Flash-интерфейсом видеороликов, но и время их загрузки. Поэтому нужно заранее продумать, какие названия будут использоваться в вызовах событий.
  • Используйте точные и понятные названия: все названия категорий, действий и ярлыков, которые вы используете для отслеживания событий, отображаются в интерфейсе отчетов. Кроме того, в отчетах нельзя использовать одинаковые пары "категория-действие", поэтому заранее обдумайте, как будут рассчитываться показатели для всех объектов одной категории.

Чем отличаются события от целей-событий

Событие – это взаимодействие пользователя с вашим сайтом или приложением, которое задается и регистрируется путем изменения кода отслеживания, как описано в этой статье.

Цель-событие – это заданная вами , определяющая событие как конверсию.

Была ли эта статья полезна?

Как можно улучшить эту статью?

  • Блог компании Фонд развития интернет-инициатив
    • Перевод

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

    User story или «пользовательские истории/пожелания пользователя» являются методикой, опирающейся на другие практики agile, включая принципы непрерывной поставки и непосредственного общения с пользователями. Недостаточно просто понять, каким будет ваш пользователь; реальный пользователь вашей системы должен находиться рядом с командой на протяжении длительного времени.


    User story вкратце описывает:

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

    Обсудите историю перед планированием этапов с:

    • соответствующими членами команды;
    • экспертами по данному вопросу;
    • заинтересованными сторонами.

    Карточки пожеланий пользователя

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

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

    Вы можете выработать список критериев приемки при выполнении пользовательских требований к системе. Он должен стать напоминанием к тому, чтобы протестировать или проверить определенные вещи, которые могут быть затронуты при обсуждении. Но не нужно ориентироваться на него при определении объемов работ на основании user story.

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

    Структура

    Карточка составляется по стандартной схеме:
    • заголовок;
    • заказчик (actor, актер);
    • примечание;
    • цель.
    Она не отражает каждую мелочь, но вы должны детально обсудить пользовательскую историю в подходящее для команды время.

    Движение «от цели»
    Создание полезных программных систем трудоёмко, поэтому вы должны быть уверены, что решаете правильную проблему. В гибких методологиях применяют подход «от обратного» – что пытается получить пользователь системы на выходе? Если вы углубитесь в решение данного вопроса без достаточного понимания ваших пользователей, вы рискуете решить не ту проблему или найти решение, которое на самом деле не подходит вашим пользователям в реальной жизни. Поэтому самой важной частью user story является её цель.
    Цель
    Понимание целей поможет вам при решении вопроса о том, выполнили ли вы требования пользователя, иными словами, соответствует ли проделанная вами работа цели, стоящей перед пользователем?

    При написании user story с вашей командой разработчиков всегда начинайте с обдумывания и обсуждения целей вашего пользователя:

    • почему он хочет использовать эту систему?
    • чего он пытается достичь?
    • что заставило его искать ваш сервис?
    • в каких условиях он использует её: дома/на работе/по телефону/во время ухода за ребенком?
    • как часто он пользуется ей?
    Сюзанна и Джеймс Робертсон дают отличный совет по этому поводу в третьем издании книги «Mastering the Requirements Process».
    Заказчик (актер)
    Подробное описание заказчика (актера) поможет вам разбить процесс построения взаимодействия на логически связанные отрезки.
    Иногда заказчик будет пользователем вашей системы, в других случаях – администратором, техником или менеджером вашей организации.
    Убедитесь, что вы достаточно хорошо знаете своих пользователей на основании уже проделанной работы или проведенных ранее исследований. Если это не так, найдите время расширить свое понимание пользователей.
    Примечания
    Используйте их как блок передачи информации об основом типе взаимодействия, которое должно рассматриваться как часть пользовательского опыта. Помните, что карточка не должна отражать каждую деталь этого взаимодействия.
    Общение с пользователем напрямую
    Agile-команды предпочитают общение лицом к лицу подробной документации.

    Разговор лицом к лицу:

    • быстрее;
    • точнее, чем написанная документация;
    • позволяет разработчикам выстроить детальную модель целей пользователя, рабочих процессов, ограничений и многих вопросов, которые должны быть приняты во внимание при разработке ПО.
    Карточка – это всего лишь шаблон, гарантия, что разговор состоится в подходящее время. Воспользуйтесь карточками и несколькими вариантами для завязки разговора, чтобы оценить, сколько времени вам понадобится, чтобы завершить работу над составлением user story и поместить её в бэклог проекта.

    Когда начнется непосредственно разработка, пообщайтесь с пользователями или их представителями на предмет уточнения деталей требований. User story – это обобщение всех разговоров, набросков и диаграмм – не просто карточка. Необязательно записывать и хранить все разговоры, но нужно правильно интерпретировать их в автоматизированные тесты или работающий код.

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

    Критерии приемки для user story

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

    Истории «работают» только для agile-команд

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

    Откуда берутся пожелания пользователя

    Пожелания могут поступать из многих мест, но наиболее распространённые источники включают в себя:
    1. Воркшопы по написанию user story (чаще всего происходят на начальном этапе проекта); команда разработчиков и заинтересованные стороны собираются, чтобы обозначить пожелания пользователей.
    2. Интервью с реальными пользователями – в идеале, вы должны найти несколько пользователей, к которым команда разработчиков будет иметь постоянный доступ.
    3. Представители пользователя в составе вашей команды – это может быть сервис-менеджер или владелец продукта.
    4. Наблюдение – посмотрите, как реальные пользователи работают в вашей системе.
    Мы обсудили данный материал с рядом стартапов 4-го набора Акселератора ФРИИ [ближайший День открытых дверей Акселератора состоится в следующий четверг 12-го февраля 2015 г. ]:

    1. Используете ли вы user story? К каким источникам вы обращаетесь при составлении user story?

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


    Cкорее нет, чем да. Мы сначала разработали продукт и уже потом общались с пользователями. Да, мы вносили определённые изменения в контент, основываясь на отзывах, но никогда не ставили себе целью полностью основывать «таски» на user story.


    Термин user story мы никогда не использовали, но по факту отвечали на вопросы «Кто наш клиент?», «Как он использует наш сервис?» и «Почему ему нужен наш сервис?». Информацию получали из заявок клиентов, которые поступают к нам на сайт, и из личного общения с теми клиентами, кто заказал и кто не заказал наши услуги (услуги переводчиков).


    Александр Грицай, CEO, Forecast NOW!

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


    Ольга Книга, со-основатель и CEO, Merku.ru

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


    Согласны ли вы с утверждением, что методика user story подходит только для agile-команд?

    Александр Богомолов, экс-проектный менеджер, ПоискСтроек

    Не очень понял вопроса – чем мешает создание историй любой команде? Да – в проектах с жестким ТЗ истории не очень нужны на первый взгляд, но они помогают превратить такую разработку в более осмысленную. Помогают понять, зачем вообще реализуется та или иная функция.


    Леонид Тощев, технический директор, Datamonkey

    Сейчас понятие agile настолько размылось– мне кажется, нет таких компаний, которые не agile. Соответственно, user story могут использоваться всеми.


    Мария Теркина, со-основатель и CEO, Глоберлэнд

    Думаю, user story вписывается в agile-подход, но может использоваться и в других подходах к разработке.


    Мария Подоляк, со-основатель Gagarin Labs

    Подход к проектированию функционала продукта или интерфейса при помощи user stories – это же изначально не разработка Agile. Так называемые use cases (юс кейсы, примеры использования) существуют давно в практике разработки технических заданий. Подход такой же, немного был видоизменен под нужды Agile.

    В частных разговорах двух менеджеров продуктов можно услышать: «О, мы начали делать новый продукт для распознавания цвета кошек на фотографиях!» В ответ можно услышать: «Давай юс кейс». Мы разговариваем пользовательскими историями, ведь только так можно понять, какая «боль пользователя» решается, какой инновационный или кардинально иной способ решения проблемы используется, как это происходит на уровне функционала. История помогает «примерить» решение на себя.

    User stories используются и при проектировании интерфейсов. С этим подходом я познакомилась в Школе проектирования интерфейсов. Мы рисовали портрет пользователя (еще их иногда персонами называют), писали сценарий истории, дальше мы вырезали из бумаги интерфейс для того, чтобы разыграть сценарий или историю (так происходило тестирование гипотезы).

    В ноябре я попробовала применить такой же подход для проектирования контент-стратегии компании или продукта на семинаре в Школе интернет-маркетинга в Нижнем Новгороде. И прекрасно получилось. Так что подход применим ко всему в продукте: функционалу, интерфейсу, маркетингу и т.д. Заставляет думать «головой пользователя», понимать его проблему, а не опускаться в галлюцинации.
    Так что Agile ничего нового не сделал, просто адаптировал существующий подход под свои нужды. Добавить метки

    Последнее обновление: 31.10.2015

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

    Чтобы установить режим аутентификации на основе форм, в файл конфигурации web.config заносится следующие строки в секцию system.web :

    .................. ............

    Блок authentication определяет параметры аутентификации, в частности, блок forms задает в параметре loginUrl ресурс, на который будет направлен пользователь, который не был аутентифицирован.

    Для рассмотрения аутентификации форм возьмем веб-приложение по шаблону Internet Application, который содержит всю базовую инфраструктуру авторизации пользователей.

    Это приложение в файле конфигурации по умолчанию содержит подключение к бд. При желании мы можем сами создать свою БД и переопределить подключение. А база данных для этого подключения, создаваемая при первом обращении пользователя в папке App_Data, хранит все данные об учетных записях, зарегистрированных в системе. Из этой же базы будут браться данные для подтверждения аутентификации пользователя.

    Вся логика аутентификации заключена в методах Login контроллера AccountController, которые осуществляют вход пользователя в систему. В зависимости от используемой версии Visual Studio. Так, в Visual Studio 2010 метод Login выглядит следующим образом:

    Public ActionResult Login(string returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } public ActionResult Login(LoginModel model, string returnUrl) { if (ModelState.IsValid) { if (Membership.ValidateUser(model.UserName, model.Password)) { FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe); if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } else { return RedirectToAction("Index", "Home"); } } else { ModelState.AddModelError("", "The user name or password provided is incorrect."); } } return View(model); }

    Тогда как в проекте, созданном в Visual Studio 2012 этот метод будет выглядеть следующим образом:

    Public ActionResult Login(string returnUrl) { ViewBag.ReturnUrl = returnUrl; return View(); } public ActionResult Login(LoginModel model, string returnUrl) { if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe)) { return RedirectToLocal(returnUrl); } ModelState.AddModelError("", "The user name or password provided is incorrect."); return View(model); }

    Несмотря на некоторые различия фактически они производят те же действия в виде аутентификации форм. Сама аутентификация разделяется на два действия:

      Валидация пользователя : в первом случае это делает метод Membership.ValidateUser . И если пользователь находится в нашей базе данных, то далее приложение переходит ко второму этапу.

      Второй этап представляет создание аутентификационного тикета - то есть некоторого билета безопасности, по которому веб-приложение будет опознавать пользователя. Этот тикет приложение устанавливает для браузера в виде куки-набора по имени .AUTHPATH с помощью метода FormsAuthentication.SetAuthCookie

    В этом и по сути и состоит сущность аутентификации форм.

    Несмотря на то, что во втором случае - в коде для Visual Studio 2012 не используются явно эти методы, а вместо них применяется только один - WebSecurity.Login , но фактически он скрывает ту же функциональность в своей внутренней реализации:

    Public static bool Login(string userName, string password, bool persistCookie = false) { VerifyProvider(); bool success = Membership.ValidateUser(userName, password); if (success) { FormsAuthentication.SetAuthCookie(userName, persistCookie); } return success; }

    Создание куки-набора

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

    Как вы видите, первый куки-набор .AUTHPATH , который содержит данные авторизованного пользователя, совершенно нечитаем, что уменьшает вероятность взломов простой подделкой занчений куки.

    Второй куки-набор - __RequestVerificationToken - представляет специальный куки-набор, который препятствует взлому в виде CSRF-атак.

    Настройка аутентификации

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

      coockieless : определяет, применяются ли куки-наборы и как они используются. Может принимать следующие значения: UseCookies (определяет, что куки-наборы будут использоваться всегда вне зависимости от устройства), UseUri (куки-наборы никогда не используются), AutoDetect (если устройство поддерживает куки-наборы, то они используются, в противном случае они не применяются, при этом проводится тестирование, определяющее, включена ли поддержка), UseDeviceProfile (используется по умолчанию) (если устройство поддерживает куки-наборы, то они используются, в противном случае они не применяются, в отличие от предыдущего случая тестирование не проводится)

      defaultUrl : определяет путь, по которому осуществляется переход после авторизации

      domain : определяет куки-наборы для всего домена. Благодаря этому мы можем использовать одни и те же куки-набры для главного домена и его субдоменов. По умолчанию имеет значение в качестве пустой строки

      loginUrl : адрес для аутентификации пользователя. Значение по умолчанию - "~/Account/Login"

      name : задает имя для куки-набора. Значение по умолчанию - ".ASPXAUTH"

      path : задает путь для куки-наборов. Значение по умолчанию - "/"

      requireSSL : определяет, требуется ли SSL-соединение для передачи куки-наборов. Значение по умолчанию false

      timeout : определяет срок действия куков в минутах

    Например, мы можем задать следующее определение аутентификации форм:

    .................. ............