Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET MVC ViewModel mapping with custom formatting

The project I'm working on has a large number of currency properties in the domain model and I'm needing for format these as $#,###.## for transmitting to and from the view. I've had a view thoughts as to different approaches which could be used. One approach could be to format the values explicitly inside the view, as in "Pattern 1" from Steve Michelotti :

...but this starts violating DRY principle very quickly.

The preferred approach appears to be to do the formatting during the mapping between DomainModel and a ViewModel (as per ASP.NET MVC in Action section 4.4.1 and "Pattern 3"). Using AutoMapper, this will result in some code like the following:

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

Using IValueFormatter this way works great. Now, how to map it back from the DomainModel to ViewModel? I've tried using a custom 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);
 }
}

And then mapped it with:

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

Which will satisfy this test:

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

... But I'm feeling that I shouldn't need to explicitly define which property it is being mapped from with FromMember after doing ResolveUsing since the properties have the same name - is there a better way to define this mapping? As I mentioned, there are a good number of properties with currency values that will need to be mapped in this fashion.

That being said - is there a way I could have these mappings automatically resolved by defining some rule globally? The ViewModel properties are already decorated with DataAnnotation attributes [DataType(DataType.Currency)] for validation, so I was hoping that I could define some rule that does:

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

... so that I can minimize the amount of boilerplate setup for each of the object types.

I'm also interested in hearing of any alternate strategies for accomplishing custom formatting to-and-from the View.


From ASP.NET MVC in Action:

At first we might be tempted to pass this simple object straight to the view, but the DateTime? properties [in the Model] will cause problems. For instance, we need to choose a formatting for them such as ToShortDateString() or ToString(). The view would be forced to do null checking to keep the screen from blowing up when the properties are null. Views are difficult to unit test, so we want to keep them as thin as possible. Because the output of a view is a string passed to the response stream, we’ll only use objects that are stringfriendly; that is, objects that will never fail when ToString() is called on them. The ConferenceForm view model object is an example of this. Notice in listing 4.14 that all of the properties are strings. We’ll have the dates properly formatted before this view model object is placed in view data. This way, the view need not consider the object, and it can format the information properly.

like image 429
James Kolpack Avatar asked Dec 31 '09 20:12

James Kolpack


3 Answers

Have you considered using an extension method to format money?

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


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

Since this is clearly a view-related (not model-related) issue, I'd try to keep it in the view if at all possible. This basically moves it to an extension method on decimal, but the usage is in the view. You could also do an HtmlHelper extension:

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


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

If you liked that style better. It is somewhat more View-related as it's an HtmlHelper extension.

like image 163
tvanfosson Avatar answered Nov 16 '22 23:11

tvanfosson


Have you considered putting a DisplayFormat on your ViewModel? That is what I use and it's quick and simple.

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


View :
    @Html.DisplayFor(m => m.CurrencyProperty)
like image 31
James Sullivan Avatar answered Nov 16 '22 22:11

James Sullivan


A custom TypeConverter is what you're looking for:

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

Then create the converter:

public class MoneyToDecimalConverter : TypeConverter<string, decimal>
{
   protected override decimal ConvertCore(string source)
   {
      // magic here to convert from string to decimal
   }
}
like image 2
Jimmy Bogard Avatar answered Nov 16 '22 23:11

Jimmy Bogard