Je travaille sur une application WPF avec des vues qui nécessitent de nombreuses conversions de valeur. Au départ, ma philosophie (inspirée en partie par ce débat animé sur les Disciples XAML ) était que je devais faire le modèle de vue strictement pour soutenir les exigences de données de la vue. Cela signifiait que toutes les conversions de valeur nécessaires pour transformer les données en éléments tels que la visibilité, les pinceaux, les tailles, etc. seraient traitées avec des convertisseurs de valeur et des convertisseurs à valeurs multiples. Conceptuellement, cela semblait assez élégant. Le modèle de vue et la vue auraient tous deux un objectif distinct et seraient bien découplés. Une ligne claire serait tracée entre "données" et "apparence".
Eh bien, après avoir donné à cette stratégie "l'ancien essai du collège", j'ai des doutes si je veux continuer à développer de cette façon. En fait, j'envisage sérieusement de vider les convertisseurs de valeur et de placer la responsabilité de (presque) toute la conversion de valeur carrément entre les mains du modèle de vue.
La réalité de l'utilisation de convertisseurs de valeur ne semble tout simplement pas être à la hauteur de la valeur apparente de problèmes clairement séparés. Mon plus gros problème avec les convertisseurs de valeur est qu'ils sont fastidieux à utiliser. Vous devez créer une nouvelle classe, implémenter IValueConverter
ou IMultiValueConverter
, convertir la valeur ou les valeurs du object
type correct, tester DependencyProperty.Unset
(au moins pour les convertisseurs à valeurs multiples), écrire la logique de conversion, enregistrer le convertisseur dans un dictionnaire de ressources [voir la mise à jour ci-dessous ], et enfin, branchez le convertisseur en utilisant du XAML assez verbeux (qui nécessite l'utilisation de chaînes magiques pour les liaisons et le nom du convertisseur)[voir la mise à jour ci-dessous]). Le processus de débogage n'est pas non plus un pique-nique, car les messages d'erreur sont souvent cryptiques, en particulier dans le mode de conception de Visual Studio / Expression Blend.
Cela ne veut pas dire que l'alternative - rendre le modèle de vue responsable de toute conversion de valeur - est une amélioration. Cela pourrait très bien être dû au fait que l'herbe est plus verte de l'autre côté. En plus de perdre l'élégante séparation des préoccupations, vous devez écrire un tas de propriétés dérivées et vous assurer d'appeler consciencieusement RaisePropertyChanged(() => DerivedProperty)
lors de la définition des propriétés de base, ce qui pourrait s'avérer être un problème de maintenance désagréable.
Voici une liste initiale que j'ai dressée des avantages et des inconvénients de permettre aux modèles de vue de gérer la logique de conversion et de supprimer les convertisseurs de valeur:
- Avantages:
- Moins de liaisons totales depuis l'élimination des multi-convertisseurs
- Moins de chaînes magiques (chemins de liaison
+ noms des ressources du convertisseur) Plus besoin d'enregistrer chaque convertisseur (plus de maintenir cette liste)- Moins de travail pour écrire chaque convertisseur (pas d'interface d'implémentation ni de casting requis)
- Peut facilement injecter des dépendances pour faciliter les conversions (par exemple, des tables de couleurs)
- Le balisage XAML est moins détaillé et plus facile à lire
- La réutilisation du convertisseur est toujours possible (bien qu'une certaine planification soit nécessaire)
- Aucun problème mystérieux avec DependencyProperty.Unset (un problème que j'ai remarqué avec les convertisseurs à valeurs multiples)
* Les barrés indiquent les avantages qui disparaissent si vous utilisez des extensions de balisage (voir la mise à jour ci-dessous)
- Les inconvénients:
- Couplage plus fort entre le modèle de vue et la vue (par exemple, les propriétés doivent traiter de concepts comme la visibilité et les pinceaux)
- Plus de propriétés totales pour permettre un mappage direct pour chaque liaison en vue
(voir la mise à jour 2 ci-dessous)RaisePropertyChanged
doit être appelé pour chaque propriété dérivée- Doit toujours s'appuyer sur des convertisseurs si la conversion est basée sur une propriété d'un élément d'interface utilisateur
Donc, comme vous pouvez probablement le constater, j'ai des brûlures d'estomac à ce sujet. J'hésite beaucoup à emprunter la voie de la refactorisation pour me rendre compte que le processus de codage est tout aussi inefficace et fastidieux, que j'utilise des convertisseurs de valeur ou que j'expose de nombreuses propriétés de conversion de valeur dans mon modèle de vue.
Suis-je en train de manquer des avantages / inconvénients? Pour ceux qui ont essayé les deux moyens de conversion de valeur, lequel avez-vous trouvé le mieux pour vous et pourquoi? Y a-t-il d'autres alternatives? (Les disciples ont mentionné quelque chose à propos des fournisseurs de descripteurs de type, mais je n'ai pas pu comprendre de quoi ils parlaient. Tout renseignement à ce sujet serait apprécié.)
Mise à jour
J'ai découvert aujourd'hui qu'il était possible d'utiliser quelque chose appelé une "extension de balisage" pour éliminer la nécessité d'enregistrer des convertisseurs de valeur. En fait, il élimine non seulement la nécessité de les enregistrer, mais il fournit en fait intellisense pour sélectionner un convertisseur lorsque vous tapez Converter=
. Voici l'article qui m'a lancé: http://www.wpftutorial.net/ValueConverters.html .
La possibilité d'utiliser une extension de balisage modifie quelque peu l'équilibre dans ma liste d'avantages et d'inconvénients et la discussion ci-dessus (voir les barrés).
À la suite de cette révélation, j'expérimente avec un système hybride où j'utilise des convertisseurs BoolToVisibility
et ce que j'appelle MatchToVisibility
et le modèle de vue pour toutes les autres conversions. MatchToVisibility est essentiellement un convertisseur qui me permet de vérifier si la valeur liée (généralement une énumération) correspond à une ou plusieurs valeurs spécifiées dans XAML.
Exemple:
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue=Visible, IfFalse=Hidden, Value1=Finished, Value2=Canceled}}"
Fondamentalement, cela vérifie si l'état est Terminé ou Annulé. Si c'est le cas, la visibilité est définie sur "Visible". Sinon, il prend la valeur "Hidden". Cela s'est avéré être un scénario très courant, et avoir ce convertisseur m'a sauvé environ 15 propriétés sur mon modèle de vue (plus les instructions RaisePropertyChanged associées). Notez que lorsque vous tapez Converter={vc:
, "MatchToVisibility" apparaît dans un menu intellisense. Cela réduit considérablement le risque d'erreurs et rend l'utilisation des convertisseurs de valeur moins fastidieuse (vous n'avez pas besoin de vous rappeler ou de rechercher le nom du convertisseur de valeur que vous souhaitez).
Si vous êtes curieux, je vais coller le code ci-dessous. Une caractéristique importante de cette mise en œuvre MatchToVisibility
est qu'il vérifie si la valeur limite est enum
, et si elle est, il vérifie que Value1
, Value2
etc. sont également énumérations du même type. Cela permet de vérifier au moment de la conception et de l'exécution si des valeurs d'énumération ont été mal typées. Pour améliorer cela à une vérification au moment de la compilation, vous pouvez utiliser ce qui suit à la place (j'ai tapé ceci à la main alors veuillez me pardonner si j'ai fait des erreurs):
Visibility="{Binding Status, Converter={vc:MatchToVisibility
IfTrue={x:Type {win:Visibility.Visible}},
IfFalse={x:Type {win:Visibility.Hidden}},
Value1={x:Type {enum:Status.Finished}},
Value2={x:Type {enum:Status.Canceled}}"
Bien que ce soit plus sûr, il est tout simplement trop verbeux pour en valoir la peine pour moi. Je pourrais aussi bien utiliser une propriété sur le modèle de vue si je veux le faire. Quoi qu'il en soit, je trouve que la vérification au moment de la conception est parfaitement adaptée aux scénarios que j'ai essayés jusqu'à présent.
Voici le code pour MatchToVisibility
[ValueConversion(typeof(object), typeof(Visibility))]
public class MatchToVisibility : BaseValueConverter
{
[ConstructorArgument("ifTrue")]
public object IfTrue { get; set; }
[ConstructorArgument("ifFalse")]
public object IfFalse { get; set; }
[ConstructorArgument("value1")]
public object Value1 { get; set; }
[ConstructorArgument("value2")]
public object Value2 { get; set; }
[ConstructorArgument("value3")]
public object Value3 { get; set; }
[ConstructorArgument("value4")]
public object Value4 { get; set; }
[ConstructorArgument("value5")]
public object Value5 { get; set; }
public MatchToVisibility() { }
public MatchToVisibility(
object ifTrue, object ifFalse,
object value1, object value2 = null, object value3 = null,
object value4 = null, object value5 = null)
{
IfTrue = ifTrue;
IfFalse = ifFalse;
Value1 = value1;
Value2 = value2;
Value3 = value3;
Value4 = value4;
Value5 = value5;
}
public override object Convert(
object value, Type targetType, object parameter, CultureInfo culture)
{
var ifTrue = IfTrue.ToString().ToEnum<Visibility>();
var ifFalse = IfFalse.ToString().ToEnum<Visibility>();
var values = new[] { Value1, Value2, Value3, Value4, Value5 };
var valueStrings = values.Cast<string>();
bool isMatch;
if (Enum.IsDefined(value.GetType(), value))
{
var valueEnums = valueStrings.Select(vs => vs == null ? null : Enum.Parse(value.GetType(), vs));
isMatch = valueEnums.ToList().Contains(value);
}
else
isMatch = valueStrings.Contains(value.ToString());
return isMatch ? ifTrue : ifFalse;
}
}
Voici le code pour BaseValueConverter
// this is how the markup extension capability gets wired up
public abstract class BaseValueConverter : MarkupExtension, IValueConverter
{
public override object ProvideValue(IServiceProvider serviceProvider)
{
return this;
}
public abstract object Convert(
object value, Type targetType, object parameter, CultureInfo culture);
public virtual object ConvertBack(
object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
Voici la méthode d'extension ToEnum
public static TEnum ToEnum<TEnum>(this string text)
{
return (TEnum)Enum.Parse(typeof(TEnum), text);
}
Update 2
Depuis que j'ai posté cette question, je suis tombé sur un projet open source qui utilise le "tissage IL" pour injecter du code NotifyPropertyChanged pour les propriétés et les propriétés dépendantes. Cela rend la mise en œuvre de la vision de Josh Smith du modèle de vue comme un «convertisseur de valeur sur les stéroïdes» un jeu d'enfant absolu. Vous pouvez simplement utiliser les "Propriétés implémentées automatiquement" et le tisserand fera le reste.
Exemple:
Si j'entre ce code:
public string GivenName { get; set; }
public string FamilyName { get; set; }
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
... c'est ce qui se compile:
string givenNames;
public string GivenNames
{
get { return givenName; }
set
{
if (value != givenName)
{
givenNames = value;
OnPropertyChanged("GivenName");
OnPropertyChanged("FullName");
}
}
}
string familyName;
public string FamilyName
{
get { return familyName; }
set
{
if (value != familyName)
{
familyName = value;
OnPropertyChanged("FamilyName");
OnPropertyChanged("FullName");
}
}
}
public string FullName
{
get
{
return string.Format("{0} {1}", GivenName, FamilyName);
}
}
C'est une énorme économie dans la quantité de code que vous devez taper, lire, faire défiler, etc. Plus important encore, cependant, cela vous évite d'avoir à comprendre quelles sont vos dépendances. Vous pouvez ajouter de nouvelles «propriétés» FullName
sans avoir à remonter minutieusement la chaîne de dépendances pour ajouter des RaisePropertyChanged()
appels.
Comment s'appelle ce projet open-source? La version originale s'appelle "NotifyPropertyWeaver", mais le propriétaire (Simon Potter) a depuis créé une plate-forme appelée "Fody" pour héberger toute une série de tisserands IL. L'équivalent de NotifyPropertyWeaver sous cette nouvelle plate-forme s'appelle PropertyChanged.Fody.
- Instructions de configuration de Fody: http://code.google.com/p/fody/wiki/SampleUsage (remplacez "Virtuosité" par "PropertyChanged")
- Site du projet PropertyChanged.Fody: http://code.google.com/p/propertychanged/
Si vous préférez utiliser NotifyPropertyWeaver (qui est un peu plus simple à installer, mais ne sera pas nécessairement mis à jour à l'avenir au-delà des corrections de bugs), voici le site du projet: http://code.google.com/p/ informerpropertyweaver /
Quoi qu'il en soit, ces solutions IL Weaver changent complètement le calcul dans le débat entre le modèle de vue sur les stéroïdes et les convertisseurs de valeur.
la source
BooleanToVisibility
prend une valeur liée à la visibilité (vrai / faux) et la traduit en une autre. Cela semble être une utilisation idéale de aValueConverter
. D'un autre côté,MatchToVisibility
est l'encodage de la logique métier dansView
(quels types d'éléments doivent être visibles). À mon avis, cette logique devrait être poussée vers le basViewModel
, voire plus loin dans ce que j'appelle leEditModel
. Ce que l'utilisateur peut voir devrait être quelque chose à tester.MatchToVisibility
semblait être un moyen pratique d'activer certains commutateurs de mode simples (j'ai une vue en particulier avec une tonne de pièces qui peuvent être activées et désactivées. Dans la plupart des cas, des sections de la vue sont même étiquetées (avecx:Name
) pour correspondre au mode ils correspondent.) Il ne m'est pas vraiment venu à l'esprit qu'il s'agit de «logique commerciale», mais je vais réfléchir à votre commentaire.Réponses:
J'ai utilisé
ValueConverters
dans certains cas et mis la logique dansViewModel
d'autres. Mon sentiment est que aValueConverter
devient une partie duView
calque, donc si la logique fait vraiment partie duView
alors mettez-le là, sinon mettez-le dans leViewModel
.Personnellement, je ne vois pas de problème avec un
ViewModel
traitement avec desView
concepts spécifiques commeBrush
es car dans mes applications, ilViewModel
n'existe qu'une surface testable et liable pour leView
. Cependant, certaines personnes mettent beaucoup de logique métier dansViewModel
(je ne le fais pas) et dans ce cas, celaViewModel
ressemble plus à une partie de leur couche métier, donc dans ce cas, je ne voudrais pas de choses spécifiques à WPF.Je préfère une séparation différente:
View
- Des trucs WPF, parfois non testables (comme XAML et code-behind) mais aussiValueConverter
sViewModel
- classe testable et liable qui est également spécifique à WPFEditModel
- partie de la couche métier qui représente mon modèle lors de la manipulationEntityModel
- une partie de la couche métier qui représente mon modèle comme persistantRepository
- responsable de la persistance de laEntityModel
à la base de donnéesDonc, la façon dont je le fais, j'ai peu d'utilité pour
ValueConverter
sLa façon dont je me suis éloigné de certains de vos "Con" est de rendre les miens
ViewModel
très génériques. Par exemple,ViewModel
j'ai un, appeléChangeValueViewModel
implémente une propriété Label et une propriété Value. Sur leView
il y a unLabel
qui se lie à la propriété Label et unTextBox
qui se lie à la propriété Value.J'ai ensuite un
ChangeValueView
qui est unDataTemplate
clavier duChangeValueViewModel
type. Chaque fois que WPF voitViewModel
qu'il l'appliqueView
. Le constructeur de myChangeValueViewModel
prend la logique d'interaction dont il a besoin pour rafraîchir son état à partir deEditModel
(généralement en passant simplement aFunc<string>
) et l'action qu'il doit entreprendre lorsque l'utilisateur modifie la valeur (juste uneAction
qui exécute une logique dans laEditModel
).Le parent
ViewModel
(pour l'écran) prend unEditModel
dans son constructeur et instancie simplement les éléments élémentaires appropriésViewModel
tels queChangeValueViewModel
. Étant donné que le parentViewModel
injecte l'action à effectuer lorsque l'utilisateur effectue une modification, il peut intercepter toutes ces actions et effectuer d'autres actions. Par conséquent, l'action de modification injectéeChangeValueViewModel
pourrait ressembler à:Évidemment, la
foreach
boucle peut être refactorisée ailleurs, mais ce que cela fait, c'est prendre l'action, l'appliquer au modèle, puis (en supposant que le modèle a mis à jour son état d'une manière inconnue), dit à tous les enfantsViewModel
d'aller et d'obtenir leur état à partir de le modèle à nouveau. Si l'État a changé, ils sont responsables de l'exécution de leursPropertyChanged
événements, si nécessaire.Cela gère très bien l'interaction entre, disons, une zone de liste et un panneau de détails. Lorsque l'utilisateur sélectionne un nouveau choix, il met à jour le
EditModel
avec le choix etEditModel
modifie les valeurs des propriétés exposées pour le panneau de détail. LesViewModel
enfants chargés d'afficher les informations du panneau de détail sont automatiquement informés qu'ils doivent vérifier les nouvelles valeurs et, s'ils ont changé, ils déclenchent leursPropertyChanged
événements.la source
ViewModel
calque. Tout le monde n'est pas d'accord avec moi, mais cela dépend du fonctionnement de votre architecture.CalendarViewModel
pour unCalendarView
UserControl ou unDialogViewModel
pour unDialogView
). C'est juste mon opinion cependant :)ViewModel
art.Si la conversion est liée à la vue, comme décider de la visibilité d'un objet, déterminer l'image à afficher ou déterminer la couleur de pinceau à utiliser, je place toujours mes convertisseurs dans la vue.
Si cela est lié à l'entreprise, comme déterminer si un champ doit être masqué ou si un utilisateur est autorisé à effectuer une action, la conversion se produit dans mon ViewModel.
A partir de vos exemples, je pense que vous manque un gros morceau de WPF:
DataTriggers
. Vous semblez utiliser des convertisseurs pour déterminer des valeurs conditionnelles, mais les convertisseurs devraient vraiment être destinés à convertir un type de données en un autre.Dans votre exemple ci-dessus
J'utiliserais un
DataTrigger
pour déterminer quelle image afficher, pas unConverter
. Un convertisseur sert à convertir un type de données en un autre, tandis qu'un déclencheur est utilisé pour déterminer certaines propriétés en fonction d'une valeur.La seule fois où j'envisagerais d'utiliser un convertisseur pour cela, c'est si la valeur liée contenait réellement les données d'image, et j'avais besoin de la convertir en un type de données que l'interface utilisateur pouvait comprendre. Par exemple, si la source de données contenait une propriété appelée
ImageFilePath
, j'envisagerais d'utiliser un convertisseur pour convertir la chaîne contenant l'emplacement du fichier image en uneBitmapImage
qui pourrait être utilisée comme source pour mon image.Le résultat final est que j'ai un espace de noms de bibliothèque plein de convertisseurs génériques qui convertissent un type de données en un autre, et j'ai rarement à coder un nouveau convertisseur. Il y a des occasions où je voudrai des convertisseurs pour des conversions spécifiques, mais ils sont assez rares pour que cela ne me dérange pas de les écrire.
la source
Grid
éléments entiers . J'essaie également de faire des choses comme définir des pinceaux pour le premier plan / l'arrière-plan / le contour en fonction des données de mon modèle de vue et d'une palette de couleurs spécifique définie dans le fichier de configuration. Je ne suis pas sûr que ce soit un bon choix pour un déclencheur ou un convertisseur. Le seul problème que j'ai jusqu'à présent avec la plupart des logiques de vue dans le modèle de vue est de câbler tous lesRaisePropertyChanged()
appels.DataTrigger
, même en supprimant les éléments de Grid. Habituellement, je place un endroitContentControl
où mon contenu dynamique devrait être et j'échange leContentTemplate
dans un déclencheur. J'ai un exemple sur le lien suivant si vous êtes intéressé (faites défiler jusqu'à la section avec l'en-tête deUsing a DataTrigger
) rachel53461.wordpress.com/2011/05/28/…<TextBlock Text="I'm a Person" Visibility={Binding ConsumerType, Converter={vc:MatchToVisibility IfTrue=Visible, IfFalse=Hidden, Value1=Person}}"
et<TextBlock Text="I'm a Business" Visibility={Binding ConsumerType, Converter={vc:MatchToVisibility IfTrue=Visible, IfFalse=Hidden, Value1=Business}}"
Ça dépend de ce que vous testez , le cas échéant.
Pas de tests: mélanger le code View w / ViewModel à volonté (vous pouvez toujours refactoriser plus tard).
Tests sur ViewModel et / ou inférieur: utilisez des convertisseurs.
Tests sur les couches Model et / ou inférieures: mélanger Voir le code avec ViewModel à volonté
ViewModel résume le modèle de la vue . Personnellement, j'utiliserais ViewModel pour les pinceaux, etc. et je sauterais les convertisseurs. Testez la ou les couches où les données sont dans leur forme "la plus pure " (c'est-à-dire les couches modèles ).
la source
Visibility
,SolidColorBrush
etThickness
.Cela ne résoudra probablement pas tous les problèmes que vous avez mentionnés, mais il y a deux points à considérer:
Tout d'abord, vous devez placer le code du convertisseur quelque part dans votre première stratégie. Considérez-vous cette partie de la vue ou du modèle de vue? Si cela fait partie de la vue, pourquoi ne pas placer les propriétés spécifiques à la vue dans la vue au lieu du modèle de vue?
Deuxièmement, il semble que votre conception sans convertisseur tente de modifier les propriétés réelles des objets qui existent déjà. Il semble qu'ils implémentent déjà INotifyPropertyChanged, alors pourquoi ne pas créer un objet wrapper spécifique à la vue auquel se lier? Voici un exemple simple:
la source
Parfois, il est bon d'utiliser un convertisseur de valeur pour profiter de la virtualisation.
Un exemple de cela dans un projet où nous devions afficher des données masquées par bit pour des centaines de milliers de cellules dans une grille. Lorsque nous avons décodé les masques de bits dans le modèle de vue pour chaque cellule, le programme a pris beaucoup trop de temps à charger.
Mais lorsque nous avons créé un convertisseur de valeur qui a décodé une seule cellule, le programme a été chargé en une fraction du temps et était tout aussi réactif car le convertisseur n'est appelé que lorsque l'utilisateur regarde une cellule particulière (et il ne devrait être appelé un maximum de trente fois à chaque fois que l'utilisateur change son point de vue sur la grille).
Je ne sais pas comment MVVM se plaignait de cette solution, mais elle a réduit le temps de chargement de 95%.
la source