Как «высушить» атрибуты C # в моделях и моделях представления? - PullRequest
14 голосов
/ 16 февраля 2010

Этот вопрос был вдохновлен моей борьбой с 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 (и, следовательно, между представлениями).

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

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

Обновление

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

Ответы [ 5 ]

7 голосов
/ 16 февраля 2010

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

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

Для начала приведу простой пример, который разбивает имена свойств на верблюдах на пробелы, 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.

    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; }
    }
7 голосов
/ 16 февраля 2010

Объявите 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

EDIT

О категориях. Разве редактируемая модель представления не должна иметь public string CategoryName { get; set; } и public List<string> Categories { get; set; } вместо SelectList? Таким образом, вы можете поместить public string CategoryName { get; set; } в базовый класс и сохранить СУХОЙ. Редактировать представление улучшает класс, добавив List<string>.

1 голос
/ 16 февраля 2010

Первое, что я заметил - у вас есть 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 для управления моделью представления, у него есть хорошая функция - он может сгладить граф объектов . Следовательно, вы можете создать модель представления (для каждого вида), которая напрямую имеет реквизиты модели представления (которая для модели предметной области), независимо от того, насколько глубоко вы хотите зайти (Хаак сказал все в порядке).

1 голос
/ 16 февраля 2010

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

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

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

0 голосов
/ 19 апреля 2010

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

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

...