Implémentation d'INotifyPropertyChanged - existe-t-il un meilleur moyen?

647

Microsoft aurait dû implémenter quelque chose de rapide INotifyPropertyChanged, comme dans les propriétés automatiques, spécifiez simplement {get; set; notify;} que je pense que cela a beaucoup de sens de le faire. Ou y a-t-il des complications pour le faire?

Pouvons-nous nous-mêmes implémenter quelque chose comme «notifier» dans nos propriétés. Existe-t-il une solution gracieuse pour l'implémentation INotifyPropertyChangeddans votre classe ou la seule façon de le faire est de déclencher l' PropertyChangedévénement dans chaque propriété.

Sinon, pouvons-nous écrire quelque chose pour générer automatiquement le morceau de code pour déclencher l' PropertyChanged événement?

PK
la source
7
code.google.com/p/notifypropertyweaver peut être utile
Ian Ringrose
7
le lien ci-dessus est mort. github.com/SimonCropp/NotifyPropertyWeaver
prime23
2
Vous pouvez utiliser DependencyObject et DependencyProperties à la place. HA! J'ai fait un drôle.
Phil
5
À l'époque, il n'était pas possible d'apporter des modifications à C # étant donné que nous avions un énorme arriéré d'interdépendances. Donc, à la naissance de MVVM, je suppose que nous n'avons vraiment pas fait beaucoup d'efforts pour résoudre ce problème et je sais que l'équipe Patterns & Practices a fait quelques essais en cours de route (d'où vous avez également obtenu MEF dans le cadre de cela fil de recherche). Aujourd'hui, je pense que [CallerMemberName] est la réponse à ce qui précède.
Scott Barnes

Réponses:

633

Sans utiliser quelque chose comme postsharp, la version minimale que j'utilise utilise quelque chose comme:

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler handler = PropertyChanged;
        if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
    }
    protected bool SetField<T>(ref T field, T value, string propertyName)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return false;
        field = value;
        OnPropertyChanged(propertyName);
        return true;
    }

    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}

Chaque propriété est alors juste quelque chose comme:

    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }

ce qui n'est pas énorme; il peut également être utilisé comme classe de base si vous le souhaitez. Le boolretour de SetFieldvous indique s'il s'agissait d'un no-op, au cas où vous souhaiteriez appliquer une autre logique.


ou encore plus simple avec C # 5:

protected bool SetField<T>(ref T field, T value,
    [CallerMemberName] string propertyName = null)
{...}

qui peut être appelé comme ceci:

set { SetField(ref name, value); }

avec lequel le compilateur ajoutera "Name"automatiquement.


C # 6.0 facilite l'implémentation:

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

... et maintenant avec C # 7:

protected void OnPropertyChanged(string propertyName)
   => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));

protected bool SetField<T>(ref T field, T value,[CallerMemberName] string propertyName =  null)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(propertyName);
    return true;
}

private string name;
public string Name
{
    get => name;
    set => SetField(ref name, value);
}
Marc Gravell
la source
4
Belle astuce Marc! J'ai suggéré une amélioration pour utiliser une expression lambda au lieu du nom de la propriété, voir ma réponse
Thomas Levesque
7
@Thomas - le lambda va bien, mais il ajoute beaucoup de frais généraux pour quelque chose qui est en fait très simple. Un truc pratique, mais je ne suis pas sûr que ce soit toujours pratique.
Marc Gravell
14
@Marc - Oui, cela peut probablement dégrader les performances ... Cependant, j'aime vraiment le fait qu'il soit vérifié au moment de la compilation et qu'il soit correctement refactorisé par la commande "Renommer"
Thomas Levesque
4
@Gusdor heureusement, avec C # 5 il n'y a pas besoin de faire de compromis - vous pouvez tirer le meilleur parti des deux via (comme le note Pedro77)[CallerMemberName]
Marc Gravell
4
@Gusdor le langage et le framework sont séparés; vous pouvez utiliser le compilateur C # 5, cibler .NET 4 et simplement ajouter l'attribut manquant vous - même - cela fonctionnera bien. Il doit juste avoir le bon nom et être dans le bon espace de noms. Il n'a pas besoin d'être dans un assemblage spécifique.
Marc Gravell
196

Depuis .Net 4.5, il existe enfin un moyen simple de le faire.

.Net 4.5 introduit un nouvel attribut d'informations sur l'appelant.

private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
     // make sure only to call this if the value actually changes

     var handler = PropertyChanged;
     if (handler != null) {
        handler(this, new PropertyChangedEventArgs(caller));
     }
}

C'est probablement une bonne idée d'ajouter également un comparateur à la fonction.

EqualityComparer<T>.Default.Equals

Plus d'exemples ici et ici

Voir également Informations sur l'appelant (C # et Visual Basic)

Daniel Little
la source
12
Brillant! Mais pourquoi est-ce générique?
abatishchev
@abatishchev Je suppose que ce n'est pas forcément le cas, je jouais simplement avec l'idée que la fonction définisse également la propriété. Je vais voir si je peux mettre à jour ma réponse pour fournir la solution complète. Les exemples supplémentaires font un bon travail en attendant.
Daniel Little
3
Il a été introduit par C # 5.0. Cela n'a rien à voir avec .net 4.5, mais c'est une excellente solution!
J. Lennon
5
@J. Lennon .net 4.5 a encore quelque chose à voir avec cela, après tout l'attribut vient de quelque part msdn.microsoft.com/en-au/library/…
Daniel Little
@Lavinski changez votre application par exemple en .NET 3.5 et voyez ce qui fonctionnera (en vs2012)
J. Lennon
162

J'aime vraiment la solution de Marc, mais je pense qu'elle peut être légèrement améliorée pour éviter d'utiliser une "chaîne magique" (qui ne supporte pas le refactoring). Au lieu d'utiliser le nom de la propriété comme chaîne, il est facile d'en faire une expression lambda:

private string name;
public string Name
{
    get { return name; }
    set { SetField(ref name, value, () => Name); }
}

Ajoutez simplement les méthodes suivantes au code de Marc, cela fera l'affaire:

protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
    if (selectorExpression == null)
        throw new ArgumentNullException("selectorExpression");
    MemberExpression body = selectorExpression.Body as MemberExpression;
    if (body == null)
        throw new ArgumentException("The body must be a member expression");
    OnPropertyChanged(body.Member.Name);
}

protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
{
    if (EqualityComparer<T>.Default.Equals(field, value)) return false;
    field = value;
    OnPropertyChanged(selectorExpression);
    return true;
}

BTW, cela a été inspiré par l' URL mise à jour de cet article de blog

Thomas Levesque
la source
6
Il existe au moins un framework utilisant cette méthode, ReactiveUI .
AlSki
Très tard, cela signifiait passer par la réflexion, ce qui signifiait un coup de performance. Cela pourrait être acceptable, mais définir une propriété n'est pas un endroit où j'aimerais que mon application passe sur plusieurs cycles.
Bruno Brant
1
@BrunoBrant Êtes-vous sûr qu'il y a un impact sur les performances? Selon le blog, la réflexion se produit pendant la compilation plutôt que pendant l'exécution (c'est-à-dire la réflexion statique).
Nathaniel Elkins
6
Je crois que votre OnPropertyChanged <T> est obsolète avec l'opérateur nameof de C # 6, ce qui rend ce monstre un peu plus élégant.
Traubenfuchs
5
@Traubenfuchs, en fait, l'attribut CallerMemberName de C # 5 le rend encore plus simple, car vous n'avez pas besoin de passer quoi que ce soit ...
Thomas Levesque
120

Il y a aussi Fody qui a un complément PropertyChanged , qui vous permet d'écrire ceci:

[ImplementPropertyChanged]
public class Person 
{        
    public string GivenNames { get; set; }
    public string FamilyName { get; set; }
}

... et lors de la compilation injecte des notifications de modification de propriété.

Tom Gilder
la source
7
Je pense que c'est exactement ce que OP recherchait quand ils ont demandé "Pouvons-nous nous-mêmes implémenter quelque chose comme" notifier "dans nos propriétés. Existe-t-il une solution gracieuse pour implémenter INotifyPropertyChanged dans votre classe"
Ashoat
3
C'est vraiment la seule solution gracieuse, et cela fonctionne parfaitement comme l'a dit @CADbloke. Et j'étais également sceptique à propos du tisserand, mais j'ai vérifié / revérifié le code IL derrière et c'est parfait, c'est simple, fait tout ce dont vous avez besoin et rien d'autre. Il accroche et appelle également le nom de méthode que vous avez désigné dans la classe de base, que NotifyOnProp ..., OnNotify ... n'ait pas d'importance, donc fonctionne bien avec n'importe quelle classe de base que vous pourriez avoir et qui implémente INotify .. .
NSGaga, pour la plupart inactif,
1
Vous pouvez facilement vérifier ce que fait le tisserand, regarder la fenêtre de sortie de la construction, il répertorie toutes les choses PropertyChanged qu'il a tissées. L'utilisation de l'extension VScolorOutput avec le motif d'expression régulière la "Fody/.*?:",LogCustom2,Truemet en surbrillance dans la couleur "Personnalisé 2". Je l'ai fait rose vif donc c'est facile à trouver. Juste tout Fody, c'est la meilleure façon de faire tout ce qui a beaucoup de frappe répétitive.
CAD bloke
@mahmoudnezarsarhan non, ce n'est pas le cas, je me souviens qu'il y a eu un léger changement dans la façon dont il doit être configuré, mais Fody PropertyChanged est toujours vivant et actif.
Larry
65

Je pense que les gens devraient accorder un peu plus d'attention à la performance; cela a vraiment un impact sur l'interface utilisateur lorsqu'il y a beaucoup d'objets à lier (pensez à une grille avec plus de 10000 lignes), ou si la valeur de l'objet change fréquemment (application de surveillance en temps réel).

J'ai pris diverses implémentations trouvées ici et ailleurs et j'ai fait une comparaison; consultez la comparaison des performances des implémentations INotifyPropertyChanged .


Voici un aperçu du résultat Implemenation vs Runtime

Peijen
la source
14
-1: il n'y a pas de surcharge de performances: CallerMemberName sont transformés en valeurs littérales au moment de la compilation. Essayez de décompiler votre application.
JYL
voici la question et la réponse correspondantes: stackoverflow.com/questions/22580623/…
uli78
1
@JYL, vous avez raison de dire que CallerMemberName n'a pas ajouté de surcharge importante. J'ai dû implémenter quelque chose de mal la dernière fois que je l'ai essayé. Je mettrai à jour le blog et répondrai pour refléter la référence pour la mise en œuvre de CallerMemberName et Fody plus tard.
Peijen
1
Si vous avez une grille de 10 000+ dans l'interface utilisateur, vous devriez probablement combiner des approches pour gérer les performances, comme la pagination où vous n'affichez que 10, 50, 100, 250 hits par page ...
Austin Rhymer
Austin Rhymer, si vous avez de grandes données + 50 utilisez la virtualisation de données, pas besoin de charger toutes les données, il ne chargera que les données qui sont visibles sur la zone affichée de défilement actuelle!
Bilal
38

J'introduis une classe Bindable dans mon blog à http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable utilise un dictionnaire comme sac de propriétés. Il est assez facile d'ajouter les surcharges nécessaires à une sous-classe pour gérer son propre champ de sauvegarde à l'aide des paramètres ref.

  • Pas de chaîne magique
  • Pas de réflexion
  • Peut être amélioré pour supprimer la recherche de dictionnaire par défaut

Le code:

public class Bindable : INotifyPropertyChanged {
    private Dictionary<string, object> _properties = new Dictionary<string, object>();

    /// <summary>
    /// Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    protected T Get<T>([CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T)value;
        return default(T);
    }

    /// <summary>
    /// Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    /// <remarks>Use this overload when implicitly naming the property</remarks>
    protected void Set<T>(T value, [CallerMemberName] string name = null) {
        Debug.Assert(name != null, "name != null");
        if (Equals(value, Get<T>(name)))
            return;
        _properties[name] = value;
        OnPropertyChanged(name);
    }

    public event PropertyChangedEventHandler PropertyChanged;

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

Il peut être utilisé comme ceci:

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}
TiMoch
la source
2
C'est une bonne solution, mais le seul inconvénient est qu'il y a un petit succès de performance impliquant la boxe / unboxing.
MCattle
1
Je suggérerais d'utiliser protected T Get<T>(T defaultValue, [CallerMemberName] string name = null)et de vérifier également if (_properties.ContainsKey(name) && Equals(value, Get<T>(default(T), name)))Set (pour augmenter et enregistrer lors de la première définition de la valeur par défaut)
Miquel
1
@Miquel ajoutant la prise en charge des valeurs par défaut personnalisées peut être utile à coup sûr, mais vous devez faire attention de ne déclencher l'événement modifié que lorsque la valeur a réellement changé. La définition d'une propriété à la même valeur qu'elle ne doit pas déclencher d'événements. Je dois admettre que dans la plupart des cas, il est inoffensif, mais j'ai été mordu plusieurs fois avec des propriétés définies des milliers de fois à la même valeur avec des événements détruisant la réactivité de l'interface utilisateur.
TiMoch
1
@stakx J'ai quelques applications qui s'appuient sur cela pour prendre en charge le modèle de souvenir pour annuler / rétablir ou pour activer l'unité de travail dans les applications où nhibernate n'est pas utilisable
TiMoch
1
J'aime vraiment cette solution particulière: notation courte, pas de truc de proxy dynamique, pas d'ingérence IL, etc. Bien que, vous pouvez la raccourcir en supprimant la nécessité de spécifier T à chaque fois pour Get en rendant Get return dynamique. Je sais, cela a un impact sur les performances d'exécution, mais maintenant le code pour les getters et les setters peut enfin être toujours le même et en une seule ligne , louez le Seigneur! PS, vous devez prendre des précautions supplémentaires à l'intérieur de votre méthode Get (une fois lorsque vous écrivez la classe de base) lorsque vous retournez des valeurs par défaut pour les types de valeurs comme dynamiques. Assurez-vous de toujours renvoyer les valeurs par défaut correctes (cela peut être fait)
evilkos
15

Je n'ai pas encore eu l'occasion d'essayer moi-même, mais la prochaine fois que je met en place un projet avec une grande exigence pour INotifyPropertyChanged, j'ai l'intention d'écrire un attribut Postsharp qui injectera le code lors de la compilation. Quelque chose comme:

[NotifiesChange]
public string FirstName { get; set; }

Va devenir:

private string _firstName;

public string FirstName
{
   get { return _firstname; }
   set
   {
      if (_firstname != value)
      {
          _firstname = value;
          OnPropertyChanged("FirstName")
      }
   }
}

Je ne sais pas si cela fonctionnera dans la pratique et je dois m'asseoir et l'essayer, mais je ne vois pas pourquoi. Je vais peut-être devoir lui faire accepter certains paramètres pour les situations où plus d'un OnPropertyChanged doit être déclenché (si, par exemple, j'avais une propriété FullName dans la classe ci-dessus)

Actuellement, j'utilise un modèle personnalisé dans Resharper, mais même avec cela, j'en ai marre de toutes mes propriétés étant si longues.


Ah, une recherche rapide sur Google (que j'aurais dû faire avant d'écrire ceci) montre qu'au moins une personne a déjà fait quelque chose comme ça ici . Pas exactement ce que j'avais en tête, mais assez proche pour montrer que la théorie est bonne.

Martin Harris
la source
6
Un outil gratuit appelé Fody semble faire la même chose, fonctionnant comme un injecteur de code générique à la compilation. Il est téléchargeable dans Nuget, tout comme ses packages de plugins PropertyChanged et PropertyChanging.
Triynko
11

Oui, une meilleure façon existe certainement. C'est ici:

Tutoriel étape par étape rétréci par moi, basé sur cet article utile .

  • Créer un nouveau projet
  • Installer le package de base du château dans le projet

Install-Package Castle.Core

  • Installer uniquement les bibliothèques de lumière mvvm

Install-Package MvvmLightLibs

  • Ajoutez deux classes dans le projet:

NotifierInterceptor

public class NotifierInterceptor : IInterceptor
    {
        private PropertyChangedEventHandler handler;
        public static Dictionary<String, PropertyChangedEventArgs> _cache =
          new Dictionary<string, PropertyChangedEventArgs>();

        public void Intercept(IInvocation invocation)
        {
            switch (invocation.Method.Name)
            {
                case "add_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                case "remove_PropertyChanged":
                    handler = (PropertyChangedEventHandler)
                              Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
                    invocation.ReturnValue = handler;
                    break;
                default:
                    if (invocation.Method.Name.StartsWith("set_"))
                    {
                        invocation.Proceed();
                        if (handler != null)
                        {
                            var arg = retrievePropertyChangedArg(invocation.Method.Name);
                            handler(invocation.Proxy, arg);
                        }
                    }
                    else invocation.Proceed();
                    break;
            }
        }

        private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
        {
            PropertyChangedEventArgs arg = null;
            _cache.TryGetValue(methodName, out arg);
            if (arg == null)
            {
                arg = new PropertyChangedEventArgs(methodName.Substring(4));
                _cache.Add(methodName, arg);
            }
            return arg;
        }
    }

ProxyCreator

public class ProxyCreator
{
    public static T MakeINotifyPropertyChanged<T>() where T : class, new()
    {
        var proxyGen = new ProxyGenerator();
        var proxy = proxyGen.CreateClassProxy(
          typeof(T),
          new[] { typeof(INotifyPropertyChanged) },
          ProxyGenerationOptions.Default,
          new NotifierInterceptor()
          );
        return proxy as T;
    }
}
  • Créez votre modèle de vue, par exemple:

-

 public class MainViewModel
    {
        public virtual string MainTextBox { get; set; }

        public RelayCommand TestActionCommand
        {
            get { return new RelayCommand(TestAction); }
        }

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • Mettez des liaisons dans xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
  • Placez la ligne de code dans le fichier code-derrière MainWindow.xaml.cs comme ceci:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Prendre plaisir.

entrez la description de l'image ici

Attention!!! Toutes les propriétés délimitées doivent être décorées avec un mot-clé virtuel car elles sont utilisées par le proxy du château pour remplacer.

testCoder
la source
Je suis intéressé de savoir quelle version de Castle vous utilisez. J'utilise 3.3.0 et la méthode CreateClassProxy n'ai pas ces paramètres: type, interfaces to apply, interceptors.
IAbstract
Peu importe, j'utilisais la CreateClassProxy<T>méthode générique . Beaucoup différent ... hmmm, se demandant pourquoi si limité avec la méthode générique. :(
IAbstract
7

Une approche très semblable à AOP consiste à injecter la substance INotifyPropertyChanged sur un objet déjà instancié à la volée. Vous pouvez le faire avec quelque chose comme Castle DynamicProxy. Voici un article qui explique la technique:

Ajout de INotifyPropertyChanged à un objet existant

HokieMike
la source
5

Regardez ici: http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx

Il est écrit en allemand, mais vous pouvez télécharger le ViewModelBase.cs. Tous les commentaires du fichier cs sont écrits en anglais.

Avec cette classe ViewModelBase, il est possible d'implémenter des propriétés pouvant être liées similaires aux propriétés de dépendance bien connues:

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}
DotNetMastermind
la source
1
Le lien est rompu.
Guge
4

Sur la base de la réponse de Thomas qui a été adaptée d'une réponse de Marc, j'ai transformé le code modifié de la propriété réfléchissante en une classe de base:

public abstract class PropertyChangedBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

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

    protected void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
    {
        if (selectorExpression == null)
            throw new ArgumentNullException("selectorExpression");
        var me = selectorExpression.Body as MemberExpression;

        // Nullable properties can be nested inside of a convert function
        if (me == null)
        {
            var ue = selectorExpression.Body as UnaryExpression;
            if (ue != null)
                me = ue.Operand as MemberExpression;
        }

        if (me == null)
            throw new ArgumentException("The body must be a member expression");

        OnPropertyChanged(me.Member.Name);
    }

    protected void SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression, params Expression<Func<object>>[] additonal)
    {
        if (EqualityComparer<T>.Default.Equals(field, value)) return;
        field = value;
        OnPropertyChanged(selectorExpression);
        foreach (var item in additonal)
            OnPropertyChanged(item);
    }
}

L'utilisation est la même que la réponse de Thomas, sauf que vous pouvez transmettre des propriétés supplémentaires à notifier. Cela était nécessaire pour gérer les colonnes calculées qui doivent être actualisées dans une grille.

private int _quantity;
private int _price;

public int Quantity 
{ 
    get { return _quantity; } 
    set { SetField(ref _quantity, value, () => Quantity, () => Total); } 
}
public int Price 
{ 
    get { return _price; } 
    set { SetField(ref _price, value, () => Price, () => Total); } 
}
public int Total { get { return _price * _quantity; } }

J'ai ceci conduisant une collection d'éléments stockés dans une BindingList exposée via un DataGridView. Cela a éliminé le besoin pour moi d'effectuer des appels Refresh () manuels sur la grille.

StuffOfInterest
la source
4

Permettez-moi de vous présenter ma propre approche appelée Yappi . Il appartient aux générateurs de classes dérivés du proxy Runtime, ajoutant de nouvelles fonctionnalités à un objet ou un type existant, comme le proxy dynamique de Caste Project.

Il permet d'implémenter INotifyPropertyChanged une fois dans la classe de base, puis de déclarer les classes dérivées dans le style suivant, en prenant toujours en charge INotifyPropertyChanged pour les nouvelles propriétés:

public class Animal:Concept
{
    protected Animal(){}
    public virtual string Name { get; set; }
    public virtual int Age { get; set; }
}

La complexité de la classe dérivée ou de la construction du proxy peut être masquée derrière la ligne suivante:

var animal = Concept.Create<Animal>.New();

Et tout le travail d'implémentation INotifyPropertyChanged peut être effectué comme ceci:

public class Concept:INotifyPropertyChanged
{
    //Hide constructor
    protected Concept(){}

    public static class Create<TConcept> where TConcept:Concept
    {
        //Construct derived Type calling PropertyProxy.ConstructType
        public static readonly Type Type = PropertyProxy.ConstructType<TConcept, Implementation<TConcept>>(new Type[0], true);
        //Create constructing delegate calling Constructor.Compile
        public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
    }


    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
    {
        var caller = PropertyChanged;
        if(caller!=null)
        {
            caller(this, eventArgs);
        }
    }

    //define implementation
    public class Implementation<TConcept> : DefaultImplementation<TConcept> where TConcept:Concept
    {
        public override Func<TBaseType, TResult> OverrideGetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            return PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
        }
        /// <summary>
        /// Overriding property setter implementation.
        /// </summary>
        /// <typeparam name="TBaseType">Base type for implementation. TBaseType must be TConcept, and inherits all its constraints. Also TBaseType is TDeclaringType.</typeparam>
        /// <typeparam name="TDeclaringType">Type, declaring property.</typeparam>
        /// <typeparam name="TConstructedType">Constructed type. TConstructedType is TDeclaringType and TBaseType.</typeparam>
        /// <typeparam name="TResult">Type of property.</typeparam>
        /// <param name="property">PropertyInfo of property.</param>
        /// <returns>Delegate, corresponding to property setter implementation.</returns>
        public override Action<TBaseType, TResult> OverrideSetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
        {
            //This code called once for each declared property on derived type's initialization.
            //EventArgs instance is shared between all events for each concrete property.
            var eventArgs = new PropertyChangedEventArgs(property.Name);
            //get delegates for base calls.
            Action<TBaseType, TResult> setter = PropertyImplementation<TBaseType, TDeclaringType>.GetSetter<TResult>(property.Name);
            Func<TBaseType, TResult> getter = PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);

            var comparer = EqualityComparer<TResult>.Default;

            return (pthis, value) =>
            {//This code executes each time property setter is called.
                if (comparer.Equals(value, getter(pthis))) return;
                //base. call
                setter(pthis, value);
                //Directly accessing Concept's protected method.
                pthis.OnPropertyChanged(eventArgs);
            };
        }
    }
}

Il est entièrement sûr pour le refactoring, n'utilise aucune réflexion après la construction du type et assez rapidement.

Kelqualyn
la source
Pourquoi avez-vous besoin d' TDeclarationun paramètre de type PropertyImplementation? Vous pouvez sûrement trouver le type approprié pour appeler (pas callvirt) le getter / setter avec seulement TImplementation?
Andrew Savinykh
La mise en œuvre fonctionne dans la plupart des cas. Les exceptions sont: 1. Les propriétés redéfinies avec le "nouveau" mot clé C #. 2. Propriétés d'implémentation d'interface explicite.
Kelqualyn
3

Toutes ces réponses sont très agréables.

Ma solution utilise les extraits de code pour faire le travail.

Cela utilise l'appel le plus simple à l'événement PropertyChanged.

Enregistrez cet extrait de code et utilisez-le lorsque vous utilisez l'extrait de code "fullprop".

l'emplacement peut être trouvé dans le menu 'Tools \ Code Snippet Manager ...' de Visual Studio.

<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets  xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
    <CodeSnippet Format="1.0.0">
        <Header>
            <Title>inotifypropfull</Title>
            <Shortcut>inotifypropfull</Shortcut>
            <HelpUrl>http://ofirzeitoun.wordpress.com/</HelpUrl>
            <Description>Code snippet for property and backing field with notification</Description>
            <Author>Ofir Zeitoun</Author>
            <SnippetTypes>
                <SnippetType>Expansion</SnippetType>
            </SnippetTypes>
        </Header>
        <Snippet>
            <Declarations>
                <Literal>
                    <ID>type</ID>
                    <ToolTip>Property type</ToolTip>
                    <Default>int</Default>
                </Literal>
                <Literal>
                    <ID>property</ID>
                    <ToolTip>Property name</ToolTip>
                    <Default>MyProperty</Default>
                </Literal>
                <Literal>
                    <ID>field</ID>
                    <ToolTip>The variable backing this property</ToolTip>
                    <Default>myVar</Default>
                </Literal>
            </Declarations>
            <Code Language="csharp">
                <![CDATA[private $type$ $field$;

    public $type$ $property$
    {
        get { return $field$;}
        set { 
            $field$ = value;
            var temp = PropertyChanged;
            if (temp != null)
            {
                temp(this, new PropertyChangedEventArgs("$property$"));
            }
        }
    }
    $end$]]>
            </Code>
        </Snippet>
    </CodeSnippet>
</CodeSnippets>

Vous pouvez modifier l'appel à votre guise (pour utiliser les solutions ci-dessus)

Ofir
la source
2

Si vous utilisez la dynamique dans .NET 4.5, vous n'avez pas à vous en préoccuper INotifyPropertyChanged.

dynamic obj = new ExpandoObject();
obj.Name = "John";

si Name est lié à un certain contrôle, cela fonctionne très bien.

Dilshod
la source
1
des inconvénients à utiliser cela?
juFo
2

Une autre solution combinée utilise StackFrame:

public class BaseViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void Set<T>(ref T field, T value)
    {
        MethodBase method = new StackFrame(1).GetMethod();
        field = value;
        Raise(method.Name.Substring(4));
    }

    protected void Raise(string propertyName)
    {
        var temp = PropertyChanged;
        if (temp != null)
        {
            temp(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Usage:

public class TempVM : BaseViewModel
{
    private int _intP;
    public int IntP
    {
        get { return _intP; }
        set { Set<int>(ref _intP, value); }
    }
}
Ofir
la source
2
C'est rapide? L'accès au cadre de pile n'est-il pas lié à une condition d'autorisation? Est-ce robuste dans un contexte d'utilisation asynchrone / wait?
Stéphane Gourichon
@ StéphaneGourichon Non, ce n'est pas le cas. L'accès au cadre de pile signifie un impact considérable sur les performances dans la plupart des cas.
Bruno Brant
Oui, vous pouvez le voir sur codereview.stackexchange.com/questions/13823/…
Ofir
Notez que l'inlining peut masquer la get_Foométhode en mode Release.
bytecode77
2

J'ai créé une méthode d'extension dans ma bibliothèque de base pour la réutilisation:

public static class INotifyPropertyChangedExtensions
{
    public static bool SetPropertyAndNotify<T>(this INotifyPropertyChanged sender,
               PropertyChangedEventHandler handler, ref T field, T value, 
               [CallerMemberName] string propertyName = "",
               EqualityComparer<T> equalityComparer = null)
    {
        bool rtn = false;
        var eqComp = equalityComparer ?? EqualityComparer<T>.Default;
        if (!eqComp.Equals(field,value))
        {
            field = value;
            rtn = true;
            if (handler != null)
            {
                var args = new PropertyChangedEventArgs(propertyName);
                handler(sender, args);
            }
        }
        return rtn;
    }
}

Cela fonctionne avec .Net 4.5 en raison de CallerMemberNameAttribute . Si vous souhaitez l'utiliser avec une version antérieure de .Net, vous devez modifier la déclaration de méthode de: ...,[CallerMemberName] string propertyName = "", ...à...,string propertyName, ...

Usage:

public class Dog : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    string _name;

    public string Name
    {
        get { return _name; }
        set
        {
            this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
        }
    }
}
giammin
la source
2

J'ai résolu de cette façon (c'est un peu laborieux, mais c'est sûrement le plus rapide en runtime).

En VB (désolé, mais je pense que ce n'est pas difficile de le traduire en C #), je fais cette substitution avec RE:

(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)( |\r\n)*(?<Def>(Public|Private|Friend|Protected) .*Property )(?<Name>[^ ]*) As (?<Type>.*?)[ |\r\n](?![ |\r\n]*Get)

avec:

Private _${Name} As ${Type}\r\n${Attr}\r\n${Def}${Name} As ${Type}\r\nGet\r\nReturn _${Name}\r\nEnd Get\r\nSet (Value As ${Type})\r\nIf _${Name} <> Value Then \r\n_${Name} = Value\r\nRaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("${Name}"))\r\nEnd If\r\nEnd Set\r\nEnd Property\r\n

Cette transofrm tout le code comme ceci:

<Bindable(True)>
Protected Friend Property StartDate As DateTime?

Dans

Private _StartDate As DateTime?
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
    Get
        Return _StartDate
    End Get
    Set(Value As DateTime?)
        If _StartDate <> Value Then
            _StartDate = Value
            RaiseEvent PropertyChange(Me, New ComponentModel.PropertyChangedEventArgs("StartDate"))
        End If
    End Set
End Property

Et si je veux avoir un code plus lisible, je peux être l'inverse en faisant simplement la substitution suivante:

Private _(?<Name>.*) As (?<Type>.*)[\r\n ]*(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)[\r\n ]*(?<Def>(Public|Private|Friend|Protected) .*Property )\k<Name> As \k<Type>[\r\n ]*Get[\r\n ]*Return _\k<Name>[\r\n ]*End Get[\r\n ]*Set\(Value As \k<Type>\)[\r\n ]*If _\k<Name> <> Value Then[\r\n ]*_\k<Name> = Value[\r\n ]*RaiseEvent PropertyChanged\(Me, New (.*ComponentModel\.)PropertyChangedEventArgs\("\k<Name>"\)\)[\r\n ]*End If[\r\n ]*End Set[\r\n ]*End Property

Avec

${Attr} ${Def} ${Name} As ${Type}

Je lance pour remplacer le code IL de la méthode set, mais je ne peux pas écrire beaucoup de code compilé en IL ... Si un jour je l'écris, je vous le dirai!

Lucio Menci
la source
2

Je garde ça comme un extrait. C # 6 ajoute une belle syntaxe pour invoquer le gestionnaire.

// INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;

private void Set<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(property, value) == false)
    {
        property = value;
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
Mike Ward
la source
2

Voici une version Unity3D ou non CallerMemberName de NotifyPropertyChanged

public abstract class Bindable : MonoBehaviour, INotifyPropertyChanged
{
    private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
    private static readonly StackTrace stackTrace = new StackTrace();
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    ///     Resolves a Property's name from a Lambda Expression passed in.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="property"></param>
    /// <returns></returns>
    internal string GetPropertyName<T>(Expression<Func<T>> property)
    {
        var expression = (MemberExpression) property.Body;
        var propertyName = expression.Member.Name;

        Debug.AssertFormat(propertyName != null, "Bindable Property shouldn't be null!");
        return propertyName;
    }

    #region Notification Handlers

    /// <summary>
    ///     Notify's all other objects listening that a value has changed for nominated propertyName
    /// </summary>
    /// <param name="propertyName"></param>
    internal void NotifyOfPropertyChange(string propertyName)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    /// <summary>
    ///     Notifies subscribers of the property change.
    /// </summary>
    /// <typeparam name="TProperty">The type of the property.</typeparam>
    /// <param name="property">The property expression.</param>
    internal void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
    {
        var propertyName = GetPropertyName(property);
        NotifyOfPropertyChange(propertyName);
    }

    /// <summary>
    ///     Raises the <see cref="PropertyChanged" /> event directly.
    /// </summary>
    /// <param name="e">The <see cref="PropertyChangedEventArgs" /> instance containing the event data.</param>
    internal void OnPropertyChanged(PropertyChangedEventArgs e)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    #endregion

    #region Getters

    /// <summary>
    ///     Gets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    internal T Get<T>(Expression<Func<T>> property)
    {
        var propertyName = GetPropertyName(property);
        return Get<T>(GetPropertyName(property));
    }

    /// <summary>
    ///     Gets the value of a property automatically based on its caller.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    internal T Get<T>()
    {
        var name = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
        return Get<T>(name);
    }

    /// <summary>
    ///     Gets the name of a property based on a string.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="name"></param>
    /// <returns></returns>
    internal T Get<T>(string name)
    {
        object value = null;
        if (_properties.TryGetValue(name, out value))
            return value == null ? default(T) : (T) value;
        return default(T);
    }

    #endregion

    #region Setters

    /// <summary>
    ///     Sets the value of a property whilst automatically looking up its caller name.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    internal void Set<T>(T value)
    {
        var propertyName = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
        Set(value, propertyName);
    }

    /// <summary>
    ///     Sets the value of a property
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="name"></param>
    internal void Set<T>(T value, string propertyName)
    {
        Debug.Assert(propertyName != null, "name != null");
        if (Equals(value, Get<T>(propertyName)))
            return;
        _properties[propertyName] = value;
        NotifyOfPropertyChange(propertyName);
    }

    /// <summary>
    ///     Sets the value of a property based off an Expression (()=>FieldName)
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="value"></param>
    /// <param name="property"></param>
    internal void Set<T>(T value, Expression<Func<T>> property)
    {
        var propertyName = GetPropertyName(property);

        Debug.Assert(propertyName != null, "name != null");

        if (Equals(value, Get<T>(propertyName)))
            return;
        _properties[propertyName] = value;
        NotifyOfPropertyChange(propertyName);
    }

    #endregion
}

Ce code vous permet d'écrire des champs de support de propriété comme ceci:

  public string Text
    {
        get { return Get<string>(); }
        set { Set(value); }
    }

De plus, dans resharper, si vous créez un extrait de modèle / recherche, vous pouvez également automatiser votre flux de travail en convertissant des champs d'accessoires simples en support ci-dessus.

Modèle de recherche:

public $type$ $fname$ { get; set; }

Remplacer le motif:

public $type$ $fname$
{
    get { return Get<$type$>(); }
    set { Set(value); }
}
Scott Barnes
la source
2

J'ai écrit un article qui aide à ce sujet ( https://msdn.microsoft.com/magazine/mt736453 ). Vous pouvez utiliser le package NuSet SolSoft.DataBinding. Ensuite, vous pouvez écrire du code comme ceci:

public class TestViewModel : IRaisePropertyChanged
{
  public TestViewModel()
  {
    this.m_nameProperty = new NotifyProperty<string>(this, nameof(Name), null);
  }

  private readonly NotifyProperty<string> m_nameProperty;
  public string Name
  {
    get
    {
      return m_nameProperty.Value;
    }
    set
    {
      m_nameProperty.SetValue(value);
    }
  }

  // Plus implement IRaisePropertyChanged (or extend BaseViewModel)
}

Avantages:

  1. la classe de base est facultative
  2. aucune réflexion sur chaque «valeur définie»
  3. peuvent avoir des propriétés qui dépendent d'autres propriétés, et elles déclenchent toutes automatiquement les événements appropriés (l'article en a un exemple)
Mark Sowul
la source
2

Bien qu'il existe évidemment de nombreuses façons de le faire, à l'exception des réponses magiques AOP, aucune des réponses ne semble chercher à définir la propriété d'un modèle directement à partir du modèle de vue sans avoir de champ local à référencer.

Le problème est que vous ne pouvez pas référencer une propriété. Cependant, vous pouvez utiliser une action pour définir cette propriété.

protected bool TrySetProperty<T>(Action<T> property, T newValue, T oldValue, [CallerMemberName] string propertyName = null)
{
    if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
    {
        return false;
    }

    property(newValue);
    RaisePropertyChanged(propertyName);
    return true;
}

Cela peut être utilisé comme l'extrait de code suivant.

public int Prop {
    get => model.Prop;
    set => TrySetProperty(x => model.Prop = x, value, model.Prop);
}

Consultez ce dépôt BitBucket pour une implémentation complète de la méthode et quelques façons différentes d'obtenir le même résultat, y compris une méthode qui utilise LINQ et une méthode qui utilise la réflexion. Notez que ces méthodes sont plus lentes en termes de performances.

Dan
la source
1

Vous pouvez également prendre en compte lors de l'implémentation de ces types de propriétés le fait que les INotifyPropertyChang * ed * ing utilisent tous les deux des classes d'arguments d'événement.

Si vous avez un grand nombre de propriétés en cours de définition, le nombre d'instances de classe d'argument d'événement peut être énorme, vous devez envisager de les mettre en cache car elles sont l'une des zones dans lesquelles une explosion de chaîne peut se produire.

Jetez un œil à cette implémentation et expliquez pourquoi elle a été conçue.

Blog de Josh Smiths

Peter
la source
1

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. Cela fonctionne basé sur le 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é).

Ian Ringrose
la source
1

Une idée par réflexion:

class ViewModelBase : INotifyPropertyChanged {

    public event PropertyChangedEventHandler PropertyChanged;

    bool Notify<T>(MethodBase mb, ref T oldValue, T newValue) {

        // Get Name of Property
        string name = mb.Name.Substring(4);

        // Detect Change
        bool changed = EqualityComparer<T>.Default.Equals(oldValue, newValue);

        // Return if no change
        if (!changed) return false;

        // Update value
        oldValue = newValue;

        // Raise Event
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(name));
        }//if

        // Notify caller of change
        return true;

    }//method

    string name;

    public string Name {
        get { return name; }
        set {
            Notify(MethodInfo.GetCurrentMethod(), ref this.name, value);
        }
    }//method

}//class
Jack
la source
C'est plutôt cool, je l'aime plus que l'approche d'expression. À la baisse, devrait être plus lent.
nawfal
1

Je me rends compte que cette question a déjà des milliers de réponses, mais aucune ne me semblait tout à fait appropriée. Mon problème est que je ne veux pas de succès de performance et je suis prêt à supporter un peu de verbosité pour cette seule raison. Je ne me soucie pas non plus trop des propriétés automobiles, ce qui m'a conduit à la solution suivante:

public abstract class AbstractObject : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
    {
        //Set value if the new value is different from the old
        if (!Source.Equals(NewValue))
        {
            Source = NewValue;

            //Notify all applicable properties
            foreach (var i in Notify)
                OnPropertyChanged(i);

            return true;
        }

        return false;
    }

    public AbstractObject()
    {
    }
}

En d'autres termes, la solution ci-dessus est pratique si cela ne vous dérange pas:

public class SomeObject : AbstractObject
{
    public string AnotherProperty
    {
        get
        {
            return someProperty ? "Car" : "Plane";
        }
    }

    bool someProperty = false;
    public bool SomeProperty
    {
        get
        {
            return someProperty;
        }
        set
        {
            SetValue(ref someProperty, value, "SomeProperty", "AnotherProperty");
        }
    }

    public SomeObject() : base()
    {
    }
}

Avantages

  • Pas de réflexion
  • Avertit uniquement si l'ancienne valeur! = Nouvelle valeur
  • Notifier plusieurs propriétés à la fois

Les inconvénients

  • Pas de propriétés automatiques (vous pouvez cependant ajouter la prise en charge des deux!)
  • Un peu de verbosité
  • Boxe (petit coup de performance?)

Hélas, c'est encore mieux que de faire ça,

set
{
    if (!someProperty.Equals(value))
    {
        someProperty = value;
        OnPropertyChanged("SomeProperty");
        OnPropertyChanged("AnotherProperty");
    }
}

Pour chaque propriété, qui devient un cauchemar avec la verbosité supplémentaire ;-(

Remarque, je ne prétends pas que cette solution est meilleure en termes de performances que les autres, mais simplement que c'est une solution viable pour ceux qui n'aiment pas les autres solutions présentées.

James M
la source
1

Je suis venu avec cette classe de base pour implémenter le modèle observable, fait à peu près ce dont vous avez besoin ( implémenter "automatiquement" l'ensemble et obtenir). J'ai passé une heure en ligne sur ce prototype, donc il n'a pas beaucoup de tests unitaires, mais prouve le concept. Notez qu'il utilise le Dictionary<string, ObservablePropertyContext>pour supprimer le besoin de champs privés.

  public class ObservableByTracking<T> : IObservable<T>
  {
    private readonly Dictionary<string, ObservablePropertyContext> _expando;
    private bool _isDirty;

    public ObservableByTracking()
    {
      _expando = new Dictionary<string, ObservablePropertyContext>();

      var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
      foreach (var property in properties)
      {
        var valueContext = new ObservablePropertyContext(property.Name, property.PropertyType)
        {
          Value = GetDefault(property.PropertyType)
        };

        _expando[BuildKey(valueContext)] = valueContext;
      }
    }

    protected void SetValue<T>(Expression<Func<T>> expression, T value)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var originalValue = (T)_expando[key].Value;
      if (EqualityComparer<T>.Default.Equals(originalValue, value))
      {
        return;
      }

      _expando[key].Value = value;
      _isDirty = true;
    }

    protected T GetValue<T>(Expression<Func<T>> expression)
    {
      var keyContext = GetKeyContext(expression);
      var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);

      if (!_expando.ContainsKey(key))
      {
        throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
      }

      var value = _expando[key].Value;
      return (T)value;
    }

    private KeyContext GetKeyContext<T>(Expression<Func<T>> expression)
    {
      var castedExpression = expression.Body as MemberExpression;
      if (castedExpression == null)
      {
        throw new Exception($"Invalid expression.");
      }

      var parameterName = castedExpression.Member.Name;

      var propertyInfo = castedExpression.Member as PropertyInfo;
      if (propertyInfo == null)
      {
        throw new Exception($"Invalid expression.");
      }

      return new KeyContext {PropertyType = propertyInfo.PropertyType, PropertyName = parameterName};
    }

    private static string BuildKey(ObservablePropertyContext observablePropertyContext)
    {
      return $"{observablePropertyContext.Type.Name}.{observablePropertyContext.Name}";
    }

    private static string BuildKey(string parameterName, Type type)
    {
      return $"{type.Name}.{parameterName}";
    }

    private static object GetDefault(Type type)
    {
      if (type.IsValueType)
      {
        return Activator.CreateInstance(type);
      }
      return null;
    }

    public bool IsDirty()
    {
      return _isDirty;
    }

    public void SetPristine()
    {
      _isDirty = false;
    }

    private class KeyContext
    {
      public string PropertyName { get; set; }
      public Type PropertyType { get; set; }
    }
  }

  public interface IObservable<T>
  {
    bool IsDirty();
    void SetPristine();
  }

Voici l'utilisation

public class ObservableByTrackingTestClass : ObservableByTracking<ObservableByTrackingTestClass>
  {
    public ObservableByTrackingTestClass()
    {
      StringList = new List<string>();
      StringIList = new List<string>();
      NestedCollection = new List<ObservableByTrackingTestClass>();
    }

    public IEnumerable<string> StringList
    {
      get { return GetValue(() => StringList); }
      set { SetValue(() => StringIList, value); }
    }

    public IList<string> StringIList
    {
      get { return GetValue(() => StringIList); }
      set { SetValue(() => StringIList, value); }
    }

    public int IntProperty
    {
      get { return GetValue(() => IntProperty); }
      set { SetValue(() => IntProperty, value); }
    }

    public ObservableByTrackingTestClass NestedChild
    {
      get { return GetValue(() => NestedChild); }
      set { SetValue(() => NestedChild, value); }
    }

    public IList<ObservableByTrackingTestClass> NestedCollection
    {
      get { return GetValue(() => NestedCollection); }
      set { SetValue(() => NestedCollection, value); }
    }

    public string StringProperty
    {
      get { return GetValue(() => StringProperty); }
      set { SetValue(() => StringProperty, value); }
    }
  }
Homero Barbosa
la source
1

Je suggère d'utiliser ReactiveProperty. C'est la méthode la plus courte sauf Fody.

public class Data : INotifyPropertyChanged
{
    // boiler-plate
    ...
    // props
    private string name;
    public string Name
    {
        get { return name; }
        set { SetField(ref name, value, "Name"); }
    }
}

au lieu

public class Data
{
    // Don't need boiler-plate and INotifyPropertyChanged

    // props
    public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}

( DOCS )

soi
la source
0

Une autre idée...

 public class ViewModelBase : INotifyPropertyChanged
{
    private Dictionary<string, object> _propertyStore = new Dictionary<string, object>();
    protected virtual void SetValue<T>(T value, [CallerMemberName] string propertyName="") {
        _propertyStore[propertyName] = value;
        OnPropertyChanged(propertyName);
    }
    protected virtual T GetValue<T>([CallerMemberName] string propertyName = "")
    {
        object ret;
        if (_propertyStore.TryGetValue(propertyName, out ret))
        {
            return (T)ret;
        }
        else
        {
            return default(T);
        }
    }

    //Usage
    //public string SomeProperty {
    //    get { return GetValue<string>();  }
    //    set { SetValue(value); }
    //}

    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        var temp = PropertyChanged;
        if (temp != null)
            temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}
Toro
la source
0

=> voici ma solution avec les fonctionnalités suivantes

 public ResourceStatus Status
 {
     get { return _status; }
     set
     {
         _status = value;
         Notify(Npcea.Status,Npcea.Comments);
     }
 }
  1. pas de reflexion
  2. notation courte
  3. pas de chaîne magique dans votre code d'entreprise
  4. Réutilisation de PropertyChangedEventArgs à travers l'application
  5. Possibilité de notifier plusieurs propriétés dans une seule instruction
Bruno
la source
0

Utilisez ceci

using System;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;


public static class ObservableFactory
{
    public static T Create<T>(T target)
    {
        if (!typeof(T).IsInterface)
            throw new ArgumentException("Target should be an interface", "target");

        var proxy = new Observable<T>(target);
        return (T)proxy.GetTransparentProxy();
    }
}

internal class Observable<T> : RealProxy, INotifyPropertyChanged, INotifyPropertyChanging
{
    private readonly T target;

    internal Observable(T target)
        : base(ImplementINotify(typeof(T)))
    {
        this.target = target;
    }

    public override IMessage Invoke(IMessage msg)
    {
        var methodCall = msg as IMethodCallMessage;

        if (methodCall != null)
        {
            return HandleMethodCall(methodCall);
        }

        return null;
    }

    public event PropertyChangingEventHandler PropertyChanging;
    public event PropertyChangedEventHandler PropertyChanged;



    IMessage HandleMethodCall(IMethodCallMessage methodCall)
    {
        var isPropertySetterCall = methodCall.MethodName.StartsWith("set_");
        var propertyName = isPropertySetterCall ? methodCall.MethodName.Substring(4) : null;

        if (isPropertySetterCall)
        {
            OnPropertyChanging(propertyName);
        }

        try
        {
            object methodCalltarget = target;

            if (methodCall.MethodName == "add_PropertyChanged" || methodCall.MethodName == "remove_PropertyChanged"||
                methodCall.MethodName == "add_PropertyChanging" || methodCall.MethodName == "remove_PropertyChanging")
            {
                methodCalltarget = this;
            }

            var result = methodCall.MethodBase.Invoke(methodCalltarget, methodCall.InArgs);

            if (isPropertySetterCall)
            {
                OnPropertyChanged(methodCall.MethodName.Substring(4));
            }

            return new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
        }
        catch (TargetInvocationException invocationException)
        {
            var exception = invocationException.InnerException;
            return new ReturnMessage(exception, methodCall);
        }
    }

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

    protected virtual void OnPropertyChanging(string propertyName)
    {
        var handler = PropertyChanging;
        if (handler != null) handler(this, new PropertyChangingEventArgs(propertyName));
    }

    public static Type ImplementINotify(Type objectType)
    {
        var tempAssemblyName = new AssemblyName(Guid.NewGuid().ToString());

        var dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
            tempAssemblyName, AssemblyBuilderAccess.RunAndCollect);

        var moduleBuilder = dynamicAssembly.DefineDynamicModule(
            tempAssemblyName.Name,
            tempAssemblyName + ".dll");

        var typeBuilder = moduleBuilder.DefineType(
            objectType.FullName, TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);

        typeBuilder.AddInterfaceImplementation(objectType);
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
        typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanging));
        var newType = typeBuilder.CreateType();
        return newType;
    }
}

}

Mec505
la source