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).
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)
imanushin
18.11.2015 14:58+1Можно еще использовать RX для подобных вещей.
См, например, stackoverflow.
При этом вы получаете:
1. Более простой код подписки/отписки (например, насколько легко отписаться от всех event'ов сейчас? Модель может это сделать сама, или только ждать, пока UI это произведет? А а RX от подписки может отказаться и слушатель, и publisher)
2. Возможность разделения канала и способа аггрегации: т.е. по одному пути идут изменения (+ фильтры и т.д.), а в совсем других методах вы описываете, как аггрегировать.
3. Есть уже некоторые готовые аггрегации, такие как сумма, максимум и пр.
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).ademchenko
19.11.2015 01:17+2Что касается слабых ссылок, то, это, по моему, вообще из пушки по воробьям. Чтобы использовать слабые ссылки, нужна еще реализация «слабых подписок», где этот код возьмем? Тоже сами напишем? Или библиотекой сторонней воспользуемся? :-) Если сами, то чего уж там, давайте и этот код в студию :-).
FiresShadow
19.11.2015 05:34Что касается слабых ссылок, то, это, по моему, вообще из пушки по воробьям.
Вы знаете более простой и надёжный способ? В чём он заключается?Bonart
19.11.2015 14:49Ну лично я предпочитаю своевременную отписку.
С Rx можно обрезать пуповину еще и со стороны публикатора.FiresShadow
20.11.2015 06:48предпочитаю своевременную отписку
Т.е. вы предлагаете добавить метод Dispose, в котором будет происходить отписка, и возложить на плечи программиста\архитектуры дополнительную ответственность?Bonart
20.11.2015 10:22+2Нет никакой дополнительной ответственности, отвечать за утечки приходится всегда и везде.
В случае событий надо или гарантировать отписку, или доказать одинаковое время жизни публикатора и подписчика, или доказать слабость подписки. В вашем коде я пока ничего из этого не вижу.FiresShadow
20.11.2015 11:14Вы во всех ветках будете писать, что не видите в коде слабых событий? Ответил вам в другой ветке.
FiresShadow
19.11.2015 05:33-1Результат этого понятен: изменения в объектах, которые уже нашей цепочки не принадлежат, будут инициировать уведомления об изменении цепочки, что неверно.
Можно сделать отписку при удалении элемента из коллекции, а можно и не делать. Сам по себе случай, когда элемент изымается из отображения, и после этого с ним производятся какие-то действия, довольно редок. Обычно, если элемент перестаёт отображаться, значит он пользователя больше не интересует, и никаких действий с ним не планируется. Но что будет, если такой случай всё же произойдёт? В View перерисуется элемент, зависящий от вычислимого свойства в ViewModel, который мог не перерисовываться. Опять таки, а много у вас вычислимых свойств в проекте, и насколько они сложные? Не вижу необходимости делать преждевременную оптимизацию, усложняя код, чтобы в каких-то очень экзотических ситуациях он отрабатывал на несколько миллисекунд быстрее.
Хотя если у вас в проекте такие случаи встречаются, то можете и задуматься об оптимизации. Хотя раз в данном случае оптимизация будет заключаться в каких-то дополнительных действиях, то в одних местах код будет работать быстрее, а в других — медленнее. Тут лучше на конкретном проекте смотреть, сколько микросекунд мы выиграем, сколько потеряем, и стоит ли усложнение кода выигранного быстродействия.Bonart
19.11.2015 14:51+2Тут лучше на конкретном проекте смотреть, сколько микросекунд мы выиграем, сколько потеряем, и стоит ли усложнение кода выигранного быстродействия.
Проблема не в микросекундах, а утечке ресурсов: подписанный элемент и все его зависимости не могут быть утилизированы сборщиком мусора.FiresShadow
20.11.2015 06:53Я же лично вам недавно говорил про утечки памяти и слабые ссылки.
Bonart
20.11.2015 10:16Пока никаких слабых ссылок в вашем коде не наблюдается. Или вы имеете в виду, что ваш код требует реализацию слабой подписки на стороне публикатора?
FiresShadow
20.11.2015 11:09Описание слабых ссылок
Описание слабых событий
Нет, слабых событий на данный момент в коде нет. Почему нет, я вам говорил ранее — чтобы не перегружать пример кода:
Пример кода отражает основной смысл, который в него заложен (имхо, такими и должны быть примеры — краткими и конкретными). Чтобы память не текла, используйте слабые ссылки. Это, на мой взгляд, best practise при практически любом использовании событий.
Подумаю, может быть, добавлю в статью код примера слабых событий. Хотя не вижу особого смысла. В сети наверняка есть примеры, как это делается, я тут ничего нового не скажу.
Bonart
22.11.2015 16:09-1Есть еще один недостаток у отсутствия отписки. Вы неявно принимаете исключение элемента из коллекции за его уничтожение, а это по факту может быть совсем не так. Элемент может быть добавлен в другую коллекцию, возвращен в пул и задействован повторно, использован самостоятельно и т.д. И в этом случае слабые ссылки не помогут никак.
FiresShadow
22.11.2015 16:42И в этом случае слабые ссылки не помогут никак.
Не помогут в чём? Утечки памяти возникнут? Если объект не уничтожен, сборщиком мусора его собирать не надо.Bonart
22.11.2015 16:51От утечек слабые ссылки спасут, но ничего не сделают с паразитными пересчетами, которые дадут сложность N*M (N — число элементов коллекции, M — число бывших элементов коллекции) буквально на пустом месте.
FiresShadow
22.11.2015 16:57-1По поводу оптимизации я уже высказывался в этом комментарии.
Bonart
22.11.2015 17:09Оптимизация здесь вообще ни при чем.
Например, использование быстрой сортировки вместо пузырька — оптимизация.
А вот написание кода, который работает изначально работает медленнее пузырька — просто ошибка.
Нет никакого смысла допускать массовые заведомо паразитные пересчеты даже в прототипе.FiresShadow
22.11.2015 17:19использование быстрой сортировки вместо пузырька — оптимизация
Кстати, быстрая сортировка не всегда работает быстрее пузырьковой. Аккуратнее в словах, на портале могут быть неофиты.
FiresShadow
19.11.2015 06:00-1В коллекции обрабатывается только добавление элемента. А как же остальные 4 действия — Remove, Move, Replace и Reset?
Неправда, посмотрите код ещё раз.Bonart
20.11.2015 10:18Можно указать конкретное место, где обрабатывается Remove и Reset?
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);
}
}
};Bonart
20.11.2015 12:22То, что вы не делаете отписки при Remove, Replace и Reset — это утечки.
А вот то, что вы при Reset не подписываетесь на новое содержимое коллекции — это еще и проблемы непосредственно с пересчетом.
Конечно, типовая ObservableCollection делает Reset только при Clear, только любой шаг вправо или влево (пример: www.interact-sw.co.uk/iangblog/2013/02/22/batch-inotifycollectionchanged) сделает ваш код неработоспособным.FiresShadow
20.11.2015 13:14-1это утечки
Если честно, уже не смешно. Я потерял счёт, сколько раз я вам говорил про утечки памяти и слабые события. Ваш троллинг слишком толст.
Конечно, типовая ObservableCollection делает Reset только при Clear, только любой шаг вправо или влево сделает ваш код неработоспособным.
Ага, а давайте рассмотрим ещё пример: типовой List<> добавляет во время Add() ровно один элемент, а если сделать в наследнике чтобы Add добавляло сам элемент и его копию, то какой-нибудь код, рассчитывающий что после Add() Count увеличивается ровно на единицу, станет неработоспособным. Можно сделать почти любой код неработоспособным, пренебрегая элементарными правилами наследования. Почитайте про принцип Лисков.Bonart
20.11.2015 13:28+1Можно сделать почти любой код неработоспособным, пренебрегая элементарными правилами наследования. Почитайте про принцип Лисков.
Видите ли, публичный контракт ObservableCollection не гарантирует, что Reset вызывается только при Clear
Если вы делаете на эту ставку — это нарушение инкапсуляции.
Reset означает очистку только начиная с .NET 4.5, что в свою очередь ломает обратную совместимость.
FiresShadow
20.11.2015 13:42+1Не знал, спасибо. Ну тогда придётся заменить
if (args.Action == NotifyCollectionChangedAction.Add || args.Action == NotifyCollectionChangedAction.Replace)
на
if (args.NewItems != null && args.NewItems.Any()).
Сделал из этого некоторые выводы. Благодарствую, удалось извлечь пользу из диалога с вами :)FiresShadow
22.11.2015 10:48Видите ли, публичный контракт ObservableCollection не гарантирует, что Reset вызывается только при Clear
Немного вас подправлю, а то вдруг кто-то прочитает и подумает, что правда не требует. На самом деле требует, но в старых версиях .NET Framework (до 4.5) не требовал.Bonart
22.11.2015 14:45Вы точно прочитали мой комментарий полностью? Третье предложение как раз про 4.5 с прямой ссылкой на MSDN. Вот только говорить про 4.0 и ряд других версий в прошедшем времени не стоит.
FiresShadow
24.11.2015 16:38Конечно, типовая ObservableCollection делает Reset только при Clear, только любой шаг вправо или влево сделает ваш код неработоспособным.
Написал статью на тему, почему нужно по-возможности избегать таких шагов влево.
ademchenko
20.11.2015 11:21RaisePropertyChanged — это не обработка.
В частности:
Например, в случае Replace не совершается переподписка на новый элемент, а, значит, при изменениях его свойств не будет рейзится событие об изменении зависимого свойства.
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);
}
}
};
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. } }
Bonart
Можно воспользоваться Fody / PropertyChanged
Из кода достаточно одного атрибута, вложенные коллекции, правда, не потянет.
FiresShadow
Да, менеджеров зависимостей довольно много. Но я бы предпочёл иметь дело с двумя простенькими методами, чем со сторонним проектом, делающим тоже самое. Во-первых, если придёт новый человек, который с этим сторонним проектом не работал, то придётся ему показывать, что да как. Ну или он сам потратит время, чтобы почитать мануалы или разобраться интуитивно. Или если в проекте есть ошибка, например он некорректно обрабатывает вложенные коллекции, то потребуется больше времени, чтобы понять, что ошибка в чёрном ящике, а не в собственном коде. А когда работаешь с двумя простенькими методами, у которых есть summary, то сразу видно 1)как это работает и 2)где ошибка.
Bonart
Сами методы не слишком простенькие (одна подписка на событие без отписки уже намекает), а код регистрации совсем не простенький.
Объем демо и объяснений в вашем случае требуется не меньший.
Дополнительно вплетение кода гарантирует отсутствие целого класса ошибок.
Отлаживать его вроде как тяжелее, зато сама отладка потребуется куда реже.
FiresShadow
Пример кода отражает основной смысл, который в него заложен (имхо, такими и должны быть примеры — краткими и конкретными). Чтобы память не текла, используйте слабые ссылки. Это, на мой взгляд, best practise при практически любом использовании событий.
Небольшое summary и сам код — вот лучшее объяснение. Пример использования — вот лучшее демо (вы же не будете копировать эти методы в свой проект, если не собираетесь их сразу же использовать, верно?).
На основании чего вы делаете такие выводы? Похоже на гадание на кофейной гуще — что и сколько раз потребуется отлаживать. Да, там 10 контрибьюторов, но сам проект объективно сложнее устроен. Отлаживать сторонний большой проект сложнее, чем два небольших метода — с этим согласен.
Bonart
Объем библиотечного кода имеет малое значение по сравнению с объемом кода клиентского, а здесь у Fody большое преимущество.
Т.е. ваши методы это только пример или, скажем, прототип? Тогда сравнивать объем кода и т.п. с уже готовой библиотекой нелогично.
Я вообще не копирую код. Или использую библиотеку или пишу свое, ознакомившись сначала с чужими аналогами.
На основании опыта использования чужих библиотек в своих проектах. 99% отладки приходится на свой код, а процент своих ошибок еще выше. Предпочитаю не писать свои велосипеды, если доступны готовые приемлемого качества.
При использовании Fody шансов сделать свою ошибку заметно меньше, что мне и требуется.
FiresShadow
Тут нужно смотреть на известность и надёжность стороннего проекта, а также на развивающее его сообщество. Я про конкретно этот проект впервые слышу, так что не могу дать свою оценку. Если вы уверены, что с проектом проблем не возникнет и вам не придётся объяснять новому человеку на проекте, как им пользоваться, то бога ради, используйте. Если такой уверенности нету, то можете использовать мои два небольших метода. Поскольку методы небольшие и в них несложно разобраться, то 1)новый человек на проекте наверняка разберётся, увидев пример и исходный код 2)в случае чего можно будет легко отладить код и найти проблему — как в клиентском коде, так и в коде этих методов.
В любом случае, теперь вы знаете ещё один способ реализации binding-а вычислимых свойств в WPF.
halkar
1. Этот проект существует уже лет пять минимум. То, что вы про него не слышали говорит о вашей неосведомленности, а не его сырости.
2. В отличие от вашей реализации не добавляет новых библиотек к вашей сборке. Все изменения делаются в процессе сборки. Тем самым как увеличивается производительность, так и уменьшается размер приложения.
FiresShadow
Никто не говорил про сырость, речь шла про надёжность и известность. Кстати, первый коммит был сделан примерно 3 года назад, 7 июля 2012.
Давайте представим, какие сложности могут возникнуть при использовании этой библиотеки.
1)Имена свойств передаются в атрибуты через строки, что вызывает сложности при навигации и переименовании: в параметры атрибутов можно передавать только константы, и код, вычисляющий имя свойства, задействовать не удастся.
2)По слова пользователей, Fody не поддерживает обработку вложенных коллекций. Если возникнет такая необходимость, то можно: а)написать разработчикам Fody и ждать у моря погоды (но станет ли ждать заказчик?) б)использовать другой менеджер зависимостей в)использовать код из этой статьи.
Ну давайте посмотрим, насколько отличается код. В случае Fody мы непосредственно вызываем два RaisePropertyChanged, а в случае кода из статьи — вызываем один RaisePropertyChanged, а второй вызывается как подписчик. Насколько команд процессора во втором случае больше? На одного подписчика больше, значит на одну итерацию цикла больше, примерно 7 команд: инкрементация счётчика цикла, проверка на выход из цикла, переход на нужную инструкцию, 4 операции вызова метода (вычисление адреса метода по объекту и смещению, переход, копирование регистров процессора 2 раза). Поскольку это происходит не в режиме ядра, то возможно ОС выполнит какие-то действия. Грубо говоря, пусть будет с запасом 20 операций. Сколько там у вас в ViewModel обычно вычислимых свойств? А чего мелочится, пусть будет 20. Большая такая ViewModel. И допустим отображаете вы не одно поле, а целую таблицу из 100 элементов, на большом таком мониторе. Итого 40000 операций. Допустим пользователь пользуется программой на смартфоне с процессором в 1 ГГц. Итого пользователь будет ждать аж на целых 0,00004 секунды больше! Бедный пользователь. Мне почти стыдно. Уже бегу усложнять процесс сборки и отладки солюшена, чтобы страдал я и мои коллеги, а не пользователь. Шучу. Коллег жалко. И кстати, я не против Fody, я против экономии на спичках.
halkar
1. Fody/PropertyChanged это продолжение проекта PropertyChangedWeaver, про который вы конечно не слышали.
2.Действительно, чего экономить то? Процессор чай казенный. Особенно экономия бессмысленна на мобильных девайсах. Никто же далеко от розетки не отходит ;)
Bonart
Вы всерьез считаете что по критерию «известность-надежность» ваше решение превосходит PropertyChanged.Fody?
Неужели вы думаете, что кто-нибудь в здтавом уме и твердой памяти будет копипастить код из интернета, написанный за полтора часа?
Вложенные коллекции? Как-то вот ни разу нужды не возникало. А когда возникало — коллекции были виртуальные.