71 votes

Comment faire la liaison de données de type sécurisé et le soutien de refactoring

Quand je veux lier un contrôle à une propriété de mon objet, je dois fournir le nom de la propriété en tant que chaîne. Ce n'est pas très bon parce que:

  1. Si la propriété est retirée ou renommé, je ne suis pas un compilateur avertissement.
  2. Si un changement de nom de la propriété avec un outil de refactoring, il est probablement la liaison de données ne sera pas mis à jour.
  3. Je n'ai pas d'erreur jusqu'à ce que de l'exécution si le type de la propriété est faux, par exemple, la liaison d'un entier un sélecteur de date.

Est-il un design pattern qui contourne à présent, mais il a encore la facilité d'utilisation de la liaison de données?

(Ce qui est un problème en WinForm, Asp.net et WPF et probablement beaucoup d'autres systèmes)

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

Si vous êtes prêt à utiliser un post-processeur après la compilation de votre code, notifypropertyweaver est bien la peine de regarder.


Quelqu'un connaît une bonne solution pour WPF lorsque les liaisons sont fait en XML plutôt que de C#?

53voto

Ian Ringrose Points 19115

Merci à olivier de m'avoir commencé, j'ai maintenant une solution qui prend en charge à la fois de refactoring et de l'est de type sécurisé. Il m'a implémenter INotifyPropertyChanged de sorte qu'il se débrouille avec les propriétés d'être rebaptisé.

C'est l'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 la mise en œuvre INotifyPropertyChanged dans un type de façon sécuritaire (ou voir cette réponse pour un autre plutôt belle façon de mettre en œuvre INotifyPropertyChanged, ActiveSharp - Automatique INotifyPropertyChanged semble aussi bon ):

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

Les WinForms de liaison de la classe helper a de la viande dans ce qu'il fait tous les travaux:

namespace TypeSafeBinding
{
    public static class BindingHelper
    {
        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:
                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 rend l'utilisation de beaucoup de choses nouvelles en C# 3.5 et montre ce qu'il est possible. Maintenant, si seulement nous avions d'hygiène macros lisp programmeur peut cesser de l'appeler de nous des citoyens de seconde classe)

29voto

Oliver Hanappi Points 5141

Pour éviter des chaînes qui contiennent les noms de propriété, j'ai écrit une classe simple à l'aide de l'expression des arbres pour renvoyer 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 pouvez l'utiliser uniquement dans le code (donc pas dans le code XAML), il est très utile (au moins pour moi), mais ton code n'est pas encore typesafe. Vous pourriez élargir le Nom de la méthode avec un deuxième type d'argument qui définit la valeur de retour de la fonction, ce qui permet de limiter le type de la propriété.

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

Jusqu'à maintenant, je n'ai pas trouvé quelque chose qui résout le databinding typesafety question.

Meilleures Salutations

27voto

takrl Points 3285

Le framework 4.5 nous fournit l' CallerMemberNameAttribute, ce qui rend passant le nom de la propriété comme une chaîne de caractères inutiles:

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 ...
}

5voto

nedruod Points 750

Ce blog l'article pose quelques bonnes questions à propos de la performance de cette approche. Vous pourriez améliorer ces lacunes par la conversion de l'expression d'une chaîne de caractères en tant que partie d'une sorte d'initialisation statique.

Les mécanismes peuvent être un peu disgracieux, mais il serait encore être de type sécurisé, et environ le même rendement brut INotifyPropertyChanged.

Quelque chose dans ce style:

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

3voto

Thorsten Lorenz Points 4419

Une manière d'obtenir de la rétroaction si vos fixations sont brisés, est de créer un DataTemplate et de déclarer son Type de données à être le type de ce Dernier qu'il se lie à par exemple, si vous avez un PersonView et un PersonViewModel vous effectuez les opérations suivantes:

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

  2. Couper tous les PersonView xaml et le coller dans le modèle de données (qui, dans l'idéal peut-être juste en haut de la PersonView.

3a. Créer un ContentControl et définir la ContentTemplate = PersonTemplate et lier son Contenu à la PersonViewModel.

3b. Une autre option est de ne pas donner une clé pour le DataTemplate et ne pas définir de ContentTemplate de la ContentControl. Dans ce cas, WPF va comprendre ce que DataTemplate pour l'utiliser, car il sait quel type d'objet vous sont contraignantes. Il recherche en haut de l'arborescence et de trouver votre DataTemplate et depuis elle correspond au type de la liaison, il appliquera automatiquement comme le ContentTemplate.

Vous vous retrouvez avec essentiellement le même point de vue qu'avant, mais depuis que vous avez connecté le DataTemplate à un sous-jacent de Type de données, des outils comme Resharper peut vous donner de la rétroaction (par l'intermédiaire de la Couleur des identificateurs - Resharper-Options-Paramètres-Couleur Identifiants) pour déterminer si vos fixations sont en panne ou non.

Vous ne parvenez toujours pas à obtenir les avertissements du compilateur, mais peut vérifier visuellement cassé les liaisons, ce qui est mieux que d'avoir à vérifier en arrière-et-vient entre votre point de vue et viewmodel.

Un autre avantage de ce complément d'information que vous donnez, est, qu'il peut également être utilisé pour renommer les refactorings. Aussi loin que je me souviens de Resharper est capable de renommer automatiquement les liaisons tapé DataTemplates lorsque le sous-jacent ViewModel du nom de la propriété est modifiée, et vice-versa.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X