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.
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 (avec x: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.
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.