web-dev-qa-db-fra.com

INotifyPropertyChanged automatiquement

Existe-t-il un moyen d'être automatiquement informé des changements de propriété dans une classe sans avoir à écrire OnPropertyChanged dans chaque setter? (J'ai des centaines de propriétés que je veux savoir si elles ont changé).


Anton suggère mandataires dynamiques . J'ai en fait utilisé la bibliothèque "Castle" pour quelque chose de similaire dans le passé, et bien qu'elle réduise la quantité de code que j'ai dû écrire, elle a ajouté environ 30 secondes au temps de démarrage de mon programme (ymmv) - parce que c'est un solution d'exécution.

Je me demande s'il existe une solution de compilation, peut-être en utilisant des attributs de compilation ...


Slashene et TcKs donnent des suggestions qui génèrent du code répétitif - malheureusement, toutes mes propriétés ne sont pas un simple cas de m_Value = value - beaucoup d'entre elles ont un code personnalisé dans les setters, donc le code de coupe-cookie des extraits de code et du xml n'est pas vraiment faisable pour mon projet non plus.

57
Tim Gradwell

EDIT: L'auteur de NotifyPropertyWeaver a déprécié l'outil au profit du plus général Fody . (Un guide de migration pour les personnes passant du tisserand au fody est disponible.)


Un outil très pratique que j'ai utilisé pour mes projets est Notifier Property WeaverFody .

Il s'installe en tant qu'étape de génération dans vos projets et lors de la compilation injecte du code qui déclenche l'événement PropertyChanged.

Faire augmenter les propriétés PropertyChanged se fait en mettant attributs spéciaux dessus:

[ImplementPropertyChanged]
public string MyProperty { get; set; }

En prime, vous pouvez également spécifier des relations pour les propriétés qui dépendent d'autres propriétés

[ImplementPropertyChanged]
public double Radius { get; set; }

[DependsOn("Radius")]
public double Area 
{
    get { return Radius * Radius * Math.PI; }
}
43
Isak Savo

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

Nous utilisons le code ci-dessous (From http://www.ingebrigtsen.info/post/2008/12/11/INotifyPropertyChanged-revisited.aspx ). Fonctionne très bien :)

public static class NotificationExtensions
{
    #region Delegates

    /// <summary>
    /// A property changed handler without the property name.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="sender">The object that raised the event.</param>
    public delegate void PropertyChangedHandler<TSender>(TSender sender);

    #endregion

    /// <summary>
    /// Notifies listeners about a change.
    /// </summary>
    /// <param name="EventHandler">The event to raise.</param>
    /// <param name="Property">The property that changed.</param>
    public static void Notify(this PropertyChangedEventHandler EventHandler, Expression<Func<object>> Property)
    {
        // Check for null
        if (EventHandler == null)
            return;

        // Get property name
        var lambda = Property as LambdaExpression;
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression)
        {
            var unaryExpression = lambda.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = lambda.Body as MemberExpression;
        }

        ConstantExpression constantExpression;
        if (memberExpression.Expression is UnaryExpression)
        {
            var unaryExpression = memberExpression.Expression as UnaryExpression;
            constantExpression = unaryExpression.Operand as ConstantExpression;
        }
        else
        {
            constantExpression = memberExpression.Expression as ConstantExpression;
        }

        var propertyInfo = memberExpression.Member as PropertyInfo;

        // Invoke event
        foreach (Delegate del in EventHandler.GetInvocationList())
        {
            del.DynamicInvoke(new[]
            {
                constantExpression.Value, new PropertyChangedEventArgs(propertyInfo.Name)
            });
        }
    }


    /// <summary>
    /// Subscribe to changes in an object implementing INotifiyPropertyChanged.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="ObjectThatNotifies">The object you are interested in.</param>
    /// <param name="Property">The property you are interested in.</param>
    /// <param name="Handler">The delegate that will handle the event.</param>
    public static void SubscribeToChange<T>(this T ObjectThatNotifies, Expression<Func<object>> Property, PropertyChangedHandler<T> Handler) where T : INotifyPropertyChanged
    {
        // Add a new PropertyChangedEventHandler
        ObjectThatNotifies.PropertyChanged += (s, e) =>
            {
                // Get name of Property
                var lambda = Property as LambdaExpression;
                MemberExpression memberExpression;
                if (lambda.Body is UnaryExpression)
                {
                    var unaryExpression = lambda.Body as UnaryExpression;
                    memberExpression = unaryExpression.Operand as MemberExpression;
                }
                else
                {
                    memberExpression = lambda.Body as MemberExpression;
                }
                var propertyInfo = memberExpression.Member as PropertyInfo;

                // Notify handler if PropertyName is the one we were interested in
                if (e.PropertyName.Equals(propertyInfo.Name))
                {
                    Handler(ObjectThatNotifies);
                }
            };
    }
}

Utilisé par exemple de cette façon:

public class Employee : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    private string _firstName;
    public string FirstName
    {
        get { return this._firstName; }
        set
        {
            this._firstName = value;
            this.PropertyChanged.Notify(()=>this.FirstName);
        }
    }
}

private void firstName_PropertyChanged(Employee sender)
{
    Console.WriteLine(sender.FirstName);
}

employee = new Employee();
employee.SubscribeToChange(() => employee.FirstName, firstName_PropertyChanged);

Certaines erreurs de syntaxe dans l'exemple peuvent exister. Je ne l'ai pas testé. Mais vous devriez au moins avoir le concept :)

EDIT: Je vois maintenant que vous vouliez peut-être encore moins de travail, mais oui ... le truc ci-dessus au moins le rend beaucoup plus facile. Et vous évitez tous les problèmes effrayants liés à la référence aux propriétés à l'aide de chaînes.

39
Svish

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

Similaire à la solution de Svish, remplaçant simplement la génialité lambda par une fonctionnalité de cadre ennuyeux ;-)

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.

31
takrl

Vous pouvez avoir une méthode d'extension sur votre délégué PropertyChanged et l'utiliser comme ceci:

public string Name
{
    get { return name; }
    set
    {
        name = value;
        PropertyChanged.Raise(() => Name);
    }
}

Abonnement à un changement de propriété spécifique:

var obj = new Employee();

var handler = obj.SubscribeToPropertyChanged(
    o => o.FirstName, 
    o => Console.WriteLine("FirstName is now '{0}'", o.FirstName));

obj.FirstName = "abc";

// Unsubscribe when required
obj.PropertyChanged -= handler;

la méthode d'extension est capable de déterminer l'expéditeur et le nom de la propriété simplement en inspectant l'arbre d'expression lambda et sans impact majeur sur les performances:

public static class PropertyChangedExtensions
{
    public static void Raise<TProperty>(
        this PropertyChangedEventHandler handler, Expression<Func<TProperty>> property)
    {
        if (handler == null)
            return;

        var memberExpr = (MemberExpression)property.Body;
        var propertyName = memberExpr.Member.Name;
        var sender = ((ConstantExpression)memberExpr.Expression).Value;
        handler.Invoke(sender, new PropertyChangedEventArgs(propertyName));
    }

    public static PropertyChangedEventHandler SubscribeToPropertyChanged<T, TProperty>(
        this T obj, Expression<Func<T, TProperty>> property, Action<T> handler)
        where T : INotifyPropertyChanged
    {
        if (handler == null)
            return null;

        var memberExpr = (MemberExpression)property.Body;
        var propertyName = memberExpr.Member.Name;

        PropertyChangedEventHandler subscription = (sender, eventArgs) =>
        {
            if (propertyName == eventArgs.PropertyName)
                handler(obj);
        };

        obj.PropertyChanged += subscription;

        return subscription;
    }
}

Si l'événement PropertyChanged est déclaré dans un type de base, il ne sera pas visible en tant que champ délégué dans les classes dérivées. Dans ce cas, une solution consiste à déclarer un champ protégé de type PropertyChangedEventHandler et à implémenter explicitement les accesseurs add et remove de l'événement:

public class Base : INotifyPropertyChanged
{
    protected PropertyChangedEventHandler propertyChanged;
    public event PropertyChangedEventHandler PropertyChanged
    {
        add { propertyChanged += value; }
        remove { propertyChanged -= value; }
    }
}

public class Derived : Base
{
    string name;

    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            propertyChanged.Raise(() => Name);
        }
    }
}
11

Implémentez un type sécurisé INotifyPropertyChanged: voir ici

Créez ensuite votre propre extrait de code:

private $Type$ _$PropertyName$;
public $Type$ $PropertyName$
{
    get
    {
        return _$PropertyName$;
    }
    set
    {
        if(value != _$PropertyName$)
        {
            _$PropertyName$ = value;
            OnPropertyChanged(o => o.$PropertyName$);               
        }
    }
}

Avec Concepteur d'extraits de code et vous avez terminé! Un moyen simple et sécurisé de créer votre INotifyPropertyChanged.

10
Nicolas Dorier

Je ne connais pas de méthode standard, mais je connais deux solutions:

1) PostSharp peut le faire pour vous après la compilation. C'est très utile, mais cela prend du temps sur chaque build.

2) Outil personnalisé dans Visual Studio. Vous pouvez le combiner avec "classe partielle". Ensuite, vous pouvez créer un outil personnalisé pour votre XML et vous pouvez générer du code source à partir du xml.

Par exemple, ce xml:

<type scope="public" type="class" name="MyClass">
    <property scope="public" type="string" modifier="virtual" name="Text" notify="true" />
</type>

peut être source pour ce code:

public partial class MyClass {
    private string _text;
    public virtual string Text {
        get { return this._Text; }
        set {
            this.OnPropertyChanging( "Text" );
            this._Text = value;
            this.OnPropertyChanged( "Text" );
        }
    }
}
4
TcKs

vous voudrez peut-être examiner la programmation orientée aspect dans son ensemble

Frameworks => vous pouvez regarder linf

4
almog.ori

Je viens de trouver ActiveSharp - Automatic INotifyPropertyChanged , je ne l'ai pas encore utilisé, mais ça a l'air bien.

Pour citer son site Web ...


Envoyez des notifications de modification de propriété sans spécifier le nom de la propriété sous forme de chaîne.

Au lieu de cela, écrivez des propriétés comme ceci:

public int Foo
{
    get { return _foo; }
    set { SetValue(ref _foo, value); }  // <-- no property name here
}

Notez qu'il n'est pas nécessaire d'inclure le nom de la propriété sous forme de chaîne. ActiveSharp le comprend de manière fiable et correcte. Il fonctionne sur la base du fait que l'implémentation de votre propriété passe le champ de support (_foo) par ref. (ActiveSharp utilise cet appel "par référence" pour identifier le champ de sauvegarde qui a été transmis et à partir du champ, il identifie la propriété).

2
Ian Ringrose

Vous pourriez regarder Castle ou Spring.NET et implémenter la fonctionnalité d'intercepteur?

2
Neil Barnwell

Juste pour rendre l'implémentation plus rapide , vous pouvez utiliser un extrait

De http://aaron-hoffman.blogspot.it/2010/09/visual-studio-code-snippet-for-notify.html

les classes de projets ViewModel suivant le modèle M-V-VM, il est souvent nécessaire de déclencher un événement "PropertyChanged" (pour aider à l'implémentation de l'interface INotifyPropertyChanged) à partir du setter d'une propriété. C'est une tâche fastidieuse qui, espérons-le, sera un jour résolue en utilisant le compilateur en tant que service ...

L'extrait de base (pour lequel le crédit complet revient à l'auteur, qui n'est pas moi) est le suivant

  <Code Language= "csharp "> 
    <![CDATA[public $type$ $property$ 
{ 
    get { return _$property$; } 
    set 
    { 
        if (_$property$ != value) 
        { 
            _$property$ = value; 
            OnPropertyChanged($property$PropertyName); 
        } 
    } 
} 
private $type$ _$property$; 
public const string $property$PropertyName = "$property$";$end$]]> 
</Code> 

Amélioration pour appeler l'événement dans les classes d'enfants:

Appelé grâce à: this.NotifyPropertyChange (() => PageIndex);

Ajoutez ceci dans la classe NotificationExtensions:

    /// <summary>
    /// <para>Lève l'évènement de changement de valeur sur l'objet <paramref name="sender"/>
    /// pour la propriété utilisée dans la lambda <paramref name="property"/>.</para>
    /// </summary>
    /// <param name="sender">L'objet portant la propriété et l'évènement.</param>
    /// <param name="property">Une expression lambda utilisant la propriété subissant la modification.</param>
    public static void NotifyPropertyChange(this INotifyPropertyChanged sender, Expression<Func<Object>> property)
    {
        if (sender == null)
            return;

        // Récupère le nom de la propriété utilisée dans la lambda en argument
        LambdaExpression lambda = property as LambdaExpression;
        MemberExpression memberExpression;
        if (lambda.Body is UnaryExpression)
        {
            UnaryExpression unaryExpression = lambda.Body as UnaryExpression;
            memberExpression = unaryExpression.Operand as MemberExpression;
        }
        else
        {
            memberExpression = lambda.Body as MemberExpression;
        }
        ConstantExpression constantExpression = memberExpression.Expression as ConstantExpression;
        PropertyInfo propertyInfo = memberExpression.Member as PropertyInfo;


        // il faut remonter la hierarchie, car meme public, un event n est pas visible dans les enfants
        FieldInfo eventField;
        Type baseType = sender.GetType();
        do
        {
            eventField = baseType.GetField(INotifyPropertyChangedEventFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
            baseType = baseType.BaseType;
        } while (eventField == null);

        // on a trouvé l'event, on peut invoquer tt les delegates liés
        MulticastDelegate eventDelegate = eventField.GetValue(sender) as MulticastDelegate;
        if (eventDelegate == null) return; // l'event n'est bindé à aucun delegate
        foreach (Delegate handler in eventDelegate.GetInvocationList())
        {
            handler.Method.Invoke(handler.Target, new Object[] { sender, new PropertyChangedEventArgs(propertyInfo.Name) });
        }
    }
1
champier

Il n'y a pas d'implémentation unique de Property Changed qui peut gérer toutes les façons dont les gens veulent l'utiliser. le meilleur pari est de générer une classe d'aide pour faire le travail pour vous voici un exemple de celui que j'utilise

/// <summary>
/// Helper Class that automates most of the actions required to implement INotifyPropertyChanged
/// </summary>
public static class HPropertyChanged
{
    private static Dictionary<string, PropertyChangedEventArgs> argslookup = new Dictionary<string, PropertyChangedEventArgs>();
    public static string ThisPropertyName([CallerMemberName]string name = "")
    {
        return name;
    }

    public static string GetPropertyName<T>(Expression<Func<T>> exp)
    {
        string rtn = "";
        MemberExpression mex = exp.Body as MemberExpression;
        if(mex!=null)
            rtn = mex.Member.Name;
        return rtn;
    }

    public static void SetValue<T>(ref T target, T newVal, object sender, PropertyChangedEventHandler handler, params string[] changed)
    {
        if (!target.Equals(newVal))
        {
            target = newVal;
            PropertyChanged(sender, handler, changed);
        }
    }
    public static void SetValue<T>(ref T target, T newVal, Action<PropertyChangedEventArgs> handler, params string[] changed)
    {
        if (!target.Equals(newVal))
        {
            target = newVal;
            foreach (var item in changed)
            {
                handler(GetArg(item));
            }
        }
    }

    public static void PropertyChanged(object sender,PropertyChangedEventHandler handler,params string[] changed)
    {
        if (handler!=null)
        {
            foreach (var prop in changed)
            {
                handler(sender, GetArg(prop));
            }
        }
    }
    public static PropertyChangedEventArgs GetArg(string name)
    {
        if (!argslookup.ContainsKey(name)) argslookup.Add(name, new PropertyChangedEventArgs(name));
        return argslookup[name];
    }
}

edit: il a été suggéré que je passe d'une classe d'aide à un wrapper de valeur et j'utilise depuis celui-ci et je trouve que cela fonctionne assez bien

public class NotifyValue<T>
{
    public static implicit operator T(NotifyValue<T> item)
    {
        return item.Value;
    }

    public NotifyValue(object parent, T value = default(T), PropertyChangingEventHandler changing = null, PropertyChangedEventHandler changed = null, params object[] dependenies)
    {
        _parent = parent;
        _propertyChanged = changed;
        _propertyChanging = changing;

        if (_propertyChanged != null)
        {
            _propertyChangedArg =
                dependenies.OfType<PropertyChangedEventArgs>()
                .Union(
                    from d in dependenies.OfType<string>()
                    select new PropertyChangedEventArgs(d)
                );

        }
        if (_propertyChanging != null)
        {
            _propertyChangingArg =
                dependenies.OfType<PropertyChangingEventArgs>()
                .Union(
                    from d in dependenies.OfType<string>()
                    select new PropertyChangingEventArgs(d)
                );
        }
        _PostChangeActions = dependenies.OfType<Action>();

    }

    private T _Value;

    public T Value
    {
        get { return _Value; }
        set
        {
            SetValue(value);
        }
    }

    public bool SetValue(T value)
    {
        if (!EqualityComparer<T>.Default.Equals(_Value, value))
        {
            OnPropertyChnaging();
            _Value = value;
            OnPropertyChnaged();
            foreach (var action in _PostChangeActions)
            {
                action();
            }
            return true;
        }
        else
            return false;
    }

    private void OnPropertyChnaged()
    {
        var handler = _propertyChanged;
        if (handler != null)
        {
            foreach (var arg in _propertyChangedArg)
            {
                handler(_parent, arg);
            }           
        }
    }

    private void OnPropertyChnaging()
    {
        var handler = _propertyChanging;
        if(handler!=null)
        {
            foreach (var arg in _propertyChangingArg)
            {
                handler(_parent, arg);
            }
        }
    }

    private object _parent;
    private PropertyChangedEventHandler _propertyChanged;
    private PropertyChangingEventHandler _propertyChanging;
    private IEnumerable<PropertyChangedEventArgs> _propertyChangedArg;
    private IEnumerable<PropertyChangingEventArgs> _propertyChangingArg;
    private IEnumerable<Action> _PostChangeActions;
}

exemple d'utilisation

private NotifyValue<int> _val;
public const string ValueProperty = "Value";
public int Value
{
    get { return _val.Value; }
    set { _val.Value = value; }
}

puis en constructeur vous faites

_val = new NotifyValue<int>(this,0,PropertyChanged,PropertyChanging,ValueProperty );
1
MikeT