Comment soulever L'événement PropertyChanged sans utiliser le nom de la chaîne

Il serait bon d'avoir la capacité d'élever l'événement 'PropertyChanged' sans spécifier explicitement le nom de la propriété modifiée. Je voudrais faire quelque chose comme ceci:

    public string MyString
    {
        get { return _myString; }
        set
        {
            ChangePropertyAndNotify<string>(val=>_myString=val, value);
        }
    }

    private void ChangePropertyAndNotify<T>(Action<T> setter, T value)
    {
        setter(value);
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, new PropertyChangedEventArgs(setter.Method.Name));
        }
    }

dans ce cas, le nom reçu est un nom de lambda-méthode: " b__0".

  1. puis-je être sûr que la taille "b__0" fournira toujours le bon nom de propriété?
  2. y a-t-il autre chose à signaler au sujet de la propriété changée (de la propriété lui-même)?

je vous Remercie.

35
demandé sur rohancragg 2010-07-07 06:36:59

8 réponses

Ajouté C # 6 Réponse

En C# 6 (et quelle que soit la version de VB est livré avec Visual Studio 2015), nous avons l' nameof opérateur qui rend les choses plus facile que jamais. Dans ma réponse originale ci-dessous, j'utilise une fonction C# 5 (attributs d'information de l'appelant) pour gérer le cas commun des notifications "d'auto-modification". nameof opérateur peut être utilisé dans tous les cas, et est particulièrement utile dans le "propriété changé" notification scénario.

par souci de simplicité, je pense que je vais conserver l'approche de l'attribut caller info pour les notifications de changement de fournisseur. Moins de Dactylographie signifie moins de chances pour les fautes de frappe et les bogues induits par copier/coller... le compilateur ici s'assure que vous choisissez un type/member/variable valide, mais il ne s'assure pas que vous choisissez le bon. Il est alors simple d'utiliser le nouveau nameof opérateur pour les notifications de changement de propriété. L'exemple ci-dessous illustre un comportement clé des attributs d'information de l'appelant... le attribut n'a aucun effet sur un paramètre si le paramètre est spécifié par l'appelant (qui est, l'appelant info est fournie par la valeur du paramètre uniquement lorsque le paramètre est omis par l'appelant).

il est également intéressant d'observer que le PropertyName valeur de l'événement (qui est un string) à une propriété particulière en utilisant le nameof opérateur, éliminant plus de magie chaîne.

Référence pour info https://msdn.microsoft.com/en-us/library/dn986596.aspx

Exemple:

public class Program
{
    void Main()
    {
        var dm = new DataModel();
        dm.PropertyChanged += propertyChangedHandler;
    }

    void propertyChangedHandler(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == nameof(DataModel.NumberSquared))
        {
            //do something spectacular
        }
    }
}


public class DataModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

public class DataModel : DataModelBase
{
    //a simple property
    string _something;
    public string Something 
    { 
        get { return _something; } 
        set { _something = value; OnPropertyChanged(); } 
    }

    //a property with another related property
    int _number;
    public int Number
    {
        get { return _number; }

        set 
        { 
            _number = value; 
            OnPropertyChanged(); 
            OnPropertyChanged(nameof(this.NumberSquared)); 
         }
    }

    //a related property
    public int NumberSquared { get { return Number * Number; } }
}

Origine C# 5 réponse

depuis C# 5, il est préférable d'utiliser les attributs d'information de l'appelant, ceci est résolu au moment de la compilation, aucune réflexion n'est nécessaire.

je l'implémenter dans une classe de base, les classes dérivées suffit d'appeler le OnPropertyChanged méthode à l'intérieur de leurs limites accesseurs de propriété. Si une propriété change implicitement une autre valeur, je peux utiliser la version" explicite "de la méthode dans la propriété setter aussi, qui n'est alors plus" sûre " mais est une situation rare que j'accepte simplement.

vous pouvez aussi utiliser cette méthode pour les notifications de changement de propriété, et utiliser la réponse donnée par @Jehof pour les notifications de changement de propriété ... cela aurait l'avantage de ne pas avoir de chaînes magiques, avec l'exécution la plus rapide pour le cas commun de notifications d'auto-changement.

cette dernière suggestion est implémentée ci-dessous (je pense que je vais commencer à l'utiliser!)

public class DataModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
    {
        OnPropertyChangedExplicit(propertyName);
    }

    protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection)
    {
        var memberExpression = (MemberExpression)projection.Body;
        OnPropertyChangedExplicit(memberExpression.Member.Name);
    }

    void OnPropertyChangedExplicit(string propertyName)
    {
        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            var e = new PropertyChangedEventArgs(propertyName);
            handler(this, e);
        }
    }
}

public class DataModel : DataModelBase
{
    //a simple property
    string _something;
    public string Something 
    { 
        get { return _something; } 
        set { _something = value; OnPropertyChanged(); } 
    }

    //a property with another related property
    int _number;
    public int Number
    {
        get { return _number; }

        set 
        { 
            _number = value; 
            OnPropertyChanged(); 
            OnPropertyChanged(() => NumberSquared); 
         }
    }

    //a related property
    public int NumberSquared { get { return Number * Number; } }
}
33
répondu TCC 2015-11-25 21:06:55

mise à Jour: le code original N'est pas compatible avec Windows Phone, car il s'appuie sur LambdaExpression.Compiler () pour obtenir l'objet event source. Voici la méthode d'extension mise à jour (avec les vérifications des paramètres supprimées également):

    public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
    {
        if (handler != null)
        {
            var body = propertyExpression.Body as MemberExpression;
            var expression = body.Expression as ConstantExpression;
            handler(expression.Value, new PropertyChangedEventArgs(body.Member.Name));
        }
    }

L'utilisation reste comme ci-dessous.


vous pouvez obtenir le nom de la propriété en utilisant la réflexion sur une fonction lambda qui appelle la propriété getter. notez que vous n'avez pas à invoquer que la lambda, vous avez juste besoin pour la réflexion:

public static class INotifyPropertyChangedHelper
{
    public static void Raise<T>(this PropertyChangedEventHandler handler, Expression<Func<T>> propertyExpression)
    {
        if (handler != null)
        {
            var body = propertyExpression.Body as MemberExpression;
            if (body == null)
                throw new ArgumentException("'propertyExpression' should be a member expression");

            var expression = body.Expression as ConstantExpression;
            if (expression == null)
                throw new ArgumentException("'propertyExpression' body should be a constant expression");

            object target = Expression.Lambda(expression).Compile().DynamicInvoke();

            var e = new PropertyChangedEventArgs(body.Member.Name);
            handler(target, e);
        }
    }

    public static void Raise<T>(this PropertyChangedEventHandler handler, params Expression<Func<T>>[] propertyExpressions)
    {
        foreach (var propertyExpression in propertyExpressions)
        {
            handler.Raise<T>(propertyExpression);
        }
    }
}

Voici comment vous pouvez utiliser l'aide de votre classe pour déclencher l'événement pour une ou plusieurs propriétés:

PropertyChanged.Raise(() => this.Now);
PropertyChanged.Raise(() => this.Age, () => this.Weight);

notez que ce helper est aussi un no-op au cas où le PropertyChangednull.

28
répondu Franci Penov 2010-12-28 21:28:35

dans l'exemple suivant vous devez passer 3 valeurs (champ de soutien, nouvelle valeur, propriété comme lambda) mais il n'y a pas de chaînes magiques et l'événement de propriété modifié est seulement soulevé quand il n'est vraiment pas égal.

class Sample : INotifyPropertyChanged
{
    private string _name;
    public string Name
    {
        get { return _name; }
        set { this.SetProperty(ref _name, value, () => this.Name); }
    }


    protected void SetProperty<T>(ref T backingField, T newValue, Expression<Func<T>> propertyExpression)
    {
        if (backingField == null && newValue == null)
        {
            return;
        }

        if (backingField == null || !backingField.Equals(newValue))
        {
            backingField = newValue;
            this.OnPropertyChanged(propertyExpression);
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyExpression.GetPropertyName()));
        }
    }

}

et le code suivant contient des méthodes d'extension pour obtenir un nom de propriété à partir d'une expression lambda.

public static class Extensions
{
    public static string GetPropertyName<TProperty>(this Expression<Func<TProperty>> propertyExpression)
    {
        return propertyExpression.Body.GetMemberExpression().GetPropertyName();
    }

    public static string GetPropertyName(this MemberExpression memberExpression)
    {
        if (memberExpression == null)
        {
            return null;
        }

        if (memberExpression.Member.MemberType != MemberTypes.Property)
        {
            return null;
        }

        var child = memberExpression.Member.Name;
        var parent = GetPropertyName(memberExpression.Expression.GetMemberExpression());

        if (parent == null)
        {
            return child;
        }
        else
        {
            return parent + "." + child;
        }
    }

    public static MemberExpression GetMemberExpression(this Expression expression)
    {
        var memberExpression = expression as MemberExpression;

        if (memberExpression != null)
        {
            return memberExpression;
        }

        var unaryExpression = expression as UnaryExpression;


        if (unaryExpression != null)
        {
            memberExpression = (MemberExpression)unaryExpression.Operand;

            if (memberExpression != null)
            {
                return memberExpression;
            }

        }
        return null;
    }

    public static void ShouldEqual<T>(this T actual, T expected, string name)
    {
        if (!Object.Equals(actual, expected))
        {
            throw new Exception(String.Format("{0}: Expected <{1}> Actual <{2}>.", name, expected, actual));
        }
    }

}

Enfin, un peu de code de test:

class q3191536
{
    public static void Test()
    {
        var sample = new Sample();
        var propertyChanged = 0;

        sample.PropertyChanged += 
            new PropertyChangedEventHandler((sender, e) => 
                {
                    if (e.PropertyName == "Name")
                    {
                        propertyChanged += 1;
                    }
                }
            );

        sample.Name = "Budda";

        sample.Name.ShouldEqual("Budda", "sample.Name");
        propertyChanged.ShouldEqual(1, "propertyChanged");

        sample.Name = "Tim";
        sample.Name.ShouldEqual("Tim", sample.Name);
        propertyChanged.ShouldEqual(2, "propertyChanged");

        sample.Name = "Tim";
        sample.Name.ShouldEqual("Tim", sample.Name);
        propertyChanged.ShouldEqual(2, "propertyChanged");
    }
}
6
répondu Tim Murphy 2010-07-08 07:28:45

Im en utilisant la méthode d'extension

public static class ExpressionExtensions {
    public static string PropertyName<TProperty>(this Expression<Func<TProperty>> projection) {
        var memberExpression = (MemberExpression)projection.Body;

        return memberExpression.Member.Name;
    }
}

en combinaison avec la méthode suivante. La méthode est définie dans la classe qui implémente l'interface INotifyPropertyChanged (normalement une classe de base à partir de laquelle mes autres classes sont dérivées).

protected void OnPropertyChanged<TProperty>(Expression<Func<TProperty>> projection) {
    var e = new PropertyChangedEventArgs(projection.PropertyName());

    OnPropertyChanged(e);
}

Alors je peux augmenter le PropertyChanged-Événement comme suit

private double _rate;
public double Rate {
        get {
            return _rate;
        }
        set {
            if (_rate != value) {
              _rate = value;                     
              OnPropertyChanged(() => Rate );
            }
        }
    }

en utilisant cette approche, ses propriétés faciles à renommer (dans Visual Studio), car il assure que le PropertyChanged correspondant l'appel est aussi mis à jour.

4
répondu Jehof 2012-01-13 06:37:07

C'est la façon que j'ai trouvée pour le faire:

public abstract class ViewModel<T> : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public void RaisePropertyChanged(Expression<Func<T, object>> expression)
    {
        var propertyName = GetPropertyFromExpression(expression);

        this.OnPropertyChanged(propertyName);
    }

    public string GetPropertyFromExpression(System.Linq.Expressions.Expression expression)
    {
        if (expression == null)
            throw new ArgumentException("Getting property name form expression is not supported for this type.");

        var lamda = expression as LambdaExpression;
        if (lamda == null)
            throw new NotSupportedException("Getting property name form expression is not supported for this type.");

        var mbe = lamda.Body as MemberExpression;
        if (mbe != null)
            return mbe.Member.Name;

        var unary = lamda.Body as UnaryExpression;
        if (unary != null)
        {
            var member = unary.Operand as MemberExpression;
            if (member != null)
                return member.Member.Name;
        }

        throw new NotSupportedException("Getting property name form expression is not supported for this type.");
    }
 }
2
répondu Jeff 2011-05-03 01:16:42

Les solutions qui ont déjà posté un mélange de ces deux questions:

1) Certains vous demandent de créer une classe de base et hériter d'elle. Il s'agit d'un énorme problème qui peut jeter une clé dans la chaîne d'héritage de vos classes et vous amener à commencer à re-concevoir votre domaine juste pour permettre un développement "supplémentaire" comme celui-ci.

2) alors que les solutions existantes vous permettent de désigner quelle propriété pour lancer l'événement modifié via une expression lambda ils enregistrent et distribuent toujours un string représentation du nom de la propriété car ils s'appuient sur le PropertyChangedEventArgs classe. Donc n'importe quel code qui utilise votre PropertyChanged event doit encore faire une comparaison de chaîne de caractères qui casse encore une fois tout remaniement automatique que vous pourriez avoir à faire à l'avenir, sans mentionner que votre support de compilation time est sorti de la fenêtre qui est l'un des principaux points d'autoriser les expressions lambda au lieu des chaînes de caractères.

ce est-ce que ma version générique suit le même modèle d'événement/délégué démarré par MS, ce qui signifie qu'aucune classe de base et aucune méthode d'extension ne sont nécessaires.

public class PropertyChangedEventArgs<TObject> : EventArgs
{
    private readonly MemberInfo _property;

    public PropertyChangedEventArgs(Expression<Func<TObject, object>> expression)
    {
        _property = GetPropertyMember(expression);
    }

    private MemberInfo GetPropertyMember(LambdaExpression p)
    {
        MemberExpression memberExpression;
        if (p.Body is UnaryExpression)
        {
            UnaryExpression ue = (UnaryExpression)p.Body;
            memberExpression = (MemberExpression)ue.Operand;
        }
        else
        {
            memberExpression = (MemberExpression)p.Body;
        }
        return (PropertyInfo)(memberExpression).Member;
    }

    public virtual bool HasChanged(Expression<Func<TObject, object>> expression)
    {
        if (GetPropertyMember(expression) == Property)
            return true;
        return false;
    }

    public virtual MemberInfo Property
    {
        get
        {
            return _property;
        }
    }
}

public delegate void PropertyChangedEventHandler<TObject>(object sender, PropertyChangedEventArgs<TObject> e);

public interface INotifyPropertyChanged<TObject>
{
    event PropertyChangedEventHandler<TObject> PropertyChanged;
}

Maintenant, vous pouvez l'utiliser sur une classe comme ceci:

public class PagedProduct : INotifyPropertyChanged<PagedProduct>
{
    IPager _pager;

    public event PropertyChangedEventHandler<PagedProduct> PropertyChanged = delegate { };

    public PagedProduct() { }

    public IPager Pager
    {
        get { return _pager; }
        set
        {
            if (value != _pager)
            {
                _pager = value;
                // let everyone know this property has changed.
                PropertyChanged(this, new PropertyChangedEventArgs<PagedProduct>(a => a.Pager));
            }
        }
    }
}

et enfin vous pouvez écouter les événements sur cet objet et déterminer quelle propriété a changé en utilisant une expression lambda aussi bien!

void SomeMethod()
{
    PagedProduct pagedProducts = new PagedProduct();
    pagedProducts.PropertyChanged += pagedProducts_PropertyChanged;
}

void pagedProducts_PropertyChanged(object sender, PropertyChangedEventArgs<PagedProduct> e)
{
    // lambda expression is used to determine if the property we are interested in has changed. no strings here
    if (e.HasChanged(a => a.Pager))
    {
        // do something mind blowing like ordering pizza with a coupon
    }
}
2
répondu TugboatCaptain 2013-05-28 07:40:40

il y a plusieurs façons de le faire sans utiliser un nom propre.

mieux vaut simplement lire les blogs.

http://www.pochet.net/blog/2010/06/25/inotifypropertychanged-implementations-an-overview/

http://justinangel.net/AutomagicallyImplementingINotifyPropertyChanged

1
répondu Simon 2010-07-27 10:52:28

j'utilise une méthode d'extension simple pour obtenir le nom de la propriété pour éviter les problèmes avec les chaînes magiques. Il maintient également la lisibilité du code, c'est à dire, il est explicite ce qui se passe.

La méthode d'extension est tout simplement comme suit:

public static string GetPropertyName(this MethodBase methodBase)
{
    return methodBase.Name.Substring(4);
}

avec cela, cela signifie que vos ensembles de propriétés sont résistants aux changements de noms et ressemblent à ce qui suit:

private string _name;
public string Name
{
    get { return _name; }
    set 
    {
            name = value;
            RaisePropertyChanged(MethodBase.GetCurrentMethod().GetPropertyName()); 
    }
}

j'ai écrit plus à ce sujet méthode d'extension ici et j'ai publié un extrait de code Correspondant Ici.

0
répondu Steven Wilber 2011-06-06 08:40:05