2009-12-31 5 views
6

Le projet sur lequel je travaille comporte un grand nombre de propriétés de devise dans le modèle de domaine et je dois les formater en tant que $#,###.## pour la transmission vers et depuis la vue. J'ai eu un avis sur les différentes approches qui pourraient être utilisées. Une approche pourrait être de formater les valeurs explicitement dans la vue, comme dans "Pattern 1" from Steve Michelotti:Mappage ASP.NET MVC ViewModel avec mise en forme personnalisée

... mais cela commence DRY principle violer très rapidement.

L'approche préférée semble être de faire la mise en forme pendant le mappage entre DomainModel et un ViewModel (selon ASP.NET MVC in Action section 4.4.1 et "Pattern 3"). En utilisant AutoMapper, cela se traduira par un code comme suit:

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

utilisant IValueFormatter cette façon fonctionne très bien. Maintenant, comment le mapper du DomainModel à ViewModel? Je l'ai essayé d'utiliser une coutume 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); 
} 
} 

Et puis cartographié avec:

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

qui satisfera ce 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)); 
} 

... Mais je me sens que Je ne devrais pas besoin de définir explicitement la propriété à partir de laquelle FromMember est mappé après avoir effectué ResolveUsing car les propriétés ont le même nom - y a-t-il un meilleur façon de définir cette cartographie? Comme je l'ai mentionné, il y a un bon nombre de propriétés avec des valeurs monétaires qui devront être cartographiées de cette façon. Ceci étant dit, y a-t-il un moyen de résoudre automatiquement ces mappages en définissant une règle globalement? Les propriétés ViewModel sont déjà décorées avec DataAnnotation attributs [DataType(DataType.Currency)] pour la validation, donc j'espérais que je pourrais définir une règle qui fait:

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

... pour que je puisse minimiser la quantité de configuration boilerplate pour chacun des types d'objets.

Je suis également intéressé par toute autre stratégie pour réaliser un formatage personnalisé à partir de la vue.


De ASP.NET MVC in Action:

Au début, on pourrait être tenté de passer ce simple objet droit à la vue , mais le DateTime? propriétés [dans le modèle] entraînera des problèmes. Par exemple, nous devons choisir une mise en forme pour eux comme ToShortDateString() ou ToString(). La vue serait obligée de faire la null vérification pour garder l'écran de explose lorsque les propriétés sont null. Les vues sont difficiles à l'unité test , donc nous voulons les garder aussi minces que possible.Étant donné que la sortie d'une vue est une chaîne transmise au flux de réponse , nous utiliserons uniquement des objets stringfriendly; est, objets qui n'échoueront jamais lorsque ToString() leur est appelée. L'objet modèle de vue ConferenceForm est un exemple de . Notez dans la liste 4.14 que toutes les propriétés sont des chaînes. Nous aurons les dates correctement formaté avant que ce modèle de vue objet est placé dans les données de vue. Cette façon , la vue ne doit pas prendre en compte l'objet , et il peut formater les informations correctement.

+0

<% = chaîne.Format ("{0: c}", Model.CurrencyProperty)%> me semble bien. Peut-être que je suis juste habitué ... –

Répondre

2

Un TypeConverter personnalisé est ce que vous cherchez:

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

Ensuite, créez le convertisseur:

public class MoneyToDecimalConverter : TypeConverter<string, decimal> 
{ 
    protected override decimal ConvertCore(string source) 
    { 
     // magic here to convert from string to decimal 
    } 
} 
+0

Merci pour la réponse Jimmy. J'ai regardé en utilisant TypeConverter , mais le problème que j'ai trouvé dans mon cas est qu'il sera appliqué à * all * mappings de décimal en chaîne. Malheureusement, seules certaines propriétés décimales sont des devises. J'ai pensé faire peut-être un wrapper autour de la décimale - (class CurrencyDecimal: Decimal), mais ensuite je pourrais tout aussi bien ajouter des opérations de cast implicites entre le type et la chaîne. Ce que j'aimerais vraiment avoir, c'est quelque chose comme TypeConverter qui peut examiner les attributs de la propriété - peut-être que j'essaierai d'écrire cela un jour s'il n'existe pas. –

6

Avez-vous envisagé d'utiliser une méthode d'extension pour formater de l'argent?

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


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

Puisque c'est clairement une vue liée question (non liée modèle), je vais essayer de le garder dans la vue si possible. Cela le déplace fondamentalement vers une méthode d'extension au format décimal, mais l'utilisation est dans la vue. Vous pouvez également faire une extension HtmlHelper:

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


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

Si vous avez aimé ce style mieux. C'est un peu plus lié à la vue car c'est une extension HtmlHelper.

+0

Oui, ceux-ci ont certainement plus de sens que de faire la chaîne.Format() à chaque fois dans la vue. Le problème auquel je suis confronté est que le ViewModel sera souvent rendu au client pour la consommation javascript - ala http://www.trycatchfail.com/blog/post/2009/12/22/Exposition-the-View-Model- à-JavaScript-in-ASPNET-MVC.aspx ou pendant les requêtes AJAX. Dans ces cas, je devrais faire le formatage sur la couche client, ce qui est moins que souhaitable - en fait, je pense que je ferais tout un tas d'efforts supplémentaires juste pour avoir toutes les questions de formatage/analyse séparées en une couche . –

+1

Il est également gênant pour moi que MVC dispose d'un mécanisme robuste pour analyser les requêtes entrantes via la liaison de modèle personnalisée, mais ne fournit pas le même type d'expérience pour le formatage pendant le rendu de la vue. –

+0

Je n'ai aucun problème avec la vue ou le client étant celui qui prend les décisions de formatage. En général, je préfère que sur le contrôleur ou le modèle choisir comment représenter les données - cela semble violer le principe de la séparation des préoccupations. Et si différents clients/vues (mobile ou web, par exemple) veulent le rendre de différentes façons? – tvanfosson

3

Avez-vous envisagé de mettre un DisplayFormat sur votre ViewModel? C'est ce que j'utilise et c'est rapide et simple.

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


View : 
    @Html.DisplayFor(m => m.CurrencyProperty) 
Questions connexes