Попытка сделать логичное поведение форм редактирования в asp mvc

в 8:52, , рубрики: .net, Программирование

В данный момент я работаю над небольшим проектом на 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js