Допустим, есть проект на WPF и в нём ViewModel, в которой есть два свойства Price и Quantity, и вычислимое свойство TotalPrice=Price*Quantity

Код
public class Order : BaseViewModel
    {
        private double _price;
        private double _quantity;

        public double Price 
        {
            get { return _price; }
            set
            {
                if (_price == value)
                    return;
                _price = value;
                RaisePropertyChanged("Price");
            }
        }

        public double Quantity
        {
            get { return _quantity; }
            set
            {
                if (_quantity == value)
                    return;
                _quantity = value;
                RaisePropertyChanged("Quantity");
            }
        }

        public double TotalPrice {get { return Price*Quantity; }}
    }

    public class BaseViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

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




Если Price будет изменен в коде, то изменения цены автоматически отобразятся в View, потому что ViewModel сообщит View об изменении Price посредством вызовом события RaisePropertyChanged(«Price»). Вычисляемое TotalPrice же не изменится в View, потому что никто не вызывает RaisePropertyChanged(«TotalPrice»). Можно вызывать RaisePropertyChanged(«TotalPrice») в тех же местах, где вызывается RaisePropertyChanged(«Price») и RaisePropertyChanged(«Quantity»), но не хотелось бы размазывать по множеству мест информацию о том, что TotalPrice зависит от Price и Quantity, а хотелось бы хранить информацию об этом в одном месте. С этой целью люди пишут разнообразные менеджеры зависимостей, но давайте посмотрим какой минимальный код на самом деле нужен для этого.

Стандартный способ прокинуть логику туда, где ей не место с точки зрения дизайна, — это события. Подход в лоб заключается в создании двух событий OnPriceChanged и OnQuantityChanged. При срабатывании этих событий делать RaisePropertyChanged(«TotalPrice»). Сделаем подписку на эти события в конструкторе ViewModel. После этого информация о том, что TotalPrice зависит от Price и Quantity будет в одном месте — в конструкторе (ну, или в отдельном методе, если вы того пожелаете).

Немного упростим задачу: у нас уже есть событие PropertyChanged, срабатывающее при изменении Price, вот его и используем.

        public void RegisterPropertiesDependencies(string propertyName, List<string> dependenciesProperties)
        {
            foreach (var dependencyProperty in dependenciesProperties)
            {
                this.PropertyChanged += (sender, args) =>
                {
                    if (args.PropertyName == dependencyProperty) RaisePropertyChanged(propertyName);
                };  
            }
        }

        RegisterPropertiesDependencies("TotalPrice", new List<string> { "Price", "Quantity"});


У этого кода есть несколько недостатков: во-первых, я бы не советовал зашивать имена свойств в строки, лучше доставать их из лямбд, а во-вторых, этот код не сработает, если вычисляемой свойство имеет более сложный вид, например: TotalCost = o.OrderProperties.Orders.Sum(o => o.Price * o.Quantity).

Код OrderProperties и ViewModel. Тут всё очевидно, можно не смотреть
public class OrderProperties : BaseViewModel
    {
        private ObservableCollection<Order> _orders = new ObservableCollection<Order>();

        public ObservableCollection<Order> Orders
        {
            get { return _orders; }
            set
            {
                if (_orders == value)
                    return;
                _orders = value;
                RaisePropertyChanged("Orders");
            }
        }
    }

    public class TestViewModel : BaseViewModel
    {
        public double Summa {get { return OrderProperties.Orders.Sum(o => o.Price*o.Quantity); }}

        public OrderProperties OrderProperties
        {
            get { return _orderProperties; }
            set
            {
                if (_orderProperties == value)
                    return;
                _orderProperties = value;
                RaisePropertyChanged("OrderProperties");
            }
        }

        private OrderProperties _orderProperties;
    }




Подпишемся через события на изменения Price и Quantity каждого элемента коллекции. Но в коллекцию могут добавляться\удаляться элементы. При изменении коллекции нужно вызвать RaisePropertyChanged(«TotalPrice»). При добавлении элемента нужно подписаться на его изменении Price и Quantity. Ещё необходимо учесть, что в OrderProperties кто-то может присвоить новую коллекцию, или в ViewModel новый OrderProperties.

Получился вот такой код:

        public void RegisterElementPropertyDependencies(string propertyName, object element, ICollection<string> destinationPropertyNames, Action actionOnChanged = null)
        {
            if (element == null)
                return;

            if (actionOnChanged != null)
                actionOnChanged();

            if (element is INotifyPropertyChanged == false)
                throw new Exception(string.Format("Невозможно отслеживать изменения при биндинге в {0}, т.к. он не реализует INotifyPropertyChanged", element.GetType()));

            ((INotifyPropertyChanged)element).PropertyChanged += (o, eventArgs) =>
            {
                if (destinationPropertyNames.Contains(eventArgs.PropertyName))
                {
                    RaisePropertyChanged(propertyName);

                    if (actionOnChanged != null)
                        actionOnChanged();
                }
            };
        }

        public void RegisterCollectionPropertyDependencies<T>(string propertyName, ObservableCollection<T> collection, ICollection<string> destinationPropertyNames, Action actionOnChanged = null)
        {
            if (collection == null)
                return;

            if (actionOnChanged != null)
                actionOnChanged();

            foreach (var element in collection)
            {
                RegisterElementPropertyDependencies(propertyName, element, destinationPropertyNames);
            }

            collection.CollectionChanged += (sender, args) =>
            {
                RaisePropertyChanged(propertyName);

                if (args.NewItems != null)
                {
                    foreach (var addedItem in args.NewItems)
                    {
                        RegisterElementPropertyDependencies(propertyName, addedItem, destinationPropertyNames, actionOnChanged);
                    }
                }
            };
        }


В данном случае, для OrderProperties.Orders.Sum(o => o.Price*o.Quantity) его нужно использовать вот так:

RegisterElementPropertyDependencies("Summa", this, new[] {"OrderProperties"},
                () => RegisterElementPropertyDependencies("Summa", OrderProperties, new[] {"Orders"},
                () => RegisterCollectionPropertyDependencies("Summa", OrderProperties.Orders, new[] { "Price", "Quantity" })));


Протестировал этот код в разных ситуациях: менял Quantity у элементов, создавал новые Orders и OrderProperties, сначала менял Orders а потом Quantity и т.п., код отработал корректно.

P.S. Кстати, рекомендую посмотреть в сторону Observables в стиле Knockout. Там вообще не нужно указывать от чего зависит свойство, нужно просто передать алгоритм его вычисления:
fullName = new ComputedValue(() => FirstName.Value + " " + ToUpper(LastName.Value));
Библиотека проанализирует дерево выражений, увидит в нём доступ к членам FirstName и LastName, и сама проконтролирует зависимости. Исчезает риск забыть переуказать зависимости после изменения алгоритма вычисления свойства. Правда, говорят, что библиотека немного не доработана, и не отслеживает вложенные коллекции, но, если у вас вагон свободного времени, то можно открыть исходники (доступны по предыдущей ссылке) и немного поработать напильником, или написать свой велосипед-анализатор дерева выражений.

P.P.S. По поводу сборки мусора: если добавить в финализаторы элементов вывод сообщений, то можно обнаружить, что при закрытии окна все элементы собираются сборщиком мусора (несмотря на то, что ViewModel имеет ссылку на дочерний элемент, а дочерний элемент имеет ссылку на ViewModel в обработчике события). Это объясняется тем, что в WPF для устранения утечек памяти при DataBinding-е используются слабые события посредством PropertyChangedEventManager-а. Подробнее можно почитать по ссылкам: [1], [2], [3]

Комментарии (44)


  1. Bonart
    18.11.2015 13:52

    Можно воспользоваться Fody / PropertyChanged
    Из кода достаточно одного атрибута, вложенные коллекции, правда, не потянет.


    1. FiresShadow
      18.11.2015 14:17

      Да, менеджеров зависимостей довольно много. Но я бы предпочёл иметь дело с двумя простенькими методами, чем со сторонним проектом, делающим тоже самое. Во-первых, если придёт новый человек, который с этим сторонним проектом не работал, то придётся ему показывать, что да как. Ну или он сам потратит время, чтобы почитать мануалы или разобраться интуитивно. Или если в проекте есть ошибка, например он некорректно обрабатывает вложенные коллекции, то потребуется больше времени, чтобы понять, что ошибка в чёрном ящике, а не в собственном коде. А когда работаешь с двумя простенькими методами, у которых есть summary, то сразу видно 1)как это работает и 2)где ошибка.


      1. Bonart
        18.11.2015 14:51
        +2

        Сами методы не слишком простенькие (одна подписка на событие без отписки уже намекает), а код регистрации совсем не простенький.
        Объем демо и объяснений в вашем случае требуется не меньший.
        Дополнительно вплетение кода гарантирует отсутствие целого класса ошибок.
        Отлаживать его вроде как тяжелее, зато сама отладка потребуется куда реже.


        1. FiresShadow
          18.11.2015 15:19

          Сами методы не слишком простенькие, а код регистрации совсем не простенький.
          Это субъективно. Вот что объективно: в приведенном мной примере 2 небольших метода, в указанном вами проекте множество немаленьких классов. Не знаю сколько из них занимаются непосредственно управлением зависимостями, но в любом случае, раз там всё построено на изменении кода в момент компиляции, то кода наберётся немало.

          одна подписка на событие без отписки уже намекает
          Пример кода отражает основной смысл, который в него заложен (имхо, такими и должны быть примеры — краткими и конкретными). Чтобы память не текла, используйте слабые ссылки. Это, на мой взгляд, best practise при практически любом использовании событий.

          Объем демо и объяснений в вашем случае требуется не меньший
          Небольшое summary и сам код — вот лучшее объяснение. Пример использования — вот лучшее демо (вы же не будете копировать эти методы в свой проект, если не собираетесь их сразу же использовать, верно?).

          Отлаживать его вроде как тяжелее, зато сама отладка потребуется куда реже.
          На основании чего вы делаете такие выводы? Похоже на гадание на кофейной гуще — что и сколько раз потребуется отлаживать. Да, там 10 контрибьюторов, но сам проект объективно сложнее устроен. Отлаживать сторонний большой проект сложнее, чем два небольших метода — с этим согласен.


          1. Bonart
            18.11.2015 15:41
            +1

            Это субъективно. Вот что объективно: в приведенном мной примере 2 небольших метода, в указанном вами проекте множество немаленьких классов. Не знаю сколько из них занимаются непосредственно управлением зависимостями, но в любом случае, раз там всё построено на изменении кода в момент компиляции, то кода наберётся немало.

            Объем библиотечного кода имеет малое значение по сравнению с объемом кода клиентского, а здесь у Fody большое преимущество.
            Пример кода отражает основной смысл, который в него заложен (имхо, такими и должны быть примеры — краткими и конкретными). Чтобы память не текла, используйте слабые ссылки. Это, на мой взгляд, best practise при практически любом использовании событий.

            Т.е. ваши методы это только пример или, скажем, прототип? Тогда сравнивать объем кода и т.п. с уже готовой библиотекой нелогично.
            Небольшое summary и сам код — вот лучшее объяснение. Пример использования — вот лучшее демо (вы же не будете копировать эти методы в свой проект, если не собираетесь их сразу же использовать, верно?).

            Я вообще не копирую код. Или использую библиотеку или пишу свое, ознакомившись сначала с чужими аналогами.
            На основании чего вы делаете такие выводы? Похоже на гадание на кофейной гуще — что и сколько раз потребуется отлаживать. Да, там 10 контрибьюторов, но сам проект объективно сложнее устроен. Отлаживать сторонний большой проект сложнее, чем два небольших метода — с этим согласен.

            На основании опыта использования чужих библиотек в своих проектах. 99% отладки приходится на свой код, а процент своих ошибок еще выше. Предпочитаю не писать свои велосипеды, если доступны готовые приемлемого качества.
            При использовании Fody шансов сделать свою ошибку заметно меньше, что мне и требуется.


            1. FiresShadow
              18.11.2015 16:39

              Тут нужно смотреть на известность и надёжность стороннего проекта, а также на развивающее его сообщество. Я про конкретно этот проект впервые слышу, так что не могу дать свою оценку. Если вы уверены, что с проектом проблем не возникнет и вам не придётся объяснять новому человеку на проекте, как им пользоваться, то бога ради, используйте. Если такой уверенности нету, то можете использовать мои два небольших метода. Поскольку методы небольшие и в них несложно разобраться, то 1)новый человек на проекте наверняка разберётся, увидев пример и исходный код 2)в случае чего можно будет легко отладить код и найти проблему — как в клиентском коде, так и в коде этих методов.
              В любом случае, теперь вы знаете ещё один способ реализации binding-а вычислимых свойств в WPF.


              1. halkar
                21.11.2015 23:53
                +1

                1. Этот проект существует уже лет пять минимум. То, что вы про него не слышали говорит о вашей неосведомленности, а не его сырости.
                2. В отличие от вашей реализации не добавляет новых библиотек к вашей сборке. Все изменения делаются в процессе сборки. Тем самым как увеличивается производительность, так и уменьшается размер приложения.


                1. FiresShadow
                  22.11.2015 10:42

                  Никто не говорил про сырость, речь шла про надёжность и известность. Кстати, первый коммит был сделан примерно 3 года назад, 7 июля 2012.

                  Давайте представим, какие сложности могут возникнуть при использовании этой библиотеки.
                  1)Имена свойств передаются в атрибуты через строки, что вызывает сложности при навигации и переименовании: в параметры атрибутов можно передавать только константы, и код, вычисляющий имя свойства, задействовать не удастся.
                  2)По слова пользователей, Fody не поддерживает обработку вложенных коллекций. Если возникнет такая необходимость, то можно: а)написать разработчикам Fody и ждать у моря погоды (но станет ли ждать заказчик?) б)использовать другой менеджер зависимостей в)использовать код из этой статьи.

                  как увеличивается производительность, так и уменьшается размер приложения

                  Ну давайте посмотрим, насколько отличается код. В случае Fody мы непосредственно вызываем два RaisePropertyChanged, а в случае кода из статьи — вызываем один RaisePropertyChanged, а второй вызывается как подписчик. Насколько команд процессора во втором случае больше? На одного подписчика больше, значит на одну итерацию цикла больше, примерно 7 команд: инкрементация счётчика цикла, проверка на выход из цикла, переход на нужную инструкцию, 4 операции вызова метода (вычисление адреса метода по объекту и смещению, переход, копирование регистров процессора 2 раза). Поскольку это происходит не в режиме ядра, то возможно ОС выполнит какие-то действия. Грубо говоря, пусть будет с запасом 20 операций. Сколько там у вас в ViewModel обычно вычислимых свойств? А чего мелочится, пусть будет 20. Большая такая ViewModel. И допустим отображаете вы не одно поле, а целую таблицу из 100 элементов, на большом таком мониторе. Итого 40000 операций. Допустим пользователь пользуется программой на смартфоне с процессором в 1 ГГц. Итого пользователь будет ждать аж на целых 0,00004 секунды больше! Бедный пользователь. Мне почти стыдно. Уже бегу усложнять процесс сборки и отладки солюшена, чтобы страдал я и мои коллеги, а не пользователь. Шучу. Коллег жалко. И кстати, я не против Fody, я против экономии на спичках.


                  1. halkar
                    22.11.2015 10:48
                    +1

                    1. Fody/PropertyChanged это продолжение проекта PropertyChangedWeaver, про который вы конечно не слышали.
                    2.Действительно, чего экономить то? Процессор чай казенный. Особенно экономия бессмысленна на мобильных девайсах. Никто же далеко от розетки не отходит ;)


                  1. Bonart
                    22.11.2015 14:54

                    Вы всерьез считаете что по критерию «известность-надежность» ваше решение превосходит PropertyChanged.Fody?
                    Неужели вы думаете, что кто-нибудь в здтавом уме и твердой памяти будет копипастить код из интернета, написанный за полтора часа?
                    Вложенные коллекции? Как-то вот ни разу нужды не возникало. А когда возникало — коллекции были виртуальные.


  1. imanushin
    18.11.2015 14:58
    +1

    Можно еще использовать RX для подобных вещей.
    См, например, stackoverflow.

    При этом вы получаете:
    1. Более простой код подписки/отписки (например, насколько легко отписаться от всех event'ов сейчас? Модель может это сделать сама, или только ждать, пока UI это произведет? А а RX от подписки может отказаться и слушатель, и publisher)
    2. Возможность разделения канала и способа аггрегации: т.е. по одному пути идут изменения (+ фильтры и т.д.), а в совсем других методах вы описываете, как аггрегировать.
    3. Есть уже некоторые готовые аггрегации, такие как сумма, максимум и пр.


  1. ademchenko
    19.11.2015 01:13
    +2

    Вы меня, конечно, извините, но код пока не очень даже на прототип тянет.
    Мешает серия багов:
    1. Нет отписок при изменениях в цепочке — внутреннего объекта, коллекции. Результат этого понятен: изменения в объектах, которые уже нашей цепочки не принадлежат, будут инициировать уведомления об изменении цепочки, что неверно.
    2. В коллекции обрабатывается только добавление элемента. А как же остальные 4 действия — Remove, Move, Replace и Reset?

    Если будете фиксить эти баги, учтите, пожалуйста, следующие моменты (в частности, потому, что интересно сколько еще времени займет эта работа) по п.2:
    1. Проблема обработки Remove, Move, Replace и Reset не только проблема утечек. В некоторых случаях дело и в корректности вычислений тоже. Например, в случае Replace не совершается переподписка на новый элемент, а, значит, при изменениях его свойств не будет рейзится событие об изменении зависимого свойства.
    2. Не забудьте реализовать отписку от уже ненужных элементов коллекции в случае Reset (когда OldValues = null).


    1. ademchenko
      19.11.2015 01:17
      +2

      Что касается слабых ссылок, то, это, по моему, вообще из пушки по воробьям. Чтобы использовать слабые ссылки, нужна еще реализация «слабых подписок», где этот код возьмем? Тоже сами напишем? Или библиотекой сторонней воспользуемся? :-) Если сами, то чего уж там, давайте и этот код в студию :-).


      1. FiresShadow
        19.11.2015 05:34

        Что касается слабых ссылок, то, это, по моему, вообще из пушки по воробьям.

        Вы знаете более простой и надёжный способ? В чём он заключается?


        1. Bonart
          19.11.2015 14:49

          Ну лично я предпочитаю своевременную отписку.
          С Rx можно обрезать пуповину еще и со стороны публикатора.


          1. FiresShadow
            20.11.2015 06:48

            предпочитаю своевременную отписку
            Т.е. вы предлагаете добавить метод Dispose, в котором будет происходить отписка, и возложить на плечи программиста\архитектуры дополнительную ответственность?


            1. Bonart
              20.11.2015 10:22
              +2

              Нет никакой дополнительной ответственности, отвечать за утечки приходится всегда и везде.
              В случае событий надо или гарантировать отписку, или доказать одинаковое время жизни публикатора и подписчика, или доказать слабость подписки. В вашем коде я пока ничего из этого не вижу.


              1. FiresShadow
                20.11.2015 11:14

                Вы во всех ветках будете писать, что не видите в коде слабых событий? Ответил вам в другой ветке.


    1. FiresShadow
      19.11.2015 05:33
      -1

      Результат этого понятен: изменения в объектах, которые уже нашей цепочки не принадлежат, будут инициировать уведомления об изменении цепочки, что неверно.


      Можно сделать отписку при удалении элемента из коллекции, а можно и не делать. Сам по себе случай, когда элемент изымается из отображения, и после этого с ним производятся какие-то действия, довольно редок. Обычно, если элемент перестаёт отображаться, значит он пользователя больше не интересует, и никаких действий с ним не планируется. Но что будет, если такой случай всё же произойдёт? В View перерисуется элемент, зависящий от вычислимого свойства в ViewModel, который мог не перерисовываться. Опять таки, а много у вас вычислимых свойств в проекте, и насколько они сложные? Не вижу необходимости делать преждевременную оптимизацию, усложняя код, чтобы в каких-то очень экзотических ситуациях он отрабатывал на несколько миллисекунд быстрее.
      Хотя если у вас в проекте такие случаи встречаются, то можете и задуматься об оптимизации. Хотя раз в данном случае оптимизация будет заключаться в каких-то дополнительных действиях, то в одних местах код будет работать быстрее, а в других — медленнее. Тут лучше на конкретном проекте смотреть, сколько микросекунд мы выиграем, сколько потеряем, и стоит ли усложнение кода выигранного быстродействия.


      1. Bonart
        19.11.2015 14:51
        +2

        Тут лучше на конкретном проекте смотреть, сколько микросекунд мы выиграем, сколько потеряем, и стоит ли усложнение кода выигранного быстродействия.

        Проблема не в микросекундах, а утечке ресурсов: подписанный элемент и все его зависимости не могут быть утилизированы сборщиком мусора.


        1. FiresShadow
          20.11.2015 06:53

          Я же лично вам недавно говорил про утечки памяти и слабые ссылки.


          1. Bonart
            20.11.2015 10:16

            Пока никаких слабых ссылок в вашем коде не наблюдается. Или вы имеете в виду, что ваш код требует реализацию слабой подписки на стороне публикатора?


            1. FiresShadow
              20.11.2015 11:09

              Описание слабых ссылок
              Описание слабых событий
              Нет, слабых событий на данный момент в коде нет. Почему нет, я вам говорил ранее — чтобы не перегружать пример кода:

              Пример кода отражает основной смысл, который в него заложен (имхо, такими и должны быть примеры — краткими и конкретными). Чтобы память не текла, используйте слабые ссылки. Это, на мой взгляд, best practise при практически любом использовании событий.

              Подумаю, может быть, добавлю в статью код примера слабых событий. Хотя не вижу особого смысла. В сети наверняка есть примеры, как это делается, я тут ничего нового не скажу.


              1. FiresShadow
                23.11.2015 18:18

                Добавил замечание по сборке мусора в конец статьи


      1. Bonart
        22.11.2015 16:09
        -1

        Есть еще один недостаток у отсутствия отписки. Вы неявно принимаете исключение элемента из коллекции за его уничтожение, а это по факту может быть совсем не так. Элемент может быть добавлен в другую коллекцию, возвращен в пул и задействован повторно, использован самостоятельно и т.д. И в этом случае слабые ссылки не помогут никак.


        1. FiresShadow
          22.11.2015 16:42

          И в этом случае слабые ссылки не помогут никак.

          Не помогут в чём? Утечки памяти возникнут? Если объект не уничтожен, сборщиком мусора его собирать не надо.


          1. Bonart
            22.11.2015 16:51

            От утечек слабые ссылки спасут, но ничего не сделают с паразитными пересчетами, которые дадут сложность N*M (N — число элементов коллекции, M — число бывших элементов коллекции) буквально на пустом месте.


            1. FiresShadow
              22.11.2015 16:57
              -1

              По поводу оптимизации я уже высказывался в этом комментарии.


              1. Bonart
                22.11.2015 17:09

                Оптимизация здесь вообще ни при чем.
                Например, использование быстрой сортировки вместо пузырька — оптимизация.
                А вот написание кода, который работает изначально работает медленнее пузырька — просто ошибка.
                Нет никакого смысла допускать массовые заведомо паразитные пересчеты даже в прототипе.


                1. FiresShadow
                  22.11.2015 17:15

                  Оптимизация — это модификация системы для улучшения её эффективности.


                1. FiresShadow
                  22.11.2015 17:19

                  использование быстрой сортировки вместо пузырька — оптимизация
                  Кстати, быстрая сортировка не всегда работает быстрее пузырьковой. Аккуратнее в словах, на портале могут быть неофиты.


    1. FiresShadow
      19.11.2015 06:00
      -1

      В коллекции обрабатывается только добавление элемента. А как же остальные 4 действия — Remove, Move, Replace и Reset?
      Неправда, посмотрите код ещё раз.


      1. Bonart
        20.11.2015 10:18

        Можно указать конкретное место, где обрабатывается Remove и Reset?


        1. FiresShadow
          20.11.2015 11:12
          -1

          Подчеркнул это место.

          collection.CollectionChanged += (sender, args) =>
          {
          RaisePropertyChanged(propertyName);

          if (args.Action == NotifyCollectionChangedAction.Add || args.Action == NotifyCollectionChangedAction.Replace)
          {
          foreach (var addedItem in args.NewItems)
          {
          RegisterElementPropertyDependencies(propertyName, addedItem, destinationPropertyNames, actionOnChanged);
          }
          }
          };


          1. Bonart
            20.11.2015 12:22

            То, что вы не делаете отписки при Remove, Replace и Reset — это утечки.
            А вот то, что вы при Reset не подписываетесь на новое содержимое коллекции — это еще и проблемы непосредственно с пересчетом.
            Конечно, типовая ObservableCollection делает Reset только при Clear, только любой шаг вправо или влево (пример: www.interact-sw.co.uk/iangblog/2013/02/22/batch-inotifycollectionchanged) сделает ваш код неработоспособным.


            1. FiresShadow
              20.11.2015 13:14
              -1

              это утечки

              Если честно, уже не смешно. Я потерял счёт, сколько раз я вам говорил про утечки памяти и слабые события. Ваш троллинг слишком толст.

              Конечно, типовая ObservableCollection делает Reset только при Clear, только любой шаг вправо или влево сделает ваш код неработоспособным.
              Ага, а давайте рассмотрим ещё пример: типовой List<> добавляет во время Add() ровно один элемент, а если сделать в наследнике чтобы Add добавляло сам элемент и его копию, то какой-нибудь код, рассчитывающий что после Add() Count увеличивается ровно на единицу, станет неработоспособным. Можно сделать почти любой код неработоспособным, пренебрегая элементарными правилами наследования. Почитайте про принцип Лисков.


              1. Bonart
                20.11.2015 13:28
                +1

                Можно сделать почти любой код неработоспособным, пренебрегая элементарными правилами наследования. Почитайте про принцип Лисков.

                Видите ли, публичный контракт ObservableCollection не гарантирует, что Reset вызывается только при Clear
                Если вы делаете на эту ставку — это нарушение инкапсуляции.
                Reset означает очистку только начиная с .NET 4.5, что в свою очередь ломает обратную совместимость.


                1. FiresShadow
                  20.11.2015 13:42
                  +1

                  Не знал, спасибо. Ну тогда придётся заменить

                  if (args.Action == NotifyCollectionChangedAction.Add || args.Action == NotifyCollectionChangedAction.Replace)

                  на

                  if (args.NewItems != null && args.NewItems.Any()).

                  Сделал из этого некоторые выводы. Благодарствую, удалось извлечь пользу из диалога с вами :)


                  1. FiresShadow
                    22.11.2015 10:48

                    Видите ли, публичный контракт ObservableCollection не гарантирует, что Reset вызывается только при Clear
                    Немного вас подправлю, а то вдруг кто-то прочитает и подумает, что правда не требует. На самом деле требует, но в старых версиях .NET Framework (до 4.5) не требовал.


                    1. Bonart
                      22.11.2015 14:45

                      Вы точно прочитали мой комментарий полностью? Третье предложение как раз про 4.5 с прямой ссылкой на MSDN. Вот только говорить про 4.0 и ряд других версий в прошедшем времени не стоит.


            1. FiresShadow
              24.11.2015 16:38

              Конечно, типовая ObservableCollection делает Reset только при Clear, только любой шаг вправо или влево сделает ваш код неработоспособным.

              Написал статью на тему, почему нужно по-возможности избегать таких шагов влево.


      1. ademchenko
        20.11.2015 11:21

        RaisePropertyChanged — это не обработка.
        В частности:

        Например, в случае Replace не совершается переподписка на новый элемент, а, значит, при изменениях его свойств не будет рейзится событие об изменении зависимого свойства.


        1. FiresShadow
          20.11.2015 11:26

          Подчеркнул нужное место…

          collection.CollectionChanged += (sender, args) =>
          {
          RaisePropertyChanged(propertyName);

          if (args.Action == NotifyCollectionChangedAction.Add || args.Action == NotifyCollectionChangedAction.Replace)
          {
          foreach (var addedItem in args.NewItems)
          {
          RegisterElementPropertyDependencies(propertyName, addedItem, destinationPropertyNames, actionOnChanged);
          }
          }
          };


  1. onikiychuka
    20.11.2015 11:26

    Хмм можно разбирать вычисления из лямбд и по ним генерировать подписку.

    interface IDisposableSupplier<T> : IDisposable {
      T Supply();
    }
    class ViewModel : BaseViewModel {
      public int A {get; set;}
      public int B {get; set;}
      private readonly IDisposableSupplier<int> cSupplier;
      public int C {get{ return cSupplier.Supply(); }}
      public ViewModel(){
           cSupplier = this.CreateSubcription(() => A + B);
      }
    }
    static class ProppertyChangedHelper {
       public static<T> IDisposableSupplier<T> CreateSubcription(this BaseViewModel vm, Expression<Func<T>> callable) {
      //AST parsing and subscriptions here.
     }
    }