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

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

Допустим, у меня есть модель, сгенерированная ORM, и две модели ViewModel (одна для представления "детали" и одна для представления "редактирования"):

модель

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 не являются СУХОЙ - много информации повторяется. Теперь представьте, что этот сценарий умножен на 10 или 100, и вы можете видеть, что он может быстро стать довольно утомительным и подверженным ошибкам для обеспечения согласованности между ViewModels (и, следовательно, через Views).

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

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

Обновить

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

5 ответов

Решение

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

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

Для начала приведу простой пример, который разбивает имена свойств на верблюдах на пробелы, FirstName => First Name:

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; }
    }

Объявите 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; } в базовом классе и держать СУХОЙ. Редактировать представление улучшает класс, добавляя List<string>,

Первое, что я заметил - у вас есть 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;}
}

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам