Реалізація INotifyPropertyChanged - чи існує кращий спосіб?


647

Майкрософт повинен був реалізувати щось спритне для INotifyPropertyChanged, наприклад, в автоматичних властивостях, просто уточнюю, {get; set; notify;} я думаю, що це має багато сенсу. Або є якісь ускладнення для цього?

Чи можемо ми самі реалізувати щось подібне до "сповіщення" у власності. Чи є витончене рішення для впровадження INotifyPropertyChangedу вашому класі чи єдиний спосіб зробити це - підняття PropertyChangedподії у кожній власності.

Якщо не можемо ми написати щось для автоматичного генерування фрагмента коду для PropertyChanged події?


7
code.google.com/p/notifypropertyweaver може бути корисним
Ian Ringrose

7
вище посилання мертва. github.com/SimonCropp/NotifyPropertyWeaver
prime23

2
Ви можете замість цього використовувати DependencyObject та DependencyProperties. ХА! Я зробив смішне.
Філ-


5
На той час внесення змін до C # було неможливим, враховуючи, що ми мали величезний зворотний журнал взаємозалежностей. Тож, коли MVVM народився, я здогадуюсь, ми просто не доклали великих зусиль для вирішення цього питання, і я знаю, що команда Patterns & Practices мала декілька кроків на цьому шляху (отже, ви також отримали MEF як частину цього дослідницька нитка). Сьогодні я думаю, що [CallerMemberName] є відповіддю на сказане.
Скотт Барнс

Відповіді:


633

Не використовуючи щось на зразок postsharp, мінімальна версія, яку я використовую, використовує щось на кшталт:

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

Кожна власність - це щось на зразок:

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

яка не величезна; він також може бути використаний як базовий клас, якщо ви хочете. boolПовернення з SetFieldговорить вам , якщо це не було-оп, в разі , якщо ви хочете застосувати іншу логіку.


або ще простіше за допомогою C # 5:

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

які можна назвати так:

set { SetField(ref name, value); }

за допомогою якого компілятор додасть "Name"автоматично.


C # 6.0 полегшує реалізацію:

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

... і тепер із 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
Гарний трюк Марку! Я запропонував вдосконалити використання ламбда-виразу замість імені властивості, дивіться мою відповідь
Thomas Levesque

7
@Thomas - лямбда - це все добре і добре, але це додає багато накладних витрат на щось, що насправді дуже просто. Зручна хитрість, але я не впевнений, що це завжди практично.
Марк Гравелл

14
@Marc - Так, ймовірно, це може погіршити продуктивність ... Однак мені дуже подобається те, що він перевіряється під час компіляції і правильно відремонтований командою "Перейменувати"
Thomas Levesque

4
@Gusdor, на щастя, з C # 5 не потрібно йти на компроміси - ви можете отримати найкраще з обох через (як зазначає Pedro77)[CallerMemberName]
Марк Гравелл

4
@Gusdor мова та рамки розділені; ви можете використовувати компілятор C # 5, націлити .NET 4 і просто додати відсутній атрибут самостійно - він буде добре працювати. Він просто повинен мати правильне ім’я та знаходитись у правильному просторі імен. Це не потрібно бути в певній збірці.
Марк Гравелл

196

Станом на .Net 4.5, нарешті, є простий спосіб зробити це.

.Net 4.5 представляє нові атрибути інформації про абонента.

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

Це, мабуть, хороша ідея, щоб додати порівняльну функцію також.

EqualityComparer<T>.Default.Equals

Більше прикладів тут і тут

Також див. Інформацію про абонента (C # та Visual Basic)


12
Блискуче! Але чому це загальне?
абатищев

@abatishchev Я думаю, це не повинно бути, я просто грав з думкою про те, щоб функція встановила також властивість. Я побачу, чи можу я оновити свою відповідь, щоб забезпечити повне рішення. Додаткові приклади роблять хорошу роботу, яка тим часом.
Даніель Літтл

3
Його ввів C # 5.0. Це не має нічого спільного з .net 4.5, але це чудове рішення!
Дж. Леннон

5
@J. Lennon .net 4.5 все ще має щось спільне
Daniel Little

@Lavinski змініть свою заявку на напр. .NET 3.5 і подивіться, що буде працювати (в vs2012)
Дж. Леннон,

162

Мені дуже подобається рішення Марка, але я думаю, що його можна трохи вдосконалити, щоб уникнути використання "магічної струни" (яка не підтримує рефакторинг). Замість використання імені властивості як рядка, легко зробити це виразом лямбда:

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

Просто додайте наступні методи до коду Марка, це зробить трюк:

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

До речі, на це надихнула оновлена ​​URL-адреса цієї публікації в блозі


6
Існує принаймні один фреймворк, що використовує цей метод, ReactiveUI .
AlSki

Дуже пізно, це означало пройти роздуми, що означало хіт вистави. Це може бути прийнятним, але налаштування властивості - це не місце, де я хотів би, щоб моя програма проводила багато циклів.
Бруно Брант

1
@BrunoBrant Ви впевнені, що хіт на продуктивність? Відповідно до публікації в блозі, відображення відбувається під час компіляції, а не під час виконання (тобто статичне відображення).
Натаніел Елкінс

6
Я вважаю, що весь ваш OnPropertyChanged <T> застарілий з ім'ям оператора C # 6, що робить цього монстра трохи витонченим.
Traubenfuchs

5
@Traubenfuchs, власне, атрибут CallerMemberName C # 5 робить його ще простішим, оскільки вам взагалі нічого не потрібно пропускати ...
Томас Левеск

120

Також є Fody, який має надбудову PropertyChanged , яка дозволяє вам написати це:

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

... і під час компіляції вводить сповіщення про зміну властивості.


7
Я думаю, що саме це шукало ОП, коли вони запитували "Чи можемо ми самі реалізувати щось подібне до" сповіщення "у своїх властивостях. Чи є витончене рішення для впровадження INotifyPropertyChanged у своєму класі"
Ashoat

3
Це єдине витончене рішення насправді, і воно працює бездоганно, як сказав @CADbloke. І я скептично ставився до ткача, але я перевірив / перевірив код ІЛ позаду, і це ідеально, це просто, робить все, що вам потрібно, і ніхто інший. Він також підключає і викликає будь-яке ім'я методу, яке ви вказали в базовому класі для нього, будь то NotifyOnProp ..., OnNotify ... не має значення, тому добре працює з будь-яким базовим класом, який у вас може бути, і який реалізує INotify .. .
NSGaga - здебільшого-неактивний

1
Ви можете легко двічі перевірити, чим займається ткацький супровід, ознайомитись із вікном виводу збірки, у ньому перераховано всі об’єкти PropertyChanged, які він сплив. Використання розширення VScolorOutput з малюнком регулярного вираження "Fody/.*?:",LogCustom2,Trueвиділяє його в кольорі "Спеціальні 2". Я зробив його яскраво-рожевим, тому його легко знайти. Просто Fody все, це найновіший спосіб зробити все, що має багато повторюваних текстів.
CAD блокується

@mahmoudnezarsarhan ні, це не так, я пам’ятаю, відбулася невелика зміна способу його налаштування, але Fody PropertyChanged все ще живий і активний.
Ларрі

65

Я думаю, що люди повинні приділити трохи більше уваги продуктивності; це дійсно впливає на користувальницький інтерфейс, коли є багато об'єктів, які потрібно пов'язати (придумайте сітку з 10 000+ рядків) або якщо значення об'єкта часто змінюється (додаток для моніторингу в реальному часі).

Я брав різні варіанти знайдених тут і в інших місцях і робив порівняння; перевірте це порівняння ефективності реалізацій INotifyPropertyChanged .


Ось зазирнути на результат Реалізація проти виконання


14
-1: накладні показники продуктивності відсутні: CallerMemberName змінюються на буквальні значення під час компіляції. Просто спробуйте і декомпілюйте додаток.
JYL

ось з питання і відповідь: stackoverflow.com/questions/22580623 / ...
uli78

1
@JYL, ви праві, що CallerMemberName не додав великих накладних витрат. Я, мабуть, здійснив щось неправильне, коли я спробував це. Пізніше я оновлю блог і відповім, щоб відобразити орієнтир для реалізації CallerMemberName та Fody.
Peijen

1
Якщо в інтерфейсі у вас є сітка з 10 000+, то, ймовірно, вам слід поєднувати підходи для обробки продуктивності, наприклад, підказка, де ви показуєте лише 10, 50, 100, 250 звернень на сторінку ...
Остін Рімер

Остін Рімер, якщо у вас є великі дані + 50 використовувати віртуалізацію даних, не потрібно завантажувати всі дані, вони завантажуватимуть лише ті дані, які видно на поточній області відображення шкали!
Білал

38

Я представляю клас Bindable у своєму блозі за адресою http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable використовує словник як пакет властивостей. Досить просто додати необхідні перевантаження для підкласу для управління власним полем резервного копіювання за допомогою параметрів ref.

  • Ніякої магічної струни
  • Немає роздумів
  • Можна вдосконалити, щоб придушити пошук словника за замовчуванням

Код:

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

Його можна використовувати так:

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

2
Це приємне рішення, але єдиним недоліком є ​​те, що є невеликий хіт на продуктивність, пов’язаний з боксом / розпакуванням.
MCattle

1
Я б запропонував використати, protected T Get<T>(T defaultValue, [CallerMemberName] string name = null)а також встановити прапорець if (_properties.ContainsKey(name) && Equals(value, Get<T>(default(T), name)))Встановити (підняти та зберегти, коли вперше встановлено значення за замовчуванням)
Miquel,

1
@Miquel додавання підтримки для спеціальних значень за замовчуванням може бути корисним напевно, проте слід бути обережним лише піднімати змінену подію лише тоді, коли значення фактично змінилося. Визначення властивості таким же значенням, яке воно мало, не повинно викликати події. Я мушу визнати, що в більшості випадків це нешкідливо, однак я був кусаний досить декілька разів, коли властивості тисячі разів встановлювали однакове значення з подіями, що руйнують чуйність інтерфейсу користувача.
TiMoch

1
@stakx У мене є декілька додатків, які працюють на цьому для підтримки шаблону пам’яті для скасування / повторення або для включення одиничного шаблону роботи у додатках, де nhibernate не використовується
TiMoch

1
Мені дуже подобається це конкретне рішення: коротка нотація, відсутність динамічного проксі-сервера, відсутність IL-втручання тощо. Хоча ви можете скоротити його, усунувши необхідність вказувати T кожен раз для Get, зробивши Get return динамичним. Я знаю, це впливає на продуктивність виконання, але тепер код для геттерів і сеттерів, нарешті, може бути завжди однаковим і в одному рядку слава Господу! PS Вам слід дотримуватися додаткової обережності всередині методу Get (одноразово, коли ви пишете базовий клас), повертаючи значення за замовчуванням для valuetypes як динамічні. Не забудьте завжди повертати правильні значення за замовчуванням (це можна зробити)
evilkos

15

Насправді я ще не мав можливості спробувати це самостійно, але наступного разу, коли я створюю проект з великою вимогою до INotifyPropertyChanged, я маю намір написати атрибут Postsharp, який вводить код під час компіляції. Щось на зразок:

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

Стане:

private string _firstName;

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

Я не впевнений, чи це спрацює на практиці, і мені потрібно сісти і спробувати це, але я не бачу, чому б ні. Мені може знадобитися змусити його приймати деякі параметри для ситуацій, коли потрібно запустити більше одного OnPropertyChanged (якщо, наприклад, у мене було властивість FullName у класі вище)

Наразі я використовую користувальницький шаблон у Resharper, але навіть при цьому мені набридає всі мої властивості, що знаходяться настільки довго.


Ах, швидкий пошук Google (який я повинен був зробити , перш ніж я написав це) показує , що принаймні одна людина зробила що - щось подібне раніше тут . Не зовсім те, що я мав на увазі, але досить близько, щоб показати, що теорія хороша.


6
Вільний інструмент під назвою Fody, здається, робить те саме, функціонуючи як загальний інжектор коду компіляційного часу. Це можна завантажити в Nuget, як і його пакети плагінів PropertyChanged та PropertyChanging.
Трайнко

11

Так, кращий спосіб, безумовно, існує. Ось:

Покроковий підручник скоротив мене, спираючись на цю корисну статтю .

  • Створіть новий проект
  • Встановіть основний пакет замка в проект

Install-Package Castle.Core

  • Встановлюйте лише бібліотеки mvvm light

Встановити-пакет MvvmLightLibs

  • Додайте два класи в проект:

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;
    }
}
  • Створіть модель перегляду, наприклад:

-

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

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

        public void TestAction()
        {
            Trace.WriteLine(MainTextBox);
        }
    }
  • Покладіть прив’язки до xaml:

    <TextBox Text="{Binding MainTextBox}" ></TextBox>
    <Button Command="{Binding TestActionCommand}" >Test</Button>
  • Покладіть рядок коду у файл MainWindow.xaml.cs, що знаходиться за кодом, таким чином:

DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();

  • Насолоджуйтесь.

введіть тут опис зображення

Увага !!! Усі обмежені властивості повинні бути прикрашені віртуальним ключовим словом, оскільки вони використовуються проксі-замками для переопределення.


Мені цікаво дізнатися, яку версію Castle ви використовуєте. Я використовую 3.3.0 і CreateClassProxy метод не має цих параметрів: type, interfaces to apply, interceptors.
IAb Abstract

Незважаючи на те, я використовував загальний CreateClassProxy<T>метод. Набагато різні ... хммм, цікаво, чому так обмежений родовим методом. :(
IAb Abstract


5

Подивіться тут: http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx

Це написано німецькою мовою, але ви можете завантажити ViewModelBase.cs. Усі коментарі в cs-File написані англійською мовою.

За допомогою цього класу ViewModelBase можна реалізувати властивості біндінгу, подібні до добре відомих властивостей залежності:

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

1
Посилання розірвано.
Guge

4

На основі відповіді Томаса, яка була адаптована з відповіді Марка, я перетворив код, що відображає властивість, змінив код в базовий клас:

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

Використання те саме, що відповідь Томаса, за винятком того, що ви можете передавати додаткові властивості, про які потрібно повідомити. Це було необхідно для обробки обчислених стовпців, які потрібно оновити в сітці.

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

У мене це рушій колекції предметів, що зберігаються в BindingList, відкритих через DataGridView. Це позбавило мене необхідності робити ручні дзвінки Refresh () в мережу.


4

Дозвольте представити власний підхід під назвою Yappi . Він належить генераторам класів похідних проксі | Runtime, додаючи нову функціональність до вже існуючого об'єкта або типу, як-от динамічний проксі-клас Caste Project.

Це дозволяє реалізувати INotifyPropertyChanged один раз у базовому класі, а потім оголосити похідні класи у наступному стилі, все ще підтримуючи INotifyPropertyChanged для нових властивостей:

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

Складність побудови похідних класів або проксі може бути прихована за наступним рядком:

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

І всі роботи з впровадження INotifyPropertyChanged можна виконати так:

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

Він повністю безпечний для рефакторингу, не використовує відбитків після побудови типу та досить швидко.


Для чого вам потрібен TDeclarationпараметр типу PropertyImplementation? Напевно, ви можете знайти відповідний тип, щоб викликати (не callvirt) геттера / сеттера лише з TImplementation?
Андрій Савіних

У більшості випадків реалізація TI. Винятки становлять: 1. Властивості, визначені "новим" клавішем C #. 2. Властивості явної реалізації інтерфейсу.
Келквалін

3

Усі ці відповіді дуже приємні.

Моє рішення - використовувати фрагменти коду, щоб зробити цю роботу.

Для цього використовується найпростіший виклик події PropertyChanged.

Збережіть цей фрагмент і використовуйте його під час використання фрагмента 'fullprop'.

розташування можна знайти в меню "Інструменти \ Менеджер фрагментів коду ..." у 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>

Ви можете змінювати дзвінок за своїм бажанням (використовувати вищезазначені рішення)


2

Якщо ви використовуєте динаміку в .NET 4.5, вам не потрібно турбуватися INotifyPropertyChanged.

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

якщо Ім'я пов'язане з деяким контролем, воно просто працює добре.


1
будь-які недоліки використання цього?
juFo

2

Іншим комбінованим рішенням є використання 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));
        }
    }
}

Використання:

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

2
Це швидко? Чи не доступ до кадру стека пов'язаний з якоюсь вимогою дозволу? Це надійне в контексті використання асинхронізації / очікування?
Стефан Гурішон

@ StéphaneGourichon Ні, це не так. Доступ до кадру стека означає значну ефективність у більшості випадків.
Бруно Брант

Так, є, ви можете побачити це на codereview.stackexchange.com/questions/13823/…
Ofir

Зауважте, що вбудоване може приховати get_Fooметод у режимі випуску.
bytecode77

2

Я створив метод розширення у своїй базовій бібліотеці для повторного використання:

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

Це працює з .Net 4.5 через CallerMemberNameAttribute . Якщо ви хочете використовувати його з більш ранньою версією .Net, вам потрібно змінити декларацію методу з: ...,[CallerMemberName] string propertyName = "", ...на...,string propertyName, ...

Використання:

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

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

2

Я вирішив таким способом (це трохи трудомістке використання, але це, безумовно, швидше під час виконання).

У VB (вибачте, але я думаю, що це не важко перекласти це на C #), я роблю цю заміну на RE:

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

з:

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

У цьому коді трансфрм весь цей код:

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

В

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

І якщо я хочу мати більш читабельний код, я можу бути протилежною, лише зробивши таку заміну:

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

З

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

Я кидаю, щоб замінити IL-код заданого методу, але я не можу написати багато скомпільованого коду в IL ... Якщо за день я його напишу, я скажу вам!


2

Я зберігаю це навколо як фрагмент. C # 6 додає симпатичний синтаксис для виклику обробника.

// 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

Ось версія Unity3D або non-CallerMemberName 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
}

Цей код дозволяє записати такі резервні поля властивості:

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

Крім того, в режимі перерозподілу, якщо ви створюєте фрагмент / шаблон пошуку, ви також можете автоматизувати робочий процес шляхом перетворення простих опорних полів у вищевказану резервну копію.

Шаблон пошуку:

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

Замінити шаблон:

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

2

Я написав статтю, яка допомагає в цьому ( https://msdn.microsoft.com/magazine/mt736453 ). Ви можете використовувати пакет SolSoft.DataBinding NuGet. Тоді ви можете написати такий код:

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

Переваги:

  1. базовий клас необов’язковий
  2. відсутність роздумів про кожну "задану вартість"
  3. може мати властивості, які залежать від інших властивостей, і всі вони автоматично викликають відповідні події (стаття має приклад цього)

2

Хоча, очевидно, існує маса способів зробити це, за винятком магічних відповідей AOP, але жодна з відповідей не виглядає як встановити властивість моделі безпосередньо з моделі перегляду, не маючи місцевого поля для посилання.

Проблема полягає в тому, що ви не можете посилатися на ресурс. Однак ви можете використовувати Дію, щоб встановити це властивість.

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

Це може використовуватися як наступний витяг коду.

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

Перегляньте це репортаж BitBucket на повну реалізацію методу та кілька різних способів досягнення того ж результату, включаючи метод, що використовує LINQ, і метод, який використовує відображення. Зауважте, що ці методи мають меншу ефективність.


1

Інші речі, які ви можете врахувати при впровадженні подібних властивостей, полягає в тому, що обидва INotifyPropertyChang * ed * ing використовують класи аргументів подій.

Якщо у вас є велика кількість властивостей, які встановлюються, то кількість екземплярів класу аргументів подій може бути величезною, вам слід розглянути можливість кешування їх, оскільки вони є однією з областей, в якій може статися вибух рядків.

Погляньте на цю реалізацію та пояснення, чому вона була задумана.

Блог Джоша Смітса


1

Щойно я знайшов ActiveSharp - Automatic INotifyPropertyChanged , я ще не використовував його, але він виглядає добре.

Цитувати з цього веб-сайту ...


Надсилайте сповіщення про зміну властивості, не вказуючи ім'я властивості як рядок

Замість цього запишіть такі властивості:

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

Зауважте, що не потрібно включати ім'я властивості як рядок. ActiveSharp надійно і правильно розраховує це для себе. Він працює на основі того, що ваша реалізація властивості передає поле резервного копіювання (_foo) за посиланням. (ActiveSharp використовує цей виклик "за посиланням", щоб визначити, яке поле резервного копіювання передано, а з поля визначить властивість).


1

Ідея з використанням рефлексії:

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

Це досить круто, мені це подобається більше, ніж виразний підхід. Знизу, слід повільніше.
nawfal

1

Я усвідомлюю, що це запитання вже має мільйони відповідей, але жодна з них не вважала мене цілком правильним. Моє питання полягає в тому, що я не бажаю жодних хітів на виставу, і я готовий миритися з невеликою багатослівністю лише з цієї причини. Я також не надто переймаюся автоматичними властивостями, що призвело до наступного рішення:

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

Іншими словами, вищевказане рішення зручно, якщо ви не проти зробити це:

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

Плюси

  • Немає роздумів
  • Повідомляє лише якщо старе значення! = Нове значення
  • Повідомте відразу декілька властивостей

Мінуси

  • Немає автоматичних властивостей (проте ви можете додати підтримку для обох!)
  • Якась багатослівність
  • Бокс (невеликий хитовий показник?)

На жаль, це все-таки краще, ніж робити це,

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

Для кожного майна, яке стає кошмаром з додатковим багатослів’ям ;-(

Зауважте, я не стверджую, що це рішення є кращим за ефективність у порівнянні з іншими, лише що це життєздатне рішення для тих, хто не любить інші представлені рішення.


1

Я придумав цей базовий клас, щоб реалізувати спостережувану схему, майже все, що потрібно ( "автоматично" реалізуючи набір і отримати). Я витратив лінійку години на це як на прототип, тому в ньому немає багато одиничних тестів, але це підтверджує концепція. Зауважте, він використовує Dictionary<string, ObservablePropertyContext>для усунення потреби в приватних полях.

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

Ось використання

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

Я пропоную використовувати ReactiveProperty. Це найкоротший метод, крім Фоді.

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

замість цього

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

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

( DOCS )


0

Ще одна ідея ...

 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

=> тут моє рішення із наступними особливостями

 public ResourceStatus Status
 {
     get { return _status; }
     set
     {
         _status = value;
         Notify(Npcea.Status,Npcea.Comments);
     }
 }
  1. відсутність рефлексу
  2. коротке позначення
  3. у вашому коді бізнесу немає магічної стрічки
  4. Повторність використання PropertyChangedEventArgs для програми
  5. Можливість сповіщення кількох властивостей в одному операторі

0

Використовуй це

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

}

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.