В данный момент я работаю над небольшим проектом на asp net mvc. Сроки достаточно короткие, результат нужен как можно скорее, вот мы и начали набрасывать функционал и натягивать красивый интерфейс (говнокодить). Время шло, смотреть на это становилось все тяжелее, вносить правки все дольше, и вот пока заказчик проводит тестирование приложения, есть время подумать, что можно сделать с этой простыней кода (раньше подумать было лень и некогда).
Начинаю думать, как можно решить проблему давно надоевших карточек редактирования документов. В системе есть несколько типов документов, поля в которых могут редактировать пользователи, в зависимости от роли и статуса документа.
Приведу пример того, что имеем на данном этапе:
- несколько представлений для создания документа, редактирования, просмотра (просмотр по сути тоже редактирование но все поля, в большинстве случаев, только для чтения);
- код представления, состоящий из ветвлений с проверкой на статусы и роли, иногда только на роли, так как бизнес логика тоже не остается в стороне и принимает решение о том, что в некоторых случаях нужно показывать карточку только на чтение.
Пример кода:
@if (User.IsInRole(RolesEnum.Executor.GetDescription()))
{
@Html.TextBoxFor(model => model.RegNumber)
}
else
{
@Html.TextBoxFor(model => model.RegNumber, new { @readonly = "readonly" })
}
Собственно, это еще не все проблемы, так как в некоторых местах для того, чтобы решить задачу быстрее, кто-то оставляет редактируемые поля в представлении, которое по определению должно было быть только на чтение. Все это усугубляется частичными представлениями, которые живут своей жизнью и все их делали по разному, а местами используются шаблоны отображения.
Пытаемся улучшить.
Решаю, что можно задание правил отображения вынести в модель данных, а способом задания взять установку правил через атрибуты.
Примерный класс атрибута и правила:
/// <summary>
/// Правило отображения поля для роли пользователя
/// </summary>
public class PropertyPermission
{
public RolesEnum Role { get; set; }
public int[] Satuses { get; set; }
public bool IsReadOnly { get; set; }
public PropertyPermission(RolesEnum role, int[] statuses)
{
this.Role = role;
this.Satuses = statuses;
}
}
/// <summary>
/// Атрибут задания правил отображения полей формы
/// </summary>
public class PropertyPermissionAttribute : Attribute
{
public PropertyPermission[] Permissons { get; private set; }
public PropertyPermissionAttribute(PropertyPermission[] permissons)
{
this.Permissons = permissons;
}
public PropertyPermissionAttribute(RolesEnum role, params int[] statuses)
{
this.Permissons = new PropertyPermission[] { new PropertyPermission(role, statuses) };
}
}
Тут я считаю, что достаточно задавать правила для ролей и статусов, которым будет доступно редактирование полей, а всем остальным оставлять режим только для чтения. Так, в нашем приложении одни поля редактируются в основном только одной ролью и на определенном статусе документа, в связи с этим придется у свойства модели прописывать по одному правилу в большинстве случаев. Для создания атрибута можно вызвать конструктор и передать ему роль пользователя и набор статусов, на которых будет отключен режим для чтения.
Чтобы сделать проверку правил для объектов разных классов, нужен полиморфизм (все документы у нас никак не связанные классы, пока что), тут можно объявить интерфейс и реализовывать его в классах документов для проверки соответствия статусов и ролей документов, но так как пока логика отображения во всех документах зависит только от ролей и статусов, а свойство статуса у нас есть во всех документах, то делаем базовый класс и задаем атрибут для свойства модели:
public class BaseDocumentModel
{
[DisplayName("ID")]
public int ID { get; set; }
[DisplayName("Статус")]
public int? Status { get; set; }
[PropertyPermission(RolesEnum.Executor, (int)StatusComplaint.ToWork)]
[DisplayName("Входящий №")]
public string RegNumber { get; set; }
}
В данном случае определяем, что поле RegNumber будет доступно у нас пользователю с ролью «Executor» на статусе «ToWork». Осталось написать хелпер, чтобы наши правила ожили. Хелпер будем использовать для отображения полей редактирования:
public static class ProertyExtensions
{
public static MvcHtmlString RegistratorEditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
return RegistratorEditorFor(html, expression, null);
}
public static MvcHtmlString RegistratorEditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes)
{
return RegistratorEditorFor(html, expression, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString RegistratorEditorFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes)
{
var member = (expression.Body as MemberExpression).Member;
MvcHtmlString result = html.EditorFor(expression);
if (html.ViewData.Model is BaseDocumentModel)
{
if (IsReadOnly(member as PropertyInfo, html.ViewData.Model as BaseDocumentModel))
{
result = MvcHtmlString.Create(result.ToString().Replace("/>", "readonly = "readonly" />"));
}
}
return result;
}
static bool IsReadOnly(System.Reflection.PropertyInfo property, BaseDocumentModel document)
{
var attr = property.GetCustomAttributes(typeof(PropertyPermissionAttribute), false);
bool result = true;
foreach (PropertyPermissionAttribute a in attr)
{
foreach (var p in a.Permissons)
{
if (HttpContext.Current.User.IsInRole(p.Role.GetDescription()) &&
((document.Status != null && p.Satuses.Contains((int)document.Status)) || p.Satuses.Length == 0))
{
result = p.IsReadOnly;
}
}
}
return result;
}
}
Логику проверки правил IsReadOnly оставил в этом же классе, он проверяет атрибуты поля и выносит свое решение. Сам хелпер для вывода поля использует EditorFor и в случае необходимости подправляет выходной html, чтобы сделать поле readonly.
Всё, остается в представлении вызвать наш метод:
@Html.RegistratorEditorFor(model => model.RegNumber)
Вот так я пытался решать свои проблемы. Хотелось бы узнать, в чем я, возможно, не прав.
Автор: d3en9