Как "сушить" атрибуты С# в моделях и ViewModels?

Этот вопрос был вдохновлен моей борьбой с ASP.NET MVC, но я думаю, что это относится и к другим ситуациям.

Скажем, у меня есть ORM-сгенерированная модель и две ViewModels (одна для "подробного" представления и одна для "редактирования" ):

Model

public class FooModel // ORM generated
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string EmailAddress { get; set; }
    public int Age { get; set; }
    public int CategoryId { get; set; }
}

Показать ViewModel

public class FooDisplayViewModel // use for "details" view
{
    [DisplayName("ID Number")]
    public int Id { get; set; }

    [DisplayName("First Name")]
    public string FirstName { get; set; }

    [DisplayName("Last Name")]
    public string LastName { get; set; }

    [DisplayName("Email Address")]
    [DataType("EmailAddress")]
    public string EmailAddress { get; set; }

    public int Age { get; set; }

    [DisplayName("Category")]
    public string CategoryName { get; set; }
}

Изменить ViewModel

public class FooEditViewModel // use for "edit" view
{
    [DisplayName("First Name")] // not DRY
    public string FirstName { get; set; }

    [DisplayName("Last Name")] // not DRY
    public string LastName { get; set; }

    [DisplayName("Email Address")] // not DRY
    [DataType("EmailAddress")] // not DRY
    public string EmailAddress { get; set; }

    public int Age { get; set; }

    [DisplayName("Category")] // not DRY
    public SelectList Categories { get; set; }
}

Обратите внимание, что атрибуты в ViewModels не являются DRY - много информации повторяется. Теперь представьте, что этот сценарий умножен на 10 или 100, и вы можете видеть, что он может быстро стать довольно утомительным и подверженным ошибкам, чтобы обеспечить согласованность между ViewModels (и, следовательно, в представлениях).

Как я могу "сушить" этот код?

Прежде чем ответить: "Просто поместите все атрибуты на FooModel", я пробовал это, но это не сработало, потому что мне нужно, чтобы мои ViewModels "плоские". Другими словами, я не могу просто составить каждый ViewModel с помощью модели - мне нужно, чтобы моя ViewModel имела только свойства (и атрибуты), которые должны быть использованы View, и View не может зарываться в под-свойства для получить значения.

Обновление

Ответ LukLed предполагает использование наследования. Это определенно уменьшает количество кода, отличного от DRY, но не устраняет его. Обратите внимание, что в моем примере выше атрибут DisplayName для свойства Category должен быть записан дважды, потому что тип данных свойства отличается между отображением и редактированием ViewModels. Это не будет большой проблемой в небольших масштабах, но по мере увеличения масштабов и сложности проекта (представьте себе, что больше свойств, больше атрибутов на свойство, больше просмотров на модель), по-прежнему существует потенциальная возможность для "повторять себя" достаточно. Возможно, я слишком сильно забираю DRY, но у меня все еще есть все мои "дружественные имена", типы данных, правила проверки и т.д., Которые были напечатаны только один раз.

Ответы

Ответ 1

Я предполагаю, что вы делаете это, чтобы воспользоваться HtmlHelpers EditorFor и DisplayFor, и не хотите накладных расходов, торжественно объявляя то же самое 4000 раз в течение всего приложения.

Самый простой способ DRY это реализовать собственный ModelMetadataProvider. ModelMetadataProvider - это то, что читает эти атрибуты и представляет их в помощники шаблонов. MVC2 уже предоставляет реализацию DataAnnotationsModelMetadataProvider, чтобы добиться того, чтобы унаследовать ее, что делает вещи очень легкими.

Чтобы вы начали здесь, это простой пример, который разбивает имена свойств camelcased на пробелы, FirstName = > Имя:

public class ConventionModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
    protected override ModelMetadata CreateMetadata(IEnumerable<Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
    {
        var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);

        HumanizePropertyNamesAsDisplayName(metadata);

        if (metadata.DisplayName.ToUpper() == "ID")
            metadata.DisplayName = "Id Number";

        return metadata;
    }

    private void HumanizePropertyNamesAsDisplayName(ModelMetadata metadata)
    {
        metadata.DisplayName = HumanizeCamel((metadata.DisplayName ?? metadata.PropertyName));
    }

    public static string HumanizeCamel(string camelCasedString)
    {
        if (camelCasedString == null)
            return "";

        StringBuilder sb = new StringBuilder();

        char last = char.MinValue;
        foreach (char c in camelCasedString)
        {
            if (char.IsLower(last) && char.IsUpper(c))
            {
                sb.Append(' ');
            }
            sb.Append(c);
            last = c;
        }
        return sb.ToString();
    }
}

Затем все, что вам нужно сделать, это зарегистрировать его как добавление собственного пользовательского ViewEngine или ControllerFactory внутри приложения Global.asax. Начать:

ModelMetadataProviders.Current = new ConventionModelMetadataProvider();

Теперь просто для того, чтобы показать вам, что я не обманываю, это модель представления, которую я использую, чтобы получить тот же HtmlHelper. * Для опыта, как ваш украшенный ViewModel:

    public class FooDisplayViewModel // use for "details" view
    {
        public int Id { get; set; }

        public string FirstName { get; set; }

        public string LastName { get; set; }

        [DataType("EmailAddress")]
        public string EmailAddress { get; set; }

        public int Age { get; set; }

        [DisplayName("Category")]
        public string CategoryName { get; set; }
    }

Ответ 2

Объявить BaseModel, наследовать и добавлять другие свойства:

public class BaseFooViewModel
{
    [DisplayName("First Name")]
    public string FirstName { get; set; }

    [DisplayName("Last Name")]
    public string LastName { get; set; }

    [DisplayName("Email Address")]
    [DataType("EmailAddress")]
    public string EmailAddress { get; set; }
}

public class FooDisplayViewModel : BaseFooViewModel
{
    [DisplayName("ID Number")]
    public int Id { get; set; }
}

public class FooEditViewModel : BaseFooViewModel

ИЗМЕНИТЬ

О категориях. Не следует ли редактировать модель вида public string CategoryName { get; set; } и public List<string> Categories { get; set; } вместо SelectList? Таким образом, вы можете разместить public string CategoryName { get; set; } в базовом классе и сохранить DRY. Изменить вид увеличивает класс, добавив List<string>.

Ответ 3

Как сказал LukLed, вы можете создать базовый класс, из которого производятся модели View и Edit, или вы также можете просто получить одну модель обзора из другой. Во многих приложениях модель редактирования в основном такая же, как View plus, и некоторые дополнительные материалы (например, списки выбора), поэтому имеет смысл вывести модель Edit из модели View.

Или, если вас беспокоит "взрыв класса", вы можете использовать одну и ту же модель представления для обоих и передать дополнительный материал (например, SelectLists) через ViewData. Я не рекомендую этот подход, потому что я смущаюсь передать какое-то состояние через модель и другое состояние через ViewData, но это вариант.

Другим вариантом было бы просто охватить отдельные модели. Я все о логике DRY, но меня меньше беспокоят несколько избыточных свойств в моих DTO (особенно в проектах, использующих генерацию кода для генерации 90% моделей просмотра для меня).

Ответ 4

Первое, что я заметил - у вас есть 2 модели взглядов. Для получения более подробной информации см. Мой ответ здесь.

Другие вещи, о которых идет речь, уже упоминаются (классический подход к применению DRY - наследование и соглашения).


Думаю, я был слишком расплывчатым. Моя идея - создать модель представления для модели домена, а затем - объединить их в моделях просмотра, которые соответствуют определенному виду. В вашем случае: = >

public class FooViewModel {
  strange attributes everywhere tralalala
  firstname,lastname,bar,fizz,buzz
}

public class FooDetailsViewModel {
   public FooViewModel Foo {get;set;}
   some additional bull**** if needed
}

public class FooEditViewModel {
   public FooViewModel Foo {get;set;}
   some additional bull**** if needed
}

Это позволяет нам создавать более сложные модели просмотра (для каждого представления) тоже = >

public class ComplexViewModel {
    public PaginationInfo Pagination {get;set;}
    public FooViewModel Foo {get;set;}
    public BarViewModel Bar {get;set;}
    public HttpContext lol {get;set;}
}

Вы можете найти полезный этот вопрос моего.

hmm... получается, я действительно предлагал создать 3 модели взглядов. Во всяком случае, этот фрагмент кода a отражает мой подход.

Другой совет - я бы пошел с механизмом фильтра и соглашения (например, по типу), который заполняет viewdata с помощью необходимого selectList (mvc framework может автоматически связывать selectList с viewData по имени или чем-то).

И еще один совет: если вы используете AutoMapper для управления своей моделью просмотра, у нее есть приятная функция - он может сгладить граф объектов. Таким образом, вы можете создать модель представления (которая для каждого представления), которая непосредственно имеет реквизиты модели представления (которая является для каждой модели домена) независимо от того, насколько глубоко вы хотите идти (Haack сказал это хорошо).

Ответ 5

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

Если бы я сказал своей команде, они должны были бы создать новую модель для каждой маленькой перестановки одних и тех же данных (и впоследствии написать для них определения automapper), они бы восстали и линчевали меня. Я бы предпочел моделировать метаданные, которые были слишком осведомлены о степени использования. Например, создание атрибута, требуемого свойствами, вступает в силу только в сценарии "Добавить" (Model == null). В частности, поскольку я бы даже не написал два представления для обработки добавления/редактирования. У меня было бы одно представление для обработки обоих из них, и если бы у меня возникли разные классы моделей, я столкнулся бы с проблемой с объявлением родительского класса.. бит... ViewPage.