Question Implémentation de INotifyPropertyChanged - existe-t-il un meilleur moyen?


Microsoft aurait dû mettre en œuvre quelque chose de rapide pour INotifyPropertyChanged, comme dans les propriétés automatiques, il suffit de spécifier {get; set; notify;} 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 élégante pour la mise en œuvre INotifyPropertyChanged dans votre classe ou la seule façon de le faire est en élevant le PropertyChanged événement dans chaque propriété.

Sinon, pouvons-nous écrire quelque chose pour générer automatiquement le morceau de code à élever PropertyChanged  un événement?


555


origine


Réponses:


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 bool revenir de SetField vous indique s'il s'agit d'un no-op, au cas où vous souhaitez appliquer une autre logique.


ou encore plus facile 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 va ajouter le "Name" automatiquement.


C # 6.0 facilite la mise en œuvre:

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

... et maintenant avec C # 7:

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

513



À partir de .Net 4.5, il y a enfin un moyen facile de le faire.

.Net 4.5 introduit un nouvel attribut d'information d'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 un comparateur à la fonction.

EqualityComparer<T>.Default.Equals

Plus d'exemples ici et ici

Regarde aussi Informations sur l'appelant (C # et Visual Basic)


188



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 une 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 ce blog  URL mise à jour


160



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

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

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


102



Je pense que les gens devraient prêter un peu plus d'attention aux performances, cela affecte vraiment l'interface utilisateur quand il y a beaucoup d'objets à lier (pensez à une grille avec plus de 10 000 lignes) ou si la valeur de l'objet change fréquemment. .

J'ai pris diverses implémentations trouvées ici et ailleurs et fait une comparaison, jetez un coup d'œil Comparaison des performances des implémentations INotifyPropertyChanged.


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


60



Je présente 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 en utilisant les 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); }
    }
}

31



Je n'ai pas encore eu l'occasion de l'essayer moi-même, mais la prochaine fois que je crée un projet avec un gros besoin pour INotifyPropertyChanged, j'ai l'intention d'écrire un Postsharp attribut qui va injecter le code à 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 pas. Je pourrais avoir besoin de 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 gabarit personnalisé dans Resharper, mais même avec ça, j'en ai assez de toutes mes propriétés.


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.


15



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

Étape par étape tutoriel rétréci par moi, sur cette base article utile.

  • Créer un nouveau projet
  • Installez le paquet de noyau de château dans le projet

Install-Package Castle.Core

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

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 les liaisons dans xaml:

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

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Prendre plaisir.

enter image description here

Attention!!! Toutes les propriétés délimitées doivent être décorées avec mot-clé virtuel, car ils ont utilisé par le proxy du château pour le remplacement.


9



Une approche très similaire à AOP consiste à injecter le contenu 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


6