Сопоставление ASP.NET MVC ViewModel с пользовательским форматированием

Проект, над которым я работаю, имеет большое количество свойств валюты в модели предметной области, и мне нужно отформатировать их как $#,###.## для передачи в представление и из него. У меня были мысли о различных подходах, которые можно было бы использовать. Одним из подходов может быть явное форматирование значений внутри представления, как в "Шаблон 1" от Стива Мичелотти :

... но это очень быстро начинает нарушать принцип DRY.

По-видимому, предпочтительным подходом является выполнение форматирования во время сопоставления между DomainModel и ViewModel (согласно ASP.NET MVC в Действие, раздел 4.4.1 и "Шаблон 3"). Используя AutoMapper, это приведет к следующему коду:

[TestFixture]
public class ViewModelTests
{
 [Test]
 public void DomainModelMapsToViewModel()
 {
  var domainModel = new DomainModel {CurrencyProperty = 19.95m};

  var viewModel = new ViewModel(domainModel);

  Assert.That(viewModel.CurrencyProperty, Is.EqualTo("$19.95"));
 }
}

public class DomainModel
{
 public decimal CurrencyProperty { get; set; }
}

public class ViewModel
{
 ///<summary>Currency Property - formatted as $#,###.##</summary>
 public string CurrencyProperty { get; set; }

 ///<summary>Setup mapping between domain and view model</summary>
 static ViewModel()
 {
  // map dm to vm
  Mapper.CreateMap<DomainModel, ViewModel>()
   .ForMember(vm => vm.CurrencyProperty, mc => mc.AddFormatter<CurrencyFormatter>());
 }

 /// <summary> Creates the view model from the domain model.</summary>
 public ViewModel(DomainModel domainModel)
 {
  Mapper.Map(domainModel, this);
 }

 public ViewModel() { }
}

public class CurrencyFormatter : IValueFormatter
{
 ///<summary>Formats source value as currency</summary>
 public string FormatValue(ResolutionContext context)
 {
  return string.Format(CultureInfo.CurrentCulture, "{0:c}", context.SourceValue);
 }
}

Использование IValueFormatter таким образом прекрасно работает. Теперь, как отобразить его обратно из DomainModel в ViewModel? Я пытался использовать пользовательский class CurrencyResolver : ValueResolver<string,decimal>

public class CurrencyResolver : ValueResolver<string, decimal>
{
 ///<summary>Parses source value as currency</summary>
 protected override decimal ResolveCore(string source)
 {
  return decimal.Parse(source, NumberStyles.Currency, CultureInfo.CurrentCulture);
 }
}

А затем сопоставил его с:

  // from vm to dm
  Mapper.CreateMap<ViewModel, DomainModel>()
   .ForMember(dm => dm.CurrencyProperty, 
    mc => mc
     .ResolveUsing<CurrencyResolver>()
     .FromMember(vm => vm.CurrencyProperty));

Что удовлетворит этому тесту:

 ///<summary>DomainModel maps to ViewModel</summary>
 [Test]
 public void ViewModelMapsToDomainModel()
 {
  var viewModel = new ViewModel {CurrencyProperty = "$19.95"};

  var domainModel = new DomainModel();

  Mapper.Map(viewModel, domainModel);

  Assert.That(domainModel.CurrencyProperty, Is.EqualTo(19.95m));
 }

... Но я чувствую, что мне не нужно явно определять, какое свойство сопоставляется с FromMember после выполнения ResolveUsing, поскольку свойства имеют одно и то же имя - есть ли лучший способ определить это сопоставление? Как я уже упоминал, существует большое количество свойств со значениями в валюте, которые необходимо отобразить таким образом.

При этом - есть ли способ, которым я мог бы автоматически разрешить эти сопоставления, определив какое-то правило глобально? Свойства ViewModel уже украшены DataAnnotation атрибутами [DataType(DataType.Currency)] для проверки, поэтому я надеялся, что смогу определить какое-то правило, которое делает:

if (destinationProperty.PropertyInfo.Attributes.Has(DataType(DataType.Currency)) 
  then Mapper.Use<CurrencyFormatter>()
if (sourceProperty.PropertyInfo.Attributes.Has(DataType(DataType.Currency)) 
  then Mapper.Use<CurrencyResolver>()

... так что я могу свести к минимуму количество стандартных настроек для каждого из типов объектов.

Мне также интересно узнать о любых альтернативных стратегиях выполнения пользовательского форматирования в представлении и обратно.


Из ASP.NET MVC в действии:

Сначала у нас может возникнуть соблазн передать этот простой объект прямо в представление, но метод DateTime? свойства [в модели] вызовут проблемы. Например, нам нужно выбрать для них форматирование, такое как ToShortDateString() или ToString(). Представление будет вынуждено выполнять проверку на нуль, чтобы экран не взрывался, когда свойства равны нулю. Представления сложно тестировать, поэтому мы хотим, чтобы они были как можно тоньше. Поскольку вывод представления — это строка, передаваемая в поток ответов, мы будем использовать только те объекты, которые совместимы со строками; то есть объекты, которые никогда не перестанут работать при вызове ToString(). Примером этого является объект модели представления ConferenceForm. Обратите внимание, что в листинге 4.14 все свойства являются строками. У нас будут правильно отформатированные даты, прежде чем этот объект модели представления будет помещен в данные представления. Таким образом, представлению не нужно учитывать объект, и оно может правильно форматировать информацию.


person James Kolpack    schedule 31.12.2009    source источник
comment
‹%= string.Format({0:c}, Model.CurrencyProperty) %› выглядит красиво. Может я просто привык...   -  person Mathias F    schedule 01.01.2010


Ответы (3)


Пользовательский TypeConverter - это то, что вам нужно:

Mapper.CreateMap<string, decimal>().ConvertUsing<MoneyToDecimalConverter>();

Затем создайте конвертер:

public class MoneyToDecimalConverter : TypeConverter<string, decimal>
{
   protected override decimal ConvertCore(string source)
   {
      // magic here to convert from string to decimal
   }
}
person Jimmy Bogard    schedule 05.01.2010
comment
Спасибо за ответ Джимми. Я рассматривал использование TypeConverter‹TSource,TDest›, но проблема, которую я обнаружил в моем случае, заключается в том, что он будет применяться ко всем отображениям от десятичного до строкового. К сожалению, только некоторые из десятичных свойств являются валютой. Я подумал о том, чтобы, возможно, сделать обертку вокруг десятичного числа - (класс CurrencyDecimal : Decimal), но тогда я мог бы так же легко добавить неявные операции приведения между типом и строкой. Что мне действительно хотелось бы иметь, так это что-то вроде TypeConverter, которое может проверять атрибуты свойств - возможно, я когда-нибудь напишу это, если его не существует. - person James Kolpack; 08.01.2010

Рассматривали ли вы использование метода расширения для форматирования денег?

public static string ToMoney( this decimal source )
{
    return string.Format( "{0:c}", source );
}


<%= Model.CurrencyProperty.ToMoney() %>

Поскольку это явно проблема, связанная с представлением (а не с моделью), я бы постарался сохранить ее в представлении, если это вообще возможно. Это в основном перемещает его в метод расширения для десятичного числа, но использование находится в представлении. Вы также можете сделать расширение HtmlHelper:

public static string FormatMoney( this HtmlHelper helper, decimal amount )
{
    return string.Format( "{0:c}", amount );
}


<%= Html.FormatMoney( Model.CurrencyProperty ) %>

Если вам больше понравился этот стиль. Это несколько больше связано с представлением, поскольку это расширение HtmlHelper.

person tvanfosson    schedule 31.12.2009
comment
Да, это определенно имеет больше смысла, чем выполнение string.Format() каждый раз в представлении. Проблема, с которой я сталкиваюсь, заключается в том, что ViewModel часто будет отображаться клиенту для использования javascript - например, trycatchfail.com/blog/post/2009/12/22/ или во время запросов AJAX. В этих случаях мне нужно было бы выполнить форматирование на клиентском уровне, что менее чем желательно - на самом деле, я чувствую, что приложил бы кучу дополнительных усилий, чтобы все проблемы форматирования/анализа были разделены на одном уровне. . - person James Kolpack; 01.01.2010
comment
Меня также утомляет то, что этот MVC имеет надежный механизм для анализа входящих запросов через пользовательскую привязку модели, но не обеспечивает такой же опыт форматирования во время рендеринга представления. - person James Kolpack; 01.01.2010
comment
У меня нет проблем с представлением или клиентом, который принимает решения о форматировании. Как правило, я предпочитаю, чтобы контроллер или модель выбирали, как представлять данные, что, похоже, нарушает принцип разделения интересов. Что, если разные клиенты/представления (например, мобильные и веб-приложения) хотят отображать его по-разному? - person tvanfosson; 01.01.2010

Рассматривали ли вы размещение DisplayFormat в своей ViewModel? Это то, что я использую, и это быстро и просто.

ViewModel :
    [DisplayFormat(DataFormatString = "{0:c}", ApplyFormatInEditMode = true)]
    public decimal CurrencyProperty { get; set; }


View :
    @Html.DisplayFor(m => m.CurrencyProperty)
person James Sullivan    schedule 09.09.2011