Pytanie Implementowanie NotifyPropertyChanged bez ciągów magicznych [duplicate]


Możliwe duplikaty:
typesafe NotifyPropertyChanged za pomocą wyrażeń linq 

Pracuję nad dużą aplikacją zespołową, która cierpi z powodu intensywnego używania magicznych łańcuchów w formie NotifyPropertyChanged("PropertyName"), - standardowe wdrożenie podczas konsultacji z firmą Microsoft. Cierpimy również na dużą liczbę błędnie nazwanych właściwości (praca z modelem obiektowym dla modułu obliczeniowego, który ma setki zapisanych właściwości obliczeniowych) - z których wszystkie są powiązane z interfejsem użytkownika.

Mój zespół doświadcza wielu błędów związanych ze zmianami nazw właściwości prowadzących do niepoprawnych ciągów magicznych i zerwania powiązań. Chcę rozwiązać problem, wprowadzając powiadomienia o zmianie właściwości bez użycia magicznych ciągów. Jedyne rozwiązania, które znalazłem dla .Net 3.5, obejmują wyrażenia lambda. (na przykład: Wdrażanie INotifyPropertyChanged - czy istnieje lepszy sposób?)

Mój menedżer jest bardzo zaniepokojony kosztami związanymi z przestawieniem się

set { ... OnPropertyChanged("PropertyName"); }

do

set {  ... OnPropertyChanged(() => PropertyName); }

skąd pochodzi nazwa

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

Rozważ zastosowanie aplikacji, takiej jak arkusz kalkulacyjny, w którym po zmianie parametru około 100 wartości jest ponownie obliczanych i aktualizowanych w interfejsie użytkownika w czasie rzeczywistym. Czy wprowadzenie tej zmiany jest tak kosztowne, że wpłynie to na szybkość reakcji interfejsu użytkownika? Nie mogę nawet uzasadnić testowania tej zmiany w tej chwili, ponieważ zaktualizowanie zestawów właściwości w różnych projektach i klasach zajęłoby około 2 dni.


16
2017-10-11 15:23


pochodzenie


Używam do tego refleksji. Zobacz mój post na blogu tutaj na ten temat. http://tsells.wordpress.com/2011/02/08/using-reflection-with-wpf-and-the-inotifypropertychanged-interface/ Zwróć szczególną uwagę na notatkę dotyczącą osiągów u dołu postu. - tsells
Dobry artykuł, ale dałeś absolutną różnicę w wydajności, ale to nie jest użyteczne. Byłbym bardziej zainteresowany procentową różnicą w czasie wydajności. Istnieje ogromna różnica między przejściem od 200 ms do 300 ms i przejściem od 0,01 ms do 100,01 ms. Ta sama różnica absolutna, różna różnica procentowa. - Alain
Powiedział, że różnica wynosi około 1/4 sekundy w przypadku 10 000 powiadomień o zmianie własności. Nie wydaje mi się, żeby była na tyle duża różnica, żeby się tym przejmować, a jeśli naprawiasz od razu ponad 10 tys. Nieruchomości, poważnie zastanowiłbym się nad projektem :) - Rachel
@ Kev Nie jestem pewien, do czego się odnosisz. Wydaje się to dziwnym komentarzem do pozostawienia pytania. Jedynym linkiem w pytaniu jest link do innej odpowiedzi SO. W zaakceptowanej odpowiedzi nie ma linków. Artykuł powiązany z tselami jest nieistotny, ponieważ nie odpowiada na pytanie. Naprawdę nie ma żadnego ryzyka związanego z rotacją linków w dowolnym miejscu na tej stronie. - Alain
@Alain - przepraszam, ten komentarz został przeniesiony tutaj z odpowiedzi, która była tylko link. - Kev


Odpowiedzi:


Wykonałem dokładny test programu NotifyPropertyChanged, aby ustalić wpływ przełączania na wyrażenia lambda.

Oto moje wyniki testu:

enter image description here

Jak widać, użycie wyrażenia lambda jest około 5 razy wolniejsze niż zwykła, sztywno zakodowana implementacja zmiany właściwości łańcucha, ale użytkownicy nie powinni się denerwować, ponieważ nawet wtedy jest w stanie wypompować sto tysięcy zmian własności na sekundę tak specjalny komputer roboczy. W związku z tym korzyści wynikające z braku konieczności ciągłego kodowania ciągów znaków i możliwości posiadania jednoliniowych seterów, które zajmują się całym twoim biznesem, znacznie przewyższają mój koszt wydajności.

Test 1 użył standardowej implementacji ustawiającej, sprawdzając, czy właściwość faktycznie się zmieniła:

    public UInt64 TestValue1
    {
        get { return testValue1; }
        set
        {
            if (value != testValue1)
            {
                testValue1 = value;
                InvokePropertyChanged("TestValue1");
            }
        }
    }

Test 2 był bardzo podobny, z dodatkiem funkcji umożliwiającej śledzenie starej wartości i nowej wartości. Ponieważ te funkcje były niejawne w mojej nowej metodzie ustawiania baz, chciałem zobaczyć, ile nowego narzutu wynikało z tej funkcji:

    public UInt64 TestValue2
    {
        get { return testValue2; }
        set
        {
            if (value != testValue2)
            {
                UInt64 temp = testValue2;
                testValue2 = value;
                InvokePropertyChanged("TestValue2", temp, testValue2);
            }
        }
    }

Test 3 to tam, gdzie guma spotkała się z drogą, i mam zamiar pochwalić się tą piękną składnią do wykonywania wszystkich obserwowalnych akcji własnościowych w jednej linii:

    public UInt64 TestValue3
    {
        get { return testValue3; }
        set { SetNotifyingProperty(() => TestValue3, ref testValue3, value); }
    }

Realizacja

W mojej klasie BindingObjectBase, którą wszystkie modele ViewModels kończą dziedziczyć, znajduje się implementacja sterująca nową funkcją. Usunąłem obsługę błędów, więc mięso funkcji jest jasne:

protected void SetNotifyingProperty<T>(Expression<Func<T>> expression, ref T field, T value)
{
    if (field == null || !field.Equals(value))
    {
        T oldValue = field;
        field = value;
        OnPropertyChanged(this, new PropertyChangedExtendedEventArgs<T>(GetPropertyName(expression), oldValue, value));
    }
}
protected string GetPropertyName<T>(Expression<Func<T>> expression)
{
    MemberExpression memberExpression = (MemberExpression)expression.Body;
    return memberExpression.Member.Name;
}

Wszystkie trzy metody spotykają się w rutynie OnPropertyChanged, która wciąż jest standardem:

public virtual void OnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
        handler(sender, e);
}

Premia

Jeśli ktoś jest ciekawy, PropertyChangedExtendedEventArgs jest czymś, co właśnie wymyśliłem, aby rozszerzyć standard PropertyChangedEventArgs, więc instancja rozszerzenia zawsze może być na miejscu bazy. Wykorzystuje znajomość starej wartości, gdy właściwość zmienia się za pomocą SetNotifyingProperty, i udostępnia tę informację do obsługi.

public class PropertyChangedExtendedEventArgs<T> : PropertyChangedEventArgs
{
    public virtual T OldValue { get; private set; }
    public virtual T NewValue { get; private set; }

    public PropertyChangedExtendedEventArgs(string propertyName, T oldValue, T newValue)
        : base(propertyName)
    {
        OldValue = oldValue;
        NewValue = newValue;
    }
}

23
2017-10-12 15:28



Co ciekawe, przeprowadziłem podobny test na mojej maszynie jakiś czas temu (2013) i nie mogłem zmierzyć żadnej różnicy wydajności (.NET 3.5 SP1, Win7 x64, Core i7) - ChrisWue
@Alain - mógłby zostać ulepszony do jeszcze ładniejszej wersji przy użyciu nowego atrybutu [CallerMemberName] - SetNotifyingProperty (ref storage, value); - Axarydax
Tak, myślę, że był to dość powszechny wzorzec, że MS pracuje nad bardziej wydajnymi sposobami uzyskiwania nazw właściwości w najnowszych wersjach .NET. Zamierzam zasugerować, że jest to tak dobre, jak tylko dla wersji 3.5 lub 4.0. - Alain
Re: "jest w stanie wypompować sto tysięcy zmian własności na sekundę". Zgadzam się, że to najprawdopodobniej więcej niż wystarczające. Ale na wszelki wypadek pamiętaj, że wyrażenie szybkości implementacji w jednym sekundzie czasu może nie mieć większego znaczenia: INotifyPropertyChanged jest najczęściej używany do kodu UI, w którym chcesz zachować wyczuwalne opóźnienia do minimum; ludzki czas reakcji wynosi około 1/10 sekundy. Zatem implementacja musi działać wystarczająco dobrze w oknie o czasie 50-100 milisekund. - stakx


Osobiście lubię korzystać z Microsoft PRISM NotificationObject z tego powodu i przypuszczam, że ich kod jest rozsądnie zoptymalizowany, ponieważ został stworzony przez Microsoft.

Pozwala mi używać kodu takiego jak RaisePropertyChanged(() => this.Value);, oprócz utrzymywania "Magicznych ciągów", aby nie złamać żadnego istniejącego kodu.

Jeśli spojrzę na ich kod za pomocą Reflectora, ich implementację można odtworzyć za pomocą poniższego kodu

public class ViewModelBase : INotifyPropertyChanged
{
    // Fields
    private PropertyChangedEventHandler propertyChanged;

    // Events
    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            PropertyChangedEventHandler handler2;
            PropertyChangedEventHandler propertyChanged = this.propertyChanged;
            do
            {
                handler2 = propertyChanged;
                PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Combine(handler2, value);
                propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2);
            }
            while (propertyChanged != handler2);
        }
        remove
        {
            PropertyChangedEventHandler handler2;
            PropertyChangedEventHandler propertyChanged = this.propertyChanged;
            do
            {
                handler2 = propertyChanged;
                PropertyChangedEventHandler handler3 = (PropertyChangedEventHandler)Delegate.Remove(handler2, value);
                propertyChanged = Interlocked.CompareExchange<PropertyChangedEventHandler>(ref this.propertyChanged, handler3, handler2);
            }
            while (propertyChanged != handler2);
        }
    }

    protected void RaisePropertyChanged(params string[] propertyNames)
    {
        if (propertyNames == null)
        {
            throw new ArgumentNullException("propertyNames");
        }
        foreach (string str in propertyNames)
        {
            this.RaisePropertyChanged(str);
        }
    }

    protected void RaisePropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        string propertyName = PropertySupport.ExtractPropertyName<T>(propertyExpression);
        this.RaisePropertyChanged(propertyName);
    }

    protected virtual void RaisePropertyChanged(string propertyName)
    {
        PropertyChangedEventHandler propertyChanged = this.propertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

public static class PropertySupport
{
    // Methods
    public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
    {
        if (propertyExpression == null)
        {
            throw new ArgumentNullException("propertyExpression");
        }
        MemberExpression body = propertyExpression.Body as MemberExpression;
        if (body == null)
        {
            throw new ArgumentException("propertyExpression");
        }
        PropertyInfo member = body.Member as PropertyInfo;
        if (member == null)
        {
            throw new ArgumentException("propertyExpression");
        }
        if (member.GetGetMethod(true).IsStatic)
        {
            throw new ArgumentException("propertyExpression");
        }
        return body.Member.Name;
    }
}

4
2017-10-11 15:35



Co na świecie robi PRISM przy użyciu metod dodawania / usuwania zdarzeń PropertyChanged? W każdym razie wygląda na to, że skutecznie robią dokładnie to, co mam powyżej: Expression body.Member.Name. Nie pomaga jednak w kwestii wydajności. PRISM oczywiście nie jest zoptymalizowany pod względem wydajności - gdyby to było, wykorzystaliby ponownie swoją zdefiniowaną zmienną "member"zamiast dzwonić"body.Member"drugi raz na dole ich ExtractPropertyName metoda. - Alain
Niektórzy twierdzą, że powiedzenie "Kod xxx jest tak zoptymalizowany, jak to możliwe, ponieważ jest przez Microsoft" jest oksymoronem. Nie sądzę, że to prawda, ale prawda robi leżeć gdzieś pośrodku. - Sascha Hennig
@Alain Mogło to również zostać błędnie odzwierciedlone. Musiałem wprowadzić drobne zmiany w kodzie, aby go skompilować. Prawdopodobnie łatwo byłoby wprowadzić taką zmianę do twojej klasy bazowej, uruchomić test wydajności z twoimi Magic Strings w, a następnie zrobić find / replace używając wyrażeń regularnych, aby zastąpić połączenia PropertyChange nowymi lambda i ponownie uruchomić swoje Testy wydajności. - Rachel
Zgadzam się, nie lubię mówić, że jest "zoptymalizowany, jak to tylko możliwe" tylko dlatego, że został stworzony przez Microsoft. Trochę zmodyfikowałem tekst, aby powiedzieć, że powinien on być "rozsądnie zoptymalizowany" - Rachel
@Alain: zauważ, że Rachel napisała dekompilowany kod. Jeśli spojrzysz na oryginalny kod źródłowy, możesz nie zobaczyć tych zdarzeń zaimplementowanych ręcznie (add, remove). To wszystko może być dziełem kompilatora. Jeśli wziąłeś Reflectora z 2011 roku i spojrzałeś na swoje własne wydarzenia, możesz zobaczyć wiele z tego samego. - stakx


Jeśli obawiasz się, że rozwiązanie drzewa ekspresji lambda może być zbyt wolne, zrób profil i dowiedz się. Podejrzewam, że czas spędzony na pęknięciu drzewa wyrażeń byłby trochę mniejszy niż czas, jaki UI poświęci na odświeżenie w odpowiedzi.

Jeśli okaże się, że jest zbyt wolny i musisz użyć literalnych ciągów znaków, aby spełnić kryteria wydajności, oto jedna z metod, które widziałem:

Utwórz podstawową klasę, która implementuje INotifyPropertyChangedi daj mu RaisePropertyChanged metoda. Ta metoda sprawdza, czy zdarzenie ma wartość NULL, i tworzy wartość PropertyChangedEventArgsi odpala to wydarzenie - wszystkie zwykłe rzeczy.

Ale metoda zawiera również dodatkową diagnostykę - robi ona odbicie, aby upewnić się, że klasa naprawdę ma właściwość o tej nazwie. Jeśli właściwość nie istnieje, zgłasza wyjątek. Jeśli właściwość istnieje, to zapamiętuje ten wynik (np. Dodając nazwę właściwości do statycznego HashSet<string>), więc nie musi ponownie sprawdzać odbicia.

I gotowe: twoje automatyczne testy zaczną się zawodzić, gdy tylko zmienisz nazwę nieruchomości, ale nie zaktualizujesz magicznego ciągu znaków. (Zakładam, że masz zautomatyzowane testy dla swoich ViewModels, ponieważ to jest główny powód używania MVVM.)

Jeśli nie chcesz przestać działać tak hałaśliwie, możesz umieścić dodatkowy kod diagnostyczny w środku #if DEBUG.


2
2017-10-11 16:24



Dobra sugestia na wypadek, gdyby drzewo ekspresji lambda nie rozwinęło się. - Alain


Właściwie to omawialiśmy to samo z naszymi projektami i dużo rozmawialiśmy o zaletach i wadach. W końcu zdecydowaliśmy się zachować zwykłą metodę, ale użyliśmy do tego pola.

public class MyModel
{
    public const string ValueProperty = "Value";

    public int Value
    {
        get{return mValue;}
        set{mValue = value; RaisePropertyChanged(ValueProperty);
    }
}

Pomaga to w refaktoryzacji, utrzymuje naszą wydajność i jest szczególnie pomocne, gdy używamy PropertyChangedEventManager, gdzie będziemy potrzebować ponownie zakodowanych na sztywno łańcuchów.

public bool ReceiveWeakEvent(Type managerType, object sender, System.EventArgs e)
{
    if(managerType == typeof(PropertyChangedEventManager))
    {
        var args = e as PropertyChangedEventArgs;
        if(sender == model)
        {
            if (args.PropertyName == MyModel.ValueProperty)
            {

            }

            return true;
        }
    }
}

1
2017-10-11 15:59



Och, przyszedłeś, kiedy zgodziłeś się przynajmniej powiedzieć dlaczego. Moja odpowiedź jest ważną alternatywą dla problemu. Używanie zakodowanych na sztywno łańcuchów przeciwko magii Lambda, co może zranić wydajność. Moje rozwiązanie nie zaszkodzi wydajności i jest łatwiejsze w utrzymaniu niż ciągi kodowane na sztywno. - dowhilefor
Nie spadłem, ale to nie jest pomocne. Wciąż dotyczy to mocno zakodowanych łańcuchów. W rzeczywistości jest gorzej, ponieważ jeśli mam 100 właściwości, wymaga 100 nowych łańcuchów do przechowywania tych nazw, a teraz, gdy zmieniam nazwę tej właściwości, muszę zmienić nazwę, sztywno zakodowany ciąg i nazwę Twardy zakodowany ciąg. - Alain
Dla nas zmiana nazwy nie była problemem, może dlatego, że używamy resharpera i jesteśmy z tego powodu trochę rozpieszczeni. Po drugie, generowane są nasze modele, dzięki czemu wszystkie właściwości mogą łatwo uzyskać identyfikator. Ostatni, ale nie mniej ważny, był dla nas najważniejszy, oczywiście, że nie jest tak ładny jak lambda, ale dla nas działa całkiem nieźle. Szczególnie część o WeakEventManager. Ostatecznie pomyślałem, że inne podejście może być pomocne przy rozważaniu i rozważaniu zalet i wad dostępnych rozwiązań. Ale szanuję, jeśli nie jest to dla ciebie pomocne. - dowhilefor
Nawet bez ReSharper zmiana nazwy identyfikatora jest banalna - Visual Studio ma refaktoryzację nazwy. (Oczywiście, dlaczego ktokolwiek chciałby kodować bez ReSharper?) - Joe White


Jednym prostym rozwiązaniem jest po prostu wstępne przetworzenie wszystkich plików przed kompilacją, wykrycie OnPropertyChanged wywołania zdefiniowane w blokach zestawu {...}, określają nazwę właściwości i poprawiają parametr nazwy.

Możesz to zrobić za pomocą narzędzia ad-hoc (które byłoby moją rekomendacją) lub użyć prawdziwego parsera C # (lub VB.NET) (takiego jak te, które można znaleźć tutaj: Analizator składni dla języka C #).

Myślę, że to rozsądny sposób, aby to zrobić. Oczywiście nie jest to zbyt eleganckie ani inteligentne, ale ma zerowy wpływ na środowisko i jest zgodne z zasadami Microsoftu.

Jeśli chcesz zaoszczędzić trochę czasu na kompilację, możesz mieć obie możliwości korzystania z dyrektyw kompilacji, na przykład:

set
{
#if DEBUG // smart and fast compile way
   OnPropertyChanged(() => PropertyName);
#else // dumb but efficient way
   OnPropertyChanged("MyProp"); // this will be fixed by buid process
#endif
}

1
2017-10-11 16:31