Wdrażanie INotifyPropertyChanged - czy istnieje lepszy sposób?

647

Microsoft powinien był zaimplementować coś nieoczekiwanego INotifyPropertyChanged, na przykład we właściwościach automatycznych, po prostu sprecyzuj {get; set; notify;} , że to ma sens, aby to zrobić. A może są jakieś komplikacje?

Czy sami możemy wdrożyć coś takiego jak „powiadomić” w naszych nieruchomościach? Czy istnieje wdzięczne rozwiązanie do wdrożenia INotifyPropertyChangedw twojej klasie, czy jedynym sposobem na to jest podniesienie PropertyChangedzdarzenia w każdej właściwości.

Jeśli nie, czy możemy napisać coś do automatycznego wygenerowania fragmentu kodu w celu wywołania PropertyChanged zdarzenia?

PK
źródło
7
powyższy link jest martwy. github.com/SimonCropp/NotifyPropertyWeaver
prime23
2
Zamiast tego można użyć DependencyObject i DependencyProperties. HA! Zrobiłem śmieszne.
Phil
5
W tamtym czasie wprowadzanie zmian w języku C # nie było możliwe, biorąc pod uwagę ogromny dziennik zależności między nimi. Więc kiedy się urodził MVVM, myślę, że po prostu nie włożyliśmy dużo wysiłku w rozwiązanie tego problemu i wiem, że zespół Patterns & Practices miał na to kilka okazji (stąd też MEF jako część tego wątek badawczy). Dzisiaj myślę, że [CallerMemberName] jest odpowiedzią na powyższe.
Scott Barnes

Odpowiedzi:

633

Bez użycia czegoś takiego jak postsharp, minimalna wersja, której używam, używa czegoś takiego:

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

Każda właściwość jest wtedy po prostu czymś w rodzaju:

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

co nie jest ogromne; jeśli chcesz, można go również wykorzystać jako klasę podstawową. boolPowrót z SetFieldpowie Ci, czy to nie-op, w przypadku, gdy chcesz zastosować inną logikę.


lub jeszcze łatwiej dzięki C # 5:

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

które można nazwać tak:

set { SetField(ref name, value); }

z którym kompilator doda "Name"automatycznie.


C # 6.0 ułatwia implementację:

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

... a teraz z 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
źródło
4
Niezła sztuczka Marc! Zasugerowałem ulepszenie użycia wyrażenia lambda zamiast nazwy nieruchomości, zobacz moją odpowiedź
Thomas Levesque
7
@Thomas - lambda jest dobra i dobra, ale dodaje dużo kosztów ogólnych w przypadku czegoś, co w rzeczywistości jest bardzo proste. Przydatna sztuczka, ale nie jestem pewien, czy zawsze jest praktyczna.
Marc Gravell
14
@Marc - Tak, prawdopodobnie może obniżyć wydajność ... Jednak naprawdę podoba mi się fakt, że jest sprawdzany w czasie kompilacji i jest poprawnie refaktoryzowany poleceniem „Zmień nazwę”
Thomas Levesque
4
@Gusdor na szczęście, z C # 5 nie trzeba iść na kompromis - możesz uzyskać to, co najlepsze z obu (jak zauważa Pedro77)[CallerMemberName]
Marc Gravell
4
@Gusdor język i struktura są oddzielne; możesz użyć kompilatora C # 5, targetować .NET 4 i po prostu sam dodać brakujący atrybut - będzie działał dobrze. Musi mieć tylko poprawną nazwę i być w prawidłowej przestrzeni nazw. Nie musi znajdować się w określonym zestawie.
Marc Gravell
196

Począwszy od .Net 4.5 jest wreszcie łatwy sposób, aby to zrobić.

.Net 4.5 wprowadza nowe atrybuty informacji o dzwoniącym.

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

Prawdopodobnie dobrym pomysłem jest również dodanie funkcji porównującej.

EqualityComparer<T>.Default.Equals

Więcej przykładów tutaj i tutaj

Zobacz także informacje o dzwoniącym (C # i Visual Basic)

Daniel Little
źródło
12
Znakomity! Ale dlaczego to jest ogólne?
abatishchev
@abatishchev Chyba nie musi tak być, po prostu bawiłem się pomysłem ustawienia tej funkcji również. Zobaczę, czy mogę zaktualizować swoją odpowiedź, podając pełne rozwiązanie. Tymczasem dodatkowe przykłady wykonują dobrą robotę.
Daniel Little
3
Został wprowadzony przez C # 5.0. Nie ma to nic wspólnego z .net 4.5, ale jest to świetne rozwiązanie!
J. Lennon
5
@JOT. Lennon .net 4.5 nadal ma z tym coś wspólnego, mimo że cały atrybut pochodzi skądś z msdn.microsoft.com/en-au/library/…
Daniel Little
@Lavinski zmień aplikację na np. .NET 3.5 i zobacz, co zadziała (w vs2012)
J. Lennon
162

Naprawdę podoba mi się rozwiązanie Marca, ale myślę, że można go nieco ulepszyć, aby uniknąć użycia „magicznego ciągu” (który nie obsługuje refaktoryzacji). Zamiast używać nazwy właściwości jako łańcucha, łatwo jest uczynić ją wyrażeniem lambda:

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

Wystarczy dodać następujące metody do kodu Marca, to załatwi sprawę:

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, to zostało zainspirowane tym blogiem zaktualizowanym adresem URL

Thomas Levesque
źródło
6
Jest co najmniej jeden framework używający tej metody, ReactiveUI .
AlSki,
Bardzo późno oznaczało to przejście do refleksji, co oznaczało przebój wydajności. To może być do przyjęcia, ale ustawienie nieruchomości nie jest miejscem, w którym chciałbym, aby moja aplikacja spędzała wiele cykli.
Bruno Brant,
1
@BrunoBrant Czy na pewno jest hit wydajności? Zgodnie z postem na blogu odbicie odbywa się w czasie kompilacji, a nie w czasie wykonywania (tj. Odbicie statyczne).
Nathaniel Elkins
6
Wierzę, że cały OnPropertyChanged <T> jest przestarzały z nazwą operatora C # 6, dzięki czemu ten potwór jest nieco bardziej elegancki.
Traubenfuchs
5
@Traubenfuchs, w rzeczywistości, atrybut CallerMemberName w C # 5 czyni to jeszcze prostszym, ponieważ nie musisz niczego przekazywać ...
Thomas Levesque
120

Istnieje również Fody, który ma dodatek PropertyChanged , który pozwala napisać to:

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

... a podczas kompilacji wstrzykuje powiadomienia o zmianie właściwości.

Tom Gilder
źródło
7
Myślę, że dokładnie tego szukał OP, gdy zapytali: „Czy sami możemy wdrożyć coś takiego jak„ powiadomić ”w naszych właściwościach. Czy istnieje wdzięczne rozwiązanie dla wdrożenia INotifyPropertyChanged w twojej klasie”
Ashoat
3
To naprawdę jedyne wdzięczne rozwiązanie i działa bezbłędnie, jak powiedział @CADbloke. Byłem też sceptycznie nastawiony do tkacza, ale sprawdziłem / ponownie sprawdziłem kod IL z tyłu i jest idealny, jest prosty, robi wszystko, czego potrzebujesz i nic więcej. Przechwytuje również i wywołuje dowolną nazwę metody wyznaczoną dla tej klasy podstawowej, niezależnie od tego, czy NotifyOnProp ..., OnNotify ... nie ma znaczenia, więc działa dobrze z każdą klasą podstawową, którą możesz mieć i która implementuje INotify .. ,
NSGaga-głównie-nieaktywny
1
Możesz łatwo dokładnie sprawdzić, co robi tkacz, rzucić okiem na okno wyników kompilacji, zawiera listę wszystkich rzeczy, które utkało zmienione właściwości. Użycie rozszerzenia VScolorOutput z wzorem wyrażenia regularnego "Fody/.*?:",LogCustom2,Truewyróżnia go kolorem „Niestandardowy 2”. Zrobiłem go jasno różowym, więc łatwo go znaleźć. Po prostu wszystko Fody, to najfajniejszy sposób na robienie czegokolwiek, co ma wiele powtarzalnych pisania.
CAD bloke
@mahmoudnezarsarhan nie, nie jest, pamiętam, że nastąpiła niewielka zmiana w sposobie konfiguracji, ale Fody PropertyChanged jest wciąż żywa i aktywna.
Larry,
65

Myślę, że ludzie powinni zwracać nieco większą uwagę na wydajność; to naprawdę wpływa na interfejs użytkownika, gdy trzeba związać wiele obiektów (pomyśl o siatce z ponad 10 000 wierszy) lub jeśli wartość obiektu zmienia się często (aplikacja do monitorowania w czasie rzeczywistym).

Wziąłem różne implementacje znalezione tutaj i gdzie indziej i zrobiłem porównanie; sprawdź porównanie wydajności implementacji INotifyPropertyChanged .


Oto rzut oka na wynik Implemenation vs Runtime

Peijen
źródło
14
-1: nie ma narzutu wydajności: CallerMemberName są zmieniane na wartości dosłowne w czasie kompilacji. Po prostu spróbuj dekompilować aplikację.
JYL
oto odpowiednie pytanie i odpowiedź: stackoverflow.com/questions/22580623/…
uli78
1
@JYL, masz rację, że CallerMemberName nie dodał dużego obciążenia. Podczas ostatniej próby musiałem coś źle zaimplementować. Zaktualizuję bloga i odpowiem, aby odzwierciedlić test porównawczy dla CallerMemberName i Fody później.
Peijen
1
Jeśli masz siatkę 10000 w interfejsie to powinieneś być łączenie podejścia do wykonania uchwytu, jak stronicowania gdzie tylko pokazać, 10, 50, 100, 250 trafienia na stronę ...
Austin Rhymer
Austin Rhymer, jeśli masz duże dane + 50 użyj wirtualizacji danych, nie musisz ładować wszystkich danych, załaduje tylko dane widoczne w aktualnie wyświetlanym obszarze scolling!
Bilal
38

Wprowadzam klasę Bindable na moim blogu pod adresem http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable używa słownika jako torby właściwości. Łatwo jest dodać niezbędne przeciążenia, aby podklasa zarządzała własnym polem zaplecza za pomocą parametrów ref.

  • Bez magicznego sznurka
  • Brak odbicia
  • Można ulepszyć, aby ukryć domyślne wyszukiwanie słownika

Kod:

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

Można go użyć w następujący sposób:

public class Contact : Bindable {
    public string FirstName {
        get { return Get<string>(); }
        set { Set(value); }
    }
}
TiMoch
źródło
2
To dobre rozwiązanie, ale jedynym minusem jest to, że występuje niewielki hit związany z boksowaniem / rozpakowywaniem.
MCattle
1
Sugerowałbym użyć, protected T Get<T>(T defaultValue, [CallerMemberName] string name = null)a także sprawdzić if (_properties.ContainsKey(name) && Equals(value, Get<T>(default(T), name)))w Ustawie (aby podnieść i zapisać, gdy pierwszy raz ustawię na wartość domyślną)
Miquel
1
@Miquel dodanie obsługi niestandardowych wartości domyślnych może być z pewnością przydatne, jednak należy zachować ostrożność, aby wywołać zmienione zdarzenie tylko wtedy, gdy wartość faktycznie się zmieniła. Ustawienie właściwości na tę samą wartość, która miała, nie powinno wywoływać zdarzeń. Muszę przyznać, że w większości przypadków jest to nieszkodliwe, ale byłem już kilka razy z właściwościami ustawionymi tysiące razy na tę samą wartość, gdy zdarzenia niszczą reakcję interfejsu użytkownika.
TiMoch
1
@stakx Mam kilka aplikacji, które wykorzystują to do obsługi wzorca memento do cofania / ponawiania lub do włączania wzorca jednostki pracy w aplikacjach, w których nie można używać nhibernate
TiMoch
1
Naprawdę podoba mi się to konkretne rozwiązanie: krótka notacja, brak dynamicznego proxy, brak wtrącania się IL, itp. Chociaż możesz to skrócić , usuwając potrzebę określania T za każdym razem dla Get, czyniąc Get return dynamicznym. Wiem, że wpływa to na wydajność środowiska uruchomieniowego, ale teraz kod dla programów pobierających i ustawiających może wreszcie być zawsze taki sam iw jednej linii , chwalcie Pana! PS powinieneś zachować szczególną ostrożność w metodzie Get (jeden raz, gdy piszesz klasę podstawową), zwracając wartości domyślne dla typów wartości jako dynamiczne. Pamiętaj, aby zawsze zwracać prawidłowe wartości domyślne (można to zrobić)
evilkos
15

Właściwie to nie miałem okazji tego spróbować, ale następnym razem konfiguruję projekt z dużym wymaganiem INotifyPropertyChanged Mam zamiar napisać atrybut Postsharp , który wstrzyknie kod w czasie kompilacji. Coś jak:

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

Stanie się:

private string _firstName;

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

Nie jestem pewien, czy to zadziała w praktyce i muszę usiąść i wypróbować, ale nie rozumiem, dlaczego nie. Może być konieczne, aby zaakceptował niektóre parametry w sytuacjach, w których trzeba uruchomić więcej niż jeden OnPropertyChanged (jeśli na przykład miałem właściwość FullName w powyższej klasie)

Obecnie używam niestandardowego szablonu w Resharper, ale mimo to mam dość wszystkich moich właściwości, które są tak długie.


Ach, szybkie wyszukiwanie w Google (które powinienem był zrobić, zanim to napisałem) pokazuje, że przynajmniej jedna osoba zrobiła coś takiego wcześniej tutaj . Nie do końca to, co miałem na myśli, ale wystarczająco blisko, aby pokazać, że teoria jest dobra.

Martin Harris
źródło
6
Wydaje się, że darmowe narzędzie o nazwie Fody działa tak samo, działając jak ogólny aplikator kodu czasu kompilacji. Można go pobrać w Nuget, podobnie jak pakiety wtyczek PropertyChanged i PropertyChanging.
Triynko
11

Tak, z pewnością istnieje lepszy sposób. Oto on:

Samouczek krok po kroku zmniejszył się w oparciu o ten przydatny artykuł .

  • Utwórz nowy projekt
  • Zainstaluj pakiet zamkowy w projekcie

Zainstaluj pakiet Castle.Core

  • Instaluj tylko lekkie biblioteki mvvm

Zainstaluj pakiet MvvmLightLibs

  • Dodaj dwie klasy w projekcie:

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;
    }
}
  • Utwórz model widoku, na przykład:

-

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

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

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • Umieść wiązania w xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
  • Umieść wiersz kodu w pliku za kodem MainWindow.xaml.cs w następujący sposób:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Cieszyć się.

wprowadź opis zdjęcia tutaj

Uwaga!!! Wszystkie ograniczone właściwości powinny być ozdobione słowem kluczowym virtual, ponieważ były używane przez proxy proxy do przesłonięcia.

testCoder
źródło
Chcę wiedzieć, której wersji Castle używasz. Używam 3.3.0 i metoda CreateClassProxy nie ma tych parametrów: type, interfaces to apply, interceptors.
IAbstract 13.04.16
Nieważne, użyłem ogólnej CreateClassProxy<T>metody. Zupełnie inaczej ... hmmm, zastanawiam się, dlaczego tak ograniczone ogólną metodą. :(
IAbstract 13.04.16
5

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

Jest napisany w języku niemieckim, ale możesz pobrać ViewModelBase.cs. Wszystkie komentarze w pliku cs są napisane w języku angielskim.

Za pomocą tej klasy ViewModelBase można zaimplementować właściwości wiążące podobne do dobrze znanych właściwości zależności:

public string SomeProperty
{
    get { return GetValue( () => SomeProperty ); }
    set { SetValue( () => SomeProperty, value ); }
}
DotNetMastermind
źródło
1
Link jest zepsuty.
Guge
4

Na podstawie odpowiedzi Thomasa, która została zaadaptowana z odpowiedzi Marca, zmieniłem kod właściwości odbijającej w klasę podstawową:

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

Sposób użycia jest taki sam jak odpowiedź Thomasa, z tą różnicą, że możesz przekazać dodatkowe właściwości, o których chcesz powiadomić. Było to konieczne do obsługi kolumn obliczeniowych, które należy odświeżyć w siatce.

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

Mam ten element sterujący kolekcją elementów przechowywanych w BindingList odsłoniętym przez DataGridView. Eliminuje to konieczność ręcznego wywoływania funkcji Refresh () do siatki.

StuffOfInterest
źródło
4

Pozwól, że przedstawię własne podejście zwane Yappi . Należy do generatorów klas pochodnych Runtime proxy, dodając nowe funkcje do istniejącego obiektu lub typu, takich jak dynamiczny serwer proxy Caste Project.

Pozwala zaimplementować INotifyPropertyChanged raz w klasie bazowej, a następnie zadeklarować klasy pochodne w następującym stylu, nadal obsługując INotifyPropertyChanged dla nowych właściwości:

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

Złożoność pochodnej konstrukcji klasy lub proxy można ukryć za następującą linią:

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

Wszystkie prace związane z implementacją INotifyPropertyChanged można wykonać w następujący sposób:

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

Jest w pełni bezpieczny do refaktoryzacji, nie wymaga odbicia po konstrukcji typu i jest wystarczająco szybki.

Kelqualyn
źródło
Dlaczego potrzebujesz TDeclarationparametru type na PropertyImplementation? Z pewnością możesz znaleźć odpowiedni typ, z którego można wywoływać (a nie callvirt) tylko getter / setter TImplementation?
Andrew Savinykh
T Wdrożenie działa w większości przypadków. Wyjątkami są: 1. Właściwości zdefiniowane na nowo za pomocą „nowego” słowa kluczowego C #. 2. Właściwości implementacji jawnego interfejsu.
Kelqualyn
3

Wszystkie te odpowiedzi są bardzo miłe.

Moje rozwiązanie wykorzystuje fragmenty kodu do wykonania zadania.

Wykorzystuje to najprostsze wywołanie zdarzenia PropertyChanged.

Zapisz ten fragment i użyj go, gdy używasz fragmentu „fullprop”.

lokalizację można znaleźć w menu „Narzędzia \ Menedżer kodu Snippet ...” w 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>

Możesz zmodyfikować połączenie według własnego uznania (aby skorzystać z powyższych rozwiązań)

Ofir
źródło
2

Jeśli używasz dynamiki w .NET 4.5, nie musisz się martwić INotifyPropertyChanged.

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

jeśli Nazwa jest powiązana z jakąś formantem, działa po prostu dobrze.

Dilshod
źródło
1
jakieś wady korzystania z tego?
juFo
2

Innym połączonym rozwiązaniem jest użycie 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));
        }
    }
}

Stosowanie:

public class TempVM : BaseViewModel
{
    private int _intP;
    public int IntP
    {
        get { return _intP; }
        set { Set<int>(ref _intP, value); }
    }
}
Ofir
źródło
2
Czy to szybko? Czy dostęp do ramki stosu nie jest związany z niektórymi wymaganiami dotyczącymi uprawnień? Czy jest to niezawodne w kontekście używania asynchronizacji / oczekiwania?
Stéphane Gourichon
@ StéphaneGourichon Nie, nie jest. Dostęp do ramy stosu oznacza znaczny spadek wydajności w większości przypadków.
Bruno Brant
Tak, można to zobaczyć na codereview.stackexchange.com/questions/13823/…
Ofir
Pamiętaj, że wstawianie może ukryć get_Foometodę w trybie zwolnienia.
bytecode77
2

W mojej podstawowej bibliotece utworzyłem metodę rozszerzenia do ponownego użycia:

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

Działa to z .Net 4.5 z powodu CallerMemberNameAttribute . Jeśli chcesz go używać z wcześniejszą wersją .Net, musisz zmienić deklarację metody z: ...,[CallerMemberName] string propertyName = "", ...na...,string propertyName, ...

Stosowanie:

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

    public string Name
    {
        get { return _name; }
        set
        {
            this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
        }
    }
}
giammin
źródło
2

Rozwiązałem w ten sposób (jest to trochę pracochłonne, ale na pewno jest szybsze w środowisku wykonawczym).

W VB (przepraszam, ale myślę, że nie jest trudno przetłumaczyć to w C #), robię to podstawienie RE:

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

z:

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

To transofrm cały kod w ten sposób:

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

W

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

A jeśli chcę mieć bardziej czytelny kod, mogę być odwrotny, dokonując tylko następujących zmian:

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

Z

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

Rzucam, by zastąpić kod IL ustawionej metody, ale nie mogę napisać dużo skompilowanego kodu w IL ... Jeśli pewnego dnia go napiszę, powiem ci!

Lucio Menci
źródło
2

Trzymam to jako fragment. C # 6 dodaje ładną składnię do wywoływania programu obsługi.

// 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
źródło
2

Oto Unity3D lub wersja NotifyPropertyChanged dla wersji innej niż CallerMemberName

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
}

Ten kod umożliwia pisanie takich pól:

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

Ponadto w resharper, jeśli utworzysz fragment wzorca / wyszukiwania, możesz także zautomatyzować przepływ pracy, przekształcając proste pola prop w powyższy podkład.

Wzór wyszukiwania:

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

Zamień wzór:

public $type$ $fname$
{
    get { return Get<$type$>(); }
    set { Set(value); }
}
Scott Barnes
źródło
2

Napisałem artykuł, który pomaga w tym ( https://msdn.microsoft.com/magazine/mt736453 ). Możesz użyć pakietu SolSoft.DataBinding NuGet. Następnie możesz napisać taki kod:

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

Korzyści:

  1. klasa podstawowa jest opcjonalna
  2. brak refleksji na temat każdej „wartości zadanej”
  3. mogą mieć właściwości zależne od innych właściwości i wszystkie automatycznie wywołują odpowiednie zdarzenia (artykuł zawiera przykład tego)
Mark Sowul
źródło
2

Chociaż istnieje oczywiście wiele sposobów, aby to zrobić, z wyjątkiem magicznych odpowiedzi AOP, żadna z tych odpowiedzi nie wydaje się patrzeć na ustawienie właściwości Modelu bezpośrednio z modelu widoku bez posiadania lokalnego pola odniesienia.

Problem polega na tym, że nie można odwoływać się do nieruchomości. Można jednak użyć akcji, aby ustawić tę właściwość.

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

Można to wykorzystać jak następujący fragment kodu.

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

Sprawdź to repozytorium BitBucket, aby uzyskać pełną implementację metody i kilka różnych sposobów osiągnięcia tego samego wyniku, w tym metodę wykorzystującą LINQ i metodę wykorzystującą odbicie. Należy pamiętać, że te metody są wolniejsze pod względem wydajności.

Dan
źródło
1

Inną rzeczą, którą warto rozważyć przy wdrażaniu tego rodzaju właściwości, jest fakt, że oba INotifyPropertyChang * ed * używają klas argumentów zdarzeń.

Jeśli ustawiana jest duża liczba właściwości, liczba instancji klasy argumentu zdarzenia może być ogromna, należy rozważyć buforowanie ich, ponieważ są one jednym z obszarów, w których może wystąpić eksplozja łańcucha.

Spójrz na to wdrożenie i wyjaśnienie, dlaczego zostało pomyślane.

Blog Josh Smiths

Piotr
źródło
1

Właśnie znalazłem ActiveSharp - Automatic INotifyPropertyChanged , jeszcze go nie użyłem, ale wygląda dobrze.

Cytowanie z jego strony internetowej ...


Wysyłaj powiadomienia o zmianie właściwości bez podawania nazwy właściwości jako ciągu.

Zamiast tego napisz właściwości takie jak to:

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

Zauważ, że nie trzeba dołączać nazwy właściwości jako łańcucha. ActiveSharp niezawodnie i poprawnie rozpoznaje to samo. Działa w oparciu o fakt, że implementacja właściwości przekazuje pole zaplecza (_foo) przez ref. (ActiveSharp używa tego wywołania „przez ref”, aby zidentyfikować, które pole zaplecza zostało przekazane, a na podstawie pola identyfikuje właściwość).

Ian Ringrose
źródło
1

Pomysł wykorzystujący refleksję:

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
Jacek
źródło
To całkiem fajne, bardziej podoba mi się podejście ekspresyjne. Z drugiej strony powinno być wolniej.
nawfal
1

Zdaję sobie sprawę, że na to pytanie ma już gazillion odpowiedzi, ale żadne z nich nie było dla mnie odpowiednie. Mój problem polega na tym, że nie chcę żadnych hitów wydajnościowych i jestem gotów pogodzić się z małą gadatliwością tylko z tego powodu. Nie dbam też zbytnio o właściwości auto, co doprowadziło mnie do następującego rozwiązania:

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

Innymi słowy, powyższe rozwiązanie jest wygodne, jeśli nie masz nic przeciwko:

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

Plusy

  • Brak odbicia
  • Powiadamia tylko, jeśli stara wartość! = Nowa wartość
  • Powiadom wiele właściwości jednocześnie

Cons

  • Brak właściwości automatycznych (możesz jednak dodać obsługę obu tych funkcji!)
  • Trochę gadatliwości
  • Boks (mały hit wydajności?)

Niestety, nadal jest to lepsze niż robienie tego,

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

Dla każdej pojedynczej nieruchomości, która staje się koszmarem z dodatkową gadatliwością ;-(

Uwaga: nie twierdzę, że to rozwiązanie jest lepsze pod względem wydajności niż inne, tylko że jest to realne rozwiązanie dla tych, którzy nie lubią innych przedstawionych rozwiązań.

James M.
źródło
1

Wymyśliłem tę klasę bazową, aby zaimplementować możliwy do zaobserwowania wzorzec, właściwie robi to, czego potrzebujesz ( „automatycznie” wdrażając zestaw i otrzymaj). Spędziłem na tym godzinę jako prototyp, więc nie ma wielu testów jednostkowych, ale potwierdza tę koncepcję. Zauważ, że wykorzystuje to, Dictionary<string, ObservablePropertyContext>aby usunąć potrzebę prywatnych pól.

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

Oto użycie

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
źródło
1

Proponuję użyć ReactiveProperty. To najkrótsza metoda oprócz Fody.

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

zamiast

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

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

( DOCS )

soi
źródło
0

Inny pomysł...

 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
źródło
0

=> tutaj moje rozwiązanie z następującymi funkcjami

 public ResourceStatus Status
 {
     get { return _status; }
     set
     {
         _status = value;
         Notify(Npcea.Status,Npcea.Comments);
     }
 }
  1. bez refelction
  2. krótka notacja
  3. brak magicznego ciągu w kodzie biznesowym
  4. Możliwość ponownego użycia PropertyChangedEventArgs w różnych aplikacjach
  5. Możliwość powiadomienia o wielu właściwościach w jednym wyciągu
Bruno
źródło
0

Użyj tego

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

}

Koleś505
źródło