INotifyPropertyChanged implementieren - gibt es einen besseren Weg?


647

Microsoft hätte etwas Bissiges implementieren sollen INotifyPropertyChanged, wie in den automatischen Eigenschaften. Geben {get; set; notify;} Sie einfach an, dass es meiner Meinung nach sehr sinnvoll ist, dies zu tun. Oder gibt es irgendwelche Komplikationen, um es zu tun?

Können wir selbst so etwas wie "Benachrichtigen" in unseren Eigenschaften implementieren? Gibt es eine anmutige Lösung für die Implementierung INotifyPropertyChangedin Ihrer Klasse oder die einzige Möglichkeit, dies zu tun, besteht darin, das PropertyChangedEreignis in jeder Eigenschaft auszulösen.

Wenn nicht, können wir etwas schreiben, um den Code automatisch zu generieren, um das PropertyChanged Ereignis auszulösen ?




2
Sie können stattdessen DependencyObject und DependencyProperties verwenden. HA! Ich habe ein lustiges gemacht.
Phil


5
Zu der Zeit war es nicht möglich, Änderungen an C # vorzunehmen, da wir ein riesiges Backlog an Abhängigkeiten hatten. Als MVVM geboren wurde, haben wir uns wohl nicht wirklich viel Mühe gegeben, um dieses Problem zu lösen, und ich weiß, dass das Patterns & Practices-Team ein paar Versuche unternommen hat (daher haben Sie auch MEF als Teil davon Forschungsthread). Heute denke ich, dass [CallerMemberName] die Antwort auf das oben Gesagte ist.
Scott Barnes

Antworten:


633

Ohne etwas wie Postsharp zu verwenden, verwendet die von mir verwendete Minimalversion Folgendes:

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

Jede Eigenschaft ist dann nur so etwas wie:

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

das ist nicht riesig; Wenn Sie möchten, kann es auch als Basisklasse verwendet werden. Die boolRückgabe von SetFieldgibt an, ob es sich um ein No-Op handelt, falls Sie eine andere Logik anwenden möchten.


oder noch einfacher mit C # 5:

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

was so genannt werden kann:

set { SetField(ref name, value); }

mit dem der Compiler das "Name"automatisch hinzufügt .


C # 6.0 erleichtert die Implementierung:

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

... und jetzt mit 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);
}

4
Schöner Trick Marc! Ich schlug eine Verbesserung vor, um einen Lambda-Ausdruck anstelle des Eigenschaftsnamens zu verwenden, siehe meine Antwort
Thomas Levesque

7
@Thomas - das Lambda ist alles schön und gut, aber es fügt viel Aufwand für etwas hinzu, das eigentlich sehr einfach ist. Ein praktischer Trick, aber ich bin mir nicht sicher, ob er immer praktisch ist.
Marc Gravell

14
@Marc - Ja, es kann wahrscheinlich die Leistung beeinträchtigen ... Ich mag jedoch die Tatsache, dass es zur Kompilierungszeit überprüft und durch den Befehl "Umbenennen" korrekt überarbeitet wird
Thomas Levesque

4
@ Gusdor Glücklicherweise gibt es bei C # 5 keine Kompromisse - Sie können das Beste aus beiden herausholen über (wie Pedro77 bemerkt)[CallerMemberName]
Marc Gravell

4
@ Gusdor die Sprache und das Framework sind getrennt; Sie können den C # 5-Compiler als Ziel für .NET 4 verwenden und das fehlende Attribut einfach selbst hinzufügen - es funktioniert einwandfrei. Es muss nur den richtigen Namen haben und sich im richtigen Namespace befinden. Es muss sich nicht in einer bestimmten Baugruppe befinden.
Marc Gravell

196

Ab .Net 4.5 gibt es endlich eine einfache Möglichkeit, dies zu tun.

.Net 4.5 führt neue Anruferinformationsattribute ein.

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

Es ist wahrscheinlich eine gute Idee, der Funktion auch einen Vergleicher hinzuzufügen.

EqualityComparer<T>.Default.Equals

Weitere Beispiele hier und hier

Siehe auch Anruferinformationen (C # und Visual Basic).


12
Brillant! Aber warum ist es generisch?
Abatishchev

@abatishchev Ich denke, das muss nicht sein, ich habe nur mit der Idee gespielt, dass die Funktion auch die Eigenschaft festlegt. Ich werde sehen, ob ich meine Antwort aktualisieren kann, um die vollständige Lösung bereitzustellen. Die zusätzlichen Beispiele machen einen guten Job, der in der Zwischenzeit.
Daniel Little

3
Es wurde von C # 5.0 eingeführt. Es hat nichts mit .net 4.5 zu tun, aber das ist eine großartige Lösung!
J. Lennon

5
@J. Lennon .net 4.5 hat immer noch etwas damit zu tun, schließlich kommt das Attribut von irgendwoher msdn.microsoft.com/en-au/library/…
Daniel Little

@Lavinski ändern Sie Ihre Anwendung auf zB .NET 3.5 und sehen Sie, was funktionieren wird (in vs2012)
J. Lennon

162

Ich mag Marc's Lösung wirklich, aber ich denke, sie kann leicht verbessert werden, um die Verwendung einer "magischen Zeichenfolge" zu vermeiden (die kein Refactoring unterstützt). Anstatt den Eigenschaftsnamen als Zeichenfolge zu verwenden, können Sie ihn einfach zu einem Lambda-Ausdruck machen:

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

Fügen Sie einfach die folgenden Methoden zu Marc's Code hinzu, es wird den Trick machen:

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

Übrigens wurde dies von dieser aktualisierten URL des Blogposts inspiriert


6
Es gibt mindestens ein Framework, das diese Methode verwendet, ReactiveUI .
AlSki

Sehr spät bedeutete dies, die Reflexion zu durchlaufen, was einen Performance-Hit bedeutete. Es könnte akzeptabel sein, aber das Festlegen einer Eigenschaft ist kein Ort, an dem meine Anwendung viele Zyklen dauern soll.
Bruno Brant

1
@BrunoBrant Bist du sicher, dass es einen Performance-Hit gibt? Laut dem Blog-Beitrag erfolgt die Reflexion eher zur Kompilierungszeit als zur Laufzeit (dh zur statischen Reflexion).
Nathaniel Elkins

6
Ich glaube, dein gesamtes OnPropertyChanged <T> ist mit dem Namen des Operators von C # 6 veraltet, was dieses Monster ein bisschen schlanker macht.
Traubenfuchs

5
@Traubenfuchs, eigentlich macht das CallerMemberName-Attribut von C # 5 es noch einfacher, da Sie überhaupt nichts übergeben müssen ...
Thomas Levesque

120

Es gibt auch Fody mit einem PropertyChanged- Add-In, mit dem Sie Folgendes schreiben können:

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

... und beim Kompilieren werden Benachrichtigungen über geänderte Eigenschaften eingefügt.


7
Ich denke, das ist genau das, wonach OP gesucht hat, als sie gefragt haben: "Können wir selbst so etwas wie 'Benachrichtigen' in unseren Eigenschaften implementieren? Gibt es eine elegante Lösung für die Implementierung von INotifyPropertyChanged in Ihrer Klasse
?

3
Dies ist die einzig anmutige Lösung, und sie funktioniert einwandfrei, wie @CADbloke sagte. Und ich war auch skeptisch gegenüber dem Weber, aber ich habe den IL-Code dahinter überprüft / erneut überprüft und er ist perfekt, einfach, macht alles, was Sie brauchen, und sonst nichts. Es bindet und ruft auch den Methodennamen auf, den Sie in der Basisklasse dafür angegeben haben, unabhängig davon, ob NotifyOnProp ..., OnNotify ... keine Rolle spielt. Daher funktioniert es gut mit jeder Basisklasse, die Sie möglicherweise haben und die INotify implementiert. .
NSGaga-meistens-inaktiv

1
Sie können ganz einfach überprüfen, was der Weber tut. Sehen Sie sich das Build-Ausgabefenster an und es listet alle PropertyChanged-Objekte auf, die er gewebt hat. Wenn Sie die VScolorOutput-Erweiterung mit dem Regex-Muster verwenden "Fody/.*?:",LogCustom2,True, wird sie in der Farbe "Custom 2" hervorgehoben. Ich habe es hellrosa gemacht, damit es leicht zu finden ist. Fody einfach alles, es ist die sauberste Art, etwas zu tun, das viele sich wiederholende Eingaben enthält.
CAD Kerl

@mahmoudnezarsarhan Nein, das ist es nicht. Ich erinnere mich, dass sich die Art und Weise, wie es konfiguriert werden muss, geringfügig geändert hat, aber Fody PropertyChanged ist noch aktiv und aktiv.
Larry

65

Ich denke, die Leute sollten der Leistung etwas mehr Aufmerksamkeit schenken. Dies wirkt sich tatsächlich auf die Benutzeroberfläche aus, wenn viele Objekte gebunden werden müssen (denken Sie an ein Raster mit mehr als 10.000 Zeilen) oder wenn sich der Wert des Objekts häufig ändert (Echtzeitüberwachungs-App).

Ich nahm verschiedene Implementierungen, die hier und anderswo gefunden wurden, und führte einen Vergleich durch. Testen Sie den Leistungsvergleich von INotifyPropertyChanged-Implementierungen .


Hier ist ein Blick auf das Ergebnis Implemenation vs Runtime


14
-1: Es gibt keinen Leistungsaufwand: CallerMemberName werden zur Kompilierungszeit in Literalwerte geändert. Versuchen Sie einfach, Ihre App zu dekompilieren.
JYL

Hier ist die entsprechende Frage und Antwort: stackoverflow.com/questions/22580623/…
uli78

1
@JYL, Sie haben Recht, dass CallerMemberName keinen großen Overhead hinzugefügt hat. Ich muss beim letzten Versuch etwas falsches implementiert haben. Ich werde den Blog aktualisieren und antworten, um später den Benchmark für die Implementierung von CallerMemberName und Fody widerzuspiegeln.
Peijen

1
Wenn Sie ein Raster von 10.000+ in der Benutzeroberfläche haben, sollten Sie wahrscheinlich Ansätze kombinieren, um die Leistung zu bewältigen, wie z. B. Paging, bei dem Sie nur 10, 50, 100, 250 Treffer pro Seite
anzeigen

Austin Rhymer, wenn Sie große Datenmengen + 50 haben, verwenden Sie die Datenvirtualisierung, ohne alle Daten laden zu müssen. Es werden nur die Daten geladen, die im aktuellen angezeigten Bereich angezeigt werden!
Bilal

38

Ich stelle eine verbindliche Klasse in meinem Blog unter http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ vor. Bindable verwendet ein Wörterbuch als Eigenschaftstasche. Es ist einfach genug, die erforderlichen Überladungen für eine Unterklasse hinzuzufügen, um ihr eigenes Sicherungsfeld mithilfe von ref-Parametern zu verwalten.

  • Keine magische Schnur
  • Keine Reflexion
  • Kann verbessert werden, um die Standard-Wörterbuchsuche zu unterdrücken

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

Es kann folgendermaßen verwendet werden:

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}

2
Dies ist eine gute Lösung, aber der einzige Nachteil ist, dass es einen kleinen Leistungseinbruch beim Boxen / Unboxen gibt.
MCattle

1
Ich würde vorschlagen, Set zu verwenden protected T Get<T>(T defaultValue, [CallerMemberName] string name = null)und auch einzuchecken if (_properties.ContainsKey(name) && Equals(value, Get<T>(default(T), name)))(um zu erhöhen und zu speichern, wenn es zum ersten Mal auf den Standardwert gesetzt wird)
Miquel

1
@Miquel Das Hinzufügen von Unterstützung für benutzerdefinierte Standardwerte kann sicher nützlich sein. Sie sollten jedoch darauf achten, das geänderte Ereignis nur dann auszulösen, wenn sich der Wert tatsächlich geändert hat. Wenn Sie eine Eigenschaft auf denselben Wert setzen, sollten keine Ereignisse ausgelöst werden. Ich muss zugeben, dass es in den meisten Fällen harmlos ist, aber ich habe einige Male mit Eigenschaften gearbeitet, die tausende Male auf den gleichen Wert gesetzt wurden, wobei Ereignisse die Reaktionsfähigkeit der Benutzeroberfläche zerstörten.
TiMoch

1
@stakx Ich habe einige Anwendungen, die darauf aufbauen, um das Erinnerungsmuster für Rückgängig / Wiederherstellen zu unterstützen oder das Muster der Arbeitseinheit in Anwendungen zu aktivieren, in denen nhibernate nicht verwendet werden kann
TiMoch

1
Diese spezielle Lösung gefällt mir sehr gut: kurze Notation, kein dynamisches Proxy-Zeug, keine IL-Einmischung usw. Sie können sie jedoch verkürzen, indem Sie nicht jedes Mal T für Get angeben müssen, indem Sie Get return dynamisch machen. Ich weiß, dies wirkt sich auf die Laufzeitleistung aus, aber jetzt kann der Code für Getter und Setter endlich immer der gleiche sein und in einer Zeile den Herrn preisen! PS: Sie sollten in Ihrer Get-Methode (einmal beim Schreiben der Basisklasse) besonders vorsichtig sein, wenn Sie Standardwerte für Werttypen als dynamisch zurückgeben.
Stellen

15

Ich hatte noch keine Gelegenheit, dies selbst zu versuchen, aber wenn ich das nächste Mal ein Projekt mit einer großen Anforderung für INotifyPropertyChanged einrichte, beabsichtige ich, ein Postsharp- Attribut zu schreiben, das den Code beim Kompilieren einfügt . Etwas wie:

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

Wird werden:

private string _firstName;

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

Ich bin mir nicht sicher, ob dies in der Praxis funktionieren wird und ich muss mich hinsetzen und es ausprobieren, aber ich verstehe nicht, warum nicht. Möglicherweise muss es einige Parameter für Situationen akzeptieren, in denen mehr als ein OnPropertyChanged ausgelöst werden muss (wenn ich beispielsweise eine FullName-Eigenschaft in der obigen Klasse hatte).

Momentan verwende ich eine benutzerdefinierte Vorlage in Resharper, aber trotzdem habe ich es satt, dass alle meine Eigenschaften so lang sind.


Ah, eine schnelle Google-Suche (die ich hätte machen sollen, bevor ich das geschrieben habe) zeigt, dass mindestens eine Person hier schon einmal so etwas gemacht hat . Nicht genau das, was ich mir vorgestellt hatte, aber nah genug, um zu zeigen, dass die Theorie gut ist.


6
Ein kostenloses Tool namens Fody scheint dasselbe zu tun und fungiert als generischer Code-Injektor zur Kompilierungszeit. Es kann in Nuget heruntergeladen werden, ebenso wie die Plugin-Pakete PropertyChanged und PropertyChanging.
Triynko

11

Ja, es gibt sicherlich einen besseren Weg. Hier ist es:

Schritt für Schritt Tutorial von mir geschrumpft, basierend auf diesem nützlichen Artikel .

  • Neues Projekt erstellen
  • Installieren Sie das Castle Core-Paket im Projekt

Installationspaket Castle.Core

  • Installieren Sie nur mvvm light-Bibliotheken

Install-Package MvvmLightLibs

  • Fügen Sie dem Projekt zwei Klassen hinzu:

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;
    }
}
  • Erstellen Sie Ihr Ansichtsmodell, zum Beispiel:

-

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

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

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • Binden Sie Bindungen in xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
  • Fügen Sie die Codezeile wie folgt in die CodeBehind-Datei MainWindow.xaml.cs ein:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Genießen.

Geben Sie hier die Bildbeschreibung ein

Beachtung!!! Alle begrenzten Eigenschaften sollten mit dem virtuellen Schlüsselwort dekoriert werden, da sie vom Castle-Proxy zum Überschreiben verwendet werden.


Ich bin interessiert zu wissen, welche Version von Castle Sie verwenden. Ich bin mit 3.3.0 und die CreateClassProxy Methode nicht über diese Parameter: type, interfaces to apply, interceptors.
IAbstract

Egal, ich habe die generische CreateClassProxy<T>Methode verwendet. Ganz anders ... hmmm, ich frage mich, warum die generische Methode so begrenzt ist. :(
IAbstract


5

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

Es ist in deutscher Sprache geschrieben, aber Sie können die ViewModelBase.cs herunterladen. Alle Kommentare in der cs-Datei sind in englischer Sprache verfasst.

Mit dieser ViewModelBase-Klasse können bindbare Eigenschaften implementiert werden, die den bekannten Abhängigkeitseigenschaften ähneln:

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

1
Link ist unterbrochen.
Guge

4

Basierend auf der Antwort von Thomas, die aus einer Antwort von Marc übernommen wurde, habe ich den Code der reflektierten Eigenschaft in eine Basisklasse umgewandelt:

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

Die Verwendung entspricht der Antwort von Thomas, außer dass Sie zusätzliche Eigenschaften übergeben können, für die Sie benachrichtigen möchten. Dies war erforderlich, um berechnete Spalten zu verarbeiten, die in einem Raster aktualisiert werden müssen.

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

Ich habe dies eine Sammlung von Elementen steuern, die in einer BindingList gespeichert sind, die über eine DataGridView verfügbar gemacht wird. Ich muss keine manuellen Refresh () -Aufrufe mehr an das Grid senden.


4

Lassen Sie mich meinen eigenen Ansatz namens Yappi vorstellen . Es gehört zu den vom Runtime-Proxy abgeleiteten Klassengeneratoren und fügt einem vorhandenen Objekt oder Typ wie dem dynamischen Proxy von Caste Project neue Funktionen hinzu.

Es ermöglicht die einmalige Implementierung von INotifyPropertyChanged in der Basisklasse und die Deklaration abgeleiteter Klassen im folgenden Stil, wobei INotifyPropertyChanged weiterhin für neue Eigenschaften unterstützt wird:

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

Die Komplexität der abgeleiteten Klassen- oder Proxy-Konstruktion kann hinter der folgenden Zeile verborgen werden:

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

Alle Implementierungsarbeiten von INotifyPropertyChanged können folgendermaßen ausgeführt werden:

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

Es ist völlig sicher für das Refactoring, verwendet keine Reflexion nach der Typkonstruktion und ist schnell genug.


Warum benötigen Sie TDeclarationTypparameter PropertyImplementation? Sicherlich können Sie einen geeigneten Typ finden, um den Getter / Setter nur mit aufzurufen (nicht callvirt) TImplementation?
Andrew Savinykh

Die Implementierung funktioniert in den meisten Fällen. Ausnahmen sind: 1. Eigenschaften, die mit "neuem" C # -Schlüsselwort neu definiert wurden. 2. Eigenschaften der expliziten Schnittstellenimplementierung.
Kelqualyn

3

Alle diese Antworten sind sehr nett.

Meine Lösung besteht darin, die Codefragmente zu verwenden, um die Arbeit zu erledigen.

Dies verwendet den einfachsten Aufruf des PropertyChanged-Ereignisses.

Speichern Sie dieses Snippet und verwenden Sie es, während Sie das 'fullprop'-Snippet verwenden.

Der Speicherort befindet sich im Menü "Tools \ Code Snippet Manager ..." in 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>

Sie können den Anruf nach Belieben ändern (um die oben genannten Lösungen zu verwenden).


2

Wenn Sie in .NET 4.5 Dynamik verwenden, müssen Sie sich keine Sorgen machen INotifyPropertyChanged.

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

Wenn Name an ein Steuerelement gebunden ist, funktioniert es einwandfrei.


1
irgendwelche Nachteile davon?
JuFo

2

Eine andere kombinierte Lösung verwendet 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));
        }
    }
}

Verwendungszweck:

public class TempVM : BaseViewModel
{
    private int _intP;
    public int IntP
    {
        get { return _intP; }
        set { Set<int>(ref _intP, value); }
    }
}

2
Ist das schnell Ist der Zugriff auf den Stapelrahmen nicht an eine Berechtigungsanforderung gebunden? Ist das im Kontext der Verwendung von async / await robust?
Stéphane Gourichon

@ StéphaneGourichon Nein, ist es nicht. Der Zugriff auf den Stapelrahmen bedeutet in den meisten Fällen einen erheblichen Leistungseinbruch.
Bruno Brant


Beachten Sie, dass durch Inlining die get_FooMethode im Freigabemodus möglicherweise ausgeblendet wird .
Bytecode77

2

Ich habe in meiner Basisbibliothek eine Erweiterungsmethode zur Wiederverwendung erstellt:

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

Dies funktioniert mit .Net 4.5 aufgrund von CallerMemberNameAttribute . Wenn Sie es mit einer früheren .NET-Version verwenden möchten, müssen Sie die Methodendeklaration von: ...,[CallerMemberName] string propertyName = "", ...in ändern...,string propertyName, ...

Verwendungszweck:

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

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

2

Ich habe auf diese Weise gelöst (es ist ein bisschen mühsam, aber es ist sicherlich das schnellere in der Laufzeit).

In VB (sorry, aber ich denke, es ist nicht schwer, es in C # zu übersetzen) mache ich diese Ersetzung durch RE:

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

mit:

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

Diese Transofrm alle Code wie folgt:

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

Im

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

Und wenn ich einen besser lesbaren Code haben möchte, kann ich das Gegenteil sein, indem ich nur die folgende Ersetzung vornehme:

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

Mit

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

Ich werfe, um den IL-Code der set-Methode zu ersetzen, aber ich kann nicht viel kompilierten Code in IL schreiben ... Wenn ich ihn eines Tages schreibe, sage ich es Ihnen!


2

Ich behalte das als Ausschnitt. C # 6 fügt eine nette Syntax zum Aufrufen des Handlers hinzu.

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

2

Hier ist eine Unity3D- oder Nicht-CallerMemberName-Version von 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
}

Mit diesem Code können Sie Eigenschaftsunterstützungsfelder wie folgt schreiben:

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

Wenn Sie in Resharper ein Muster- / Such-Snippet erstellen, können Sie Ihren Workflow auch automatisieren, indem Sie einfache Requisitenfelder in den obigen Hintergrund konvertieren.

Suchmuster:

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

Muster ersetzen:

public $type$ $fname$
{
    get { return Get<$type$>(); }
    set { Set(value); }
}

2

Ich habe einen Artikel geschrieben, der dabei hilft ( https://msdn.microsoft.com/magazine/mt736453) ). Sie können das SolSoft.DataBinding NuGet-Paket verwenden. Dann können Sie Code wie folgt schreiben:

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

Leistungen:

  1. Basisklasse ist optional
  2. keine Reflexion über jeden 'eingestellten Wert'
  3. kann Eigenschaften haben, die von anderen Eigenschaften abhängen, und alle lösen automatisch die entsprechenden Ereignisse aus (Artikel enthält ein Beispiel dafür)

2

Obwohl es offensichtlich viele Möglichkeiten gibt, dies zu tun, mit Ausnahme der magischen AOP-Antworten, scheint keine der Antworten das Festlegen der Eigenschaft eines Modells direkt aus dem Ansichtsmodell zu betrachten, ohne dass ein lokales Feld zum Verweisen vorhanden ist.

Das Problem ist, dass Sie nicht auf eine Eigenschaft verweisen können. Sie können diese Eigenschaft jedoch mit einer Aktion festlegen.

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

Dies kann wie der folgende Code-Extrakt verwendet werden.

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

In diesem BitBucket-Repo finden Sie eine vollständige Implementierung der Methode und einige verschiedene Möglichkeiten, um dasselbe Ergebnis zu erzielen, einschließlich einer Methode, die LINQ verwendet, und einer Methode, die Reflektion verwendet. Beachten Sie, dass diese Methoden in Bezug auf die Leistung langsamer sind.


1

Andere Dinge, die Sie bei der Implementierung dieser Art von Eigenschaften berücksichtigen sollten, sind die Tatsache, dass INotifyPropertyChang * ed * beide Ereignisargumentklassen verwenden.

Wenn Sie eine große Anzahl von Eigenschaften haben, die festgelegt werden, kann die Anzahl der Instanzen von Ereignisargumentklassen sehr groß sein. Sie sollten in Betracht ziehen, sie zwischenzuspeichern, da sie einer der Bereiche sind, in denen eine Zeichenfolgenexplosion auftreten kann.

Schauen Sie sich diese Implementierung an und erklären Sie, warum sie konzipiert wurde.

Josh Smiths Blog


1

Ich habe gerade ActiveSharp - Automatic INotifyPropertyChanged gefunden . Ich habe es noch nicht verwendet, aber es sieht gut aus.

Um von seiner Website zu zitieren ...


Senden Sie Benachrichtigungen über Eigenschaftsänderungen, ohne den Eigenschaftsnamen als Zeichenfolge anzugeben.

Schreiben Sie stattdessen folgende Eigenschaften:

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

Beachten Sie, dass der Name der Eigenschaft nicht als Zeichenfolge angegeben werden muss. ActiveSharp findet das zuverlässig und korrekt heraus. Es funktioniert basierend auf der Tatsache, dass Ihre Eigenschaftsimplementierung das Hintergrundfeld (_foo) durch ref übergibt. (ActiveSharp verwendet diesen Aufruf "by ref", um zu identifizieren, welches Sicherungsfeld übergeben wurde, und anhand des Felds die Eigenschaft zu identifizieren.)


1

Eine Idee mit Reflexion:

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

Das ist ziemlich cool, ich mag es mehr als Ausdrucksansatz. Auf der anderen Seite sollte langsamer sein.
Nawfal

1

Mir ist klar, dass diese Frage bereits unzählige Antworten hat, aber keine davon fühlte sich für mich richtig an. Mein Problem ist, dass ich keine Performance-Hits will und bereit bin, allein aus diesem Grund ein wenig Ausführlichkeit zu ertragen. Ich interessiere mich auch nicht allzu sehr für Auto-Eigenschaften, was mich zu der folgenden Lösung führte:

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()
    {
    }
}

Mit anderen Worten, die obige Lösung ist praktisch, wenn Sie nichts dagegen haben:

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()
    {
    }
}

Vorteile

  • Keine Reflexion
  • Benachrichtigt nur, wenn alter Wert! = Neuer Wert
  • Benachrichtigen Sie mehrere Eigenschaften gleichzeitig

Nachteile

  • Keine automatischen Eigenschaften (Sie können jedoch Unterstützung für beide hinzufügen!)
  • Etwas Ausführlichkeit
  • Boxen (kleiner Leistungstreffer?)

Leider ist es immer noch besser als dies zu tun,

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

Für jede einzelne Eigenschaft, die mit der zusätzlichen Ausführlichkeit zum Albtraum wird ;-(

Hinweis: Ich behaupte nicht, dass diese Lösung im Vergleich zu den anderen leistungsmäßig besser ist, nur dass sie eine praktikable Lösung für diejenigen ist, die die anderen vorgestellten Lösungen nicht mögen.


1

Ich habe mir diese Basisklasse ausgedacht, um das beobachtbare Muster zu implementieren. Sie macht so ziemlich das, was Sie brauchen ( "automatisch" das Set implementieren und abrufen). Ich habe eine Stunde als Prototyp damit verbracht, daher gibt es nicht viele Unit-Tests, aber es beweist das Konzept. Beachten Sie, dass das verwendet wird Dictionary<string, ObservablePropertyContext>, um die Notwendigkeit für private Felder zu beseitigen.

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

Hier ist die Verwendung

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

1

Ich schlage vor, ReactiveProperty zu verwenden. Dies ist die kürzeste Methode außer Fody.

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

stattdessen

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

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

( DOCS )


0

Eine andere Idee...

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

0

=> hier meine Lösung mit folgenden Funktionen

 public ResourceStatus Status
 {
     get { return _status; }
     set
     {
         _status = value;
         Notify(Npcea.Status,Npcea.Comments);
     }
 }
  1. keine Reflexion
  2. kurze Notation
  3. Keine magische Zeichenfolge in Ihrem Geschäftscode
  4. Wiederverwendbarkeit von PropertyChangedEventArgs für alle Anwendungen
  5. Möglichkeit, mehrere Eigenschaften in einer Anweisung zu benachrichtigen

0

Benutze das

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

}}

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.