web-dev-qa-db-fra.com

Comment sécuriser le type de liaison de données et prendre en charge le refactoring

Lorsque je souhaite lier un contrôle à une propriété de mon objet, je dois fournir le nom de la propriété sous forme de chaîne. Ce n'est pas très bon car:

  1. Si la propriété est supprimée ou renommée, je ne reçois pas d'avertissement du compilateur.
  2. Si vous renommez la propriété avec un outil de refactoring, il est probable que la liaison de données ne sera pas mise à jour.
  3. Je ne reçois pas d'erreur avant l'exécution si le type de propriété est incorrect, par exemple lier un entier à un sélecteur de date.

Existe-t-il un modèle de conception qui contourne cela, mais qui a toujours la facilité d'utilisation de la liaison de données?

(Il s'agit d'un problème dans WinForm, Asp.net et WPF et très probablement dans beaucoup d'autres systèmes)

J'ai maintenant trouvé " solutions de contournement pour l'opérateur nameof () en C #: liaison de données typesafe " qui a également un bon point de départ pour une solution.

Si vous êtes prêt à utiliser un post-processeur après avoir compilé votre code, notifypropertyweaver vaut la peine d'être examiné.


Tout le monde connaît une bonne solution pour WPF lorsque les liaisons sont effectuées en XML plutôt qu'en C #?

69
Ian Ringrose

Merci à Oliver de m'avoir aidé à démarrer. J'ai maintenant une solution qui prend en charge le refactoring et qui est sûre pour le type. Il m'a également permis d'implémenter INotifyPropertyChanged afin de faire face aux propriétés en cours de changement de nom.

Son utilisation ressemble à:

checkBoxCanEdit.Bind(c => c.Checked, person, p => p.UserCanEdit);
textBoxName.BindEnabled(person, p => p.UserCanEdit);
checkBoxEmployed.BindEnabled(person, p => p.UserCanEdit);
trackBarAge.BindEnabled(person, p => p.UserCanEdit);

textBoxName.Bind(c => c.Text, person, d => d.Name);
checkBoxEmployed.Bind(c => c.Checked, person, d => d.Employed);
trackBarAge.Bind(c => c.Value, person, d => d.Age);

labelName.BindLabelText(person, p => p.Name);
labelEmployed.BindLabelText(person, p => p.Employed);
labelAge.BindLabelText(person, p => p.Age);

La classe personne montre comment implémenter INotifyPropertyChanged d'une manière sûre de type (ou voir cette réponse pour une autre manière plutôt agréable d'implémenter INotifyPropertyChanged, ActiveSharp - Automatic INotifyPropertyChanged semble également bien) :

public class Person : INotifyPropertyChanged
{
   private bool _employed;
   public bool Employed
   {
      get { return _employed; }
      set
      {
         _employed = value;
         OnPropertyChanged(() => c.Employed);
      }
   }

   // etc

   private void OnPropertyChanged(Expression<Func<object>> property)
   {
      if (PropertyChanged != null)
      {
         PropertyChanged(this, 
             new PropertyChangedEventArgs(BindingHelper.Name(property)));
      }
   }

   public event PropertyChangedEventHandler PropertyChanged;
}

La classe d'aide à la liaison WinForms contient la viande qui fait que tout fonctionne:

namespace TypeSafeBinding
{
    public static class BindingHelper
    {
        private static string GetMemberName(Expression expression)
        {
            // The nameof operator was implemented in C# 6.0 with .NET 4.6
            // and VS2015 in July 2015. 
            // The following is still valid for C# < 6.0

            switch (expression.NodeType)
            {
                case ExpressionType.MemberAccess:
                    var memberExpression = (MemberExpression) expression;
                    var supername = GetMemberName(memberExpression.Expression);
                    if (String.IsNullOrEmpty(supername)) return memberExpression.Member.Name;
                    return String.Concat(supername, '.', memberExpression.Member.Name);
                case ExpressionType.Call:
                    var callExpression = (MethodCallExpression) expression;
                    return callExpression.Method.Name;
                case ExpressionType.Convert:
                    var unaryExpression = (UnaryExpression) expression;
                    return GetMemberName(unaryExpression.Operand);
                case ExpressionType.Parameter:
                case ExpressionType.Constant: //Change
                    return String.Empty;
                default:
                    throw new ArgumentException("The expression is not a member access or method call expression");
            }
        }

        public static string Name<T, T2>(Expression<Func<T, T2>> expression)
        {
            return GetMemberName(expression.Body);
        }

        //NEW
        public static string Name<T>(Expression<Func<T>> expression)
        {
           return GetMemberName(expression.Body);
        }

        public static void Bind<TC, TD, TP>(this TC control, Expression<Func<TC, TP>> controlProperty, TD dataSource, Expression<Func<TD, TP>> dataMember) where TC : Control
        {
            control.DataBindings.Add(Name(controlProperty), dataSource, Name(dataMember));
        }

        public static void BindLabelText<T>(this Label control, T dataObject, Expression<Func<T, object>> dataMember)
        {
            // as this is way one any type of property is ok
            control.DataBindings.Add("Text", dataObject, Name(dataMember));
        }

        public static void BindEnabled<T>(this Control control, T dataObject, Expression<Func<T, bool>> dataMember)
        {       
           control.Bind(c => c.Enabled, dataObject, dataMember);
        }
    }
}

Cela utilise beaucoup de nouvelles choses en C # 3.5 et montre exactement ce qui est possible. Maintenant, si seulement nous avions macros hygiéniques le programmeur LISP peut cesser de nous appeler des citoyens de seconde classe)

51
Ian Ringrose

L'opérateur nameof a été implémenté en C # 6.0 avec .NET 4.6 et VS2015 en juillet 2015. Ce qui suit est toujours valide pour C # <6.0

Pour éviter les chaînes qui contiennent des noms de propriété, j'ai écrit une classe simple en utilisant des arborescences d'expressions pour retourner le nom du membre:

using System;
using System.Linq.Expressions;
using System.Reflection;

public static class Member
{
    private static string GetMemberName(Expression expression)
    {
        switch (expression.NodeType)
        {
            case ExpressionType.MemberAccess:
                var memberExpression = (MemberExpression) expression;
                var supername = GetMemberName(memberExpression.Expression);

                if (String.IsNullOrEmpty(supername))
                    return memberExpression.Member.Name;

                return String.Concat(supername, '.', memberExpression.Member.Name);

            case ExpressionType.Call:
                var callExpression = (MethodCallExpression) expression;
                return callExpression.Method.Name;

            case ExpressionType.Convert:
                var unaryExpression = (UnaryExpression) expression;
                return GetMemberName(unaryExpression.Operand);

            case ExpressionType.Parameter:
                return String.Empty;

            default:
                throw new ArgumentException("The expression is not a member access or method call expression");
        }
    }

    public static string Name<T>(Expression<Func<T, object>> expression)
    {
        return GetMemberName(expression.Body);
    }

    public static string Name<T>(Expression<Action<T>> expression)
    {
        return GetMemberName(expression.Body);
    }
}

Vous pouvez utiliser cette classe comme suit. Même si vous ne pouvez l'utiliser que dans du code (donc pas en XAML), il est très utile (au moins pour moi), mais votre code n'est toujours pas sécurisé. Vous pouvez étendre la méthode Name avec un deuxième argument de type qui définit la valeur de retour de la fonction, ce qui contraindrait le type de la propriété.

var name = Member.Name<MyClass>(x => x.MyProperty); // name == "MyProperty"

Jusqu'à présent, je n'ai rien trouvé qui résout le problème de sécurité de la liaison de données.

Meilleures salutations

27
Oliver Hanappi

Le Framework 4.5 nous fournit le CallerMemberNameAttribute , ce qui rend inutile le passage du nom de la propriété sous forme de chaîne:

private string m_myProperty;
public string MyProperty
{
    get { return m_myProperty; }
    set
    {
        m_myProperty = value;
        OnPropertyChanged();
    }
}

private void OnPropertyChanged([CallerMemberName] string propertyName = "none passed")
{
    // ... do stuff here ...
}

Si vous travaillez sur Framework 4.0 avec KB2468871 installé, vous pouvez installer le Pack de compatibilité Microsoft BCL via nuget , qui fournit également cet attribut.

24
takrl

Ce blog l'article soulève de bonnes questions sur les performances de cette approche . Vous pouvez améliorer ces lacunes en convertissant l'expression en chaîne dans le cadre d'une sorte d'initialisation statique.

La mécanique réelle pourrait être un peu disgracieuse, mais elle serait toujours de type sécurisé et aurait des performances à peu près égales à INotifyPropertyChanged brut.

Quelque chose comme ça:

public class DummyViewModel : ViewModelBase
{
    private class DummyViewModelPropertyInfo
    {
        internal readonly string Dummy;

        internal DummyViewModelPropertyInfo(DummyViewModel model)
        {
            Dummy = BindingHelper.Name(() => model.Dummy);
        }
    }

    private static DummyViewModelPropertyInfo _propertyInfo;
    private DummyViewModelPropertyInfo PropertyInfo
    {
        get { return _propertyInfo ?? (_propertyInfo = new DummyViewModelPropertyInfo(this)); }
    }

    private string _dummyProperty;
    public string Dummy
    {
        get
        {
            return this._dummyProperty;
        }
        set
        {
            this._dummyProperty = value;
            OnPropertyChanged(PropertyInfo.Dummy);
        }
    }
}
5
nedruod

Une façon d'obtenir des commentaires si vos liaisons sont rompues est de créer un DataTemplate et de déclarer son DataType comme étant le type du ViewModel auquel il se lie, par exemple. si vous avez un PersonView et un PersonViewModel, vous feriez ce qui suit:

  1. Déclarez un DataTemplate avec DataType = PersonViewModel et une clé (par exemple PersonTemplate)

  2. Coupez tous les xaml PersonView et collez-les dans le modèle de données (qui, idéalement, peut simplement être en haut de PersonView.

3a. Créez un ContentControl et définissez le ContentTemplate = PersonTemplate et liez son contenu au PersonViewModel.

3b. Une autre option consiste à ne pas donner de clé au DataTemplate et à ne pas définir le ContentTemplate du ContentControl. Dans ce cas, WPF déterminera le DataTemplate à utiliser, car il sait à quel type d'objet vous vous liez. Il recherchera l'arborescence et trouvera votre DataTemplate et puisqu'il correspond au type de la liaison, il l'appliquera automatiquement en tant que ContentTemplate.

Vous vous retrouvez avec essentiellement la même vue qu'auparavant, mais puisque vous avez mappé le DataTemplate à un DataType sous-jacent, des outils comme Resharper peuvent vous donner des commentaires (via les identificateurs de couleur - Resharper-Options-Settings-Color Identifiers) quant à savoir si vos liaisons sont rompues ou pas.

Vous n'obtiendrez toujours pas d'avertissements du compilateur, mais vous pouvez vérifier visuellement les liaisons rompues, ce qui est mieux que d'avoir à faire des allers-retours entre votre vue et votre modèle de vue.

Un autre avantage de ces informations supplémentaires que vous donnez est qu'elles peuvent également être utilisées pour renommer les refactorings. Pour autant que je me souvienne, Resharper est capable de renommer automatiquement les liaisons sur les DataTemplates typés lorsque le nom de propriété du ViewModel sous-jacent est modifié et vice versa.

3
Thorsten Lorenz

1.Si la propriété est supprimée ou renommée, je ne reçois pas d'avertissement du compilateur.

2.Si vous renommez la propriété avec un outil de refactoring, il est probable que la liaison de données ne sera pas mise à jour.

3.Je n'obtiens pas d'erreur avant l'exécution si le type de propriété est incorrect, par ex. lier un entier à un sélecteur de date.

Oui, Ian, ce sont exactement les problèmes avec la liaison de données par chaîne de nom. Vous avez demandé un modèle de conception. J'ai conçu le modèle TVM (Type-Safe View Model) qui est une concrétion de la partie View Model du modèle Model-View-ViewModel (MVVM). Il est basé sur une liaison de type sécurisé, similaire à votre propre réponse. Je viens de publier une solution pour WPF:

http://www.codeproject.com/Articles/450688/Enhanced-MVVM-Design-w-Type-Safe-View-Models-TVM

3
Harry von Borstel

x: bind (également appelé "liaisons de données compilées") pour XAML (application universelle) dans Windows 10 et Windows Phone 10 peut résoudre ce problème, voir https://channel9.msdn.com/Events/Build/2015/3-635

Je ne trouve pas les documents en ligne pour cela, mais je n'ai pas mis beaucoup d'efforts, car c'est quelque chose que je n'utiliserai pas pendant un certain temps. Cependant, cette réponse devrait être un pointeur utile pour d'autres personnes.

1
Ian Ringrose