Компактная запись тривиального случая
Рассмотренный в самой первой статье пример решения для одного свойства может быть записан компактнее:public IDisposable Observe(Page page, Action<Rectangle> invalidateHandler)
{
var pageSizeHandler = new PropertyChanged((sender, args) =>
{
if (args.PropertyName == "Size")
{
invalidateHandler(new Rectangle(Point.Empty, (Size)args.OldValue));
invalidateHandler(new Rectangle(Point.Empty, (Size)args.NewValue));
}
});
page.PropertyChanged += pageSizeHandler;
return new DisposeDelegate(() => page.PropertyChanged -= pageSizeHandler);
}где DisposeDelegate - вспомогательный класс, реализующий IDisposable, цель которого состоит в выполнении указанного действия при удалении объекта вызовом Dispose.
Идея и смысл состоят в том, что если где-то мы подписались, то в другом месте надо будет отписаться. Полагаться на сборщик мусора не стоит, потому что нас интересует детерминированное поведение кода.
Обобщение для произвольного свойства и коллекции
Приведенный выше пример можно обобщить для произвольного класса, реализующего контракт INotifyPropertyChanged, а также позаботиться о будущем рефакторинге и устранить использование имени свойства:public static IDisposable Observe<T, TI>(T obj, Expression<Func<T, TI>> getterExpr, Action<TI> action)
where T : ObjectRoot
{
var memberAssignment = (MemberExpression)getterExpr.Body;
var propertyName = memberAssignment.Member.Name;
var getter = getterExpr.Compile();
var lastValue = getter(obj);
var handler = new PropertyChanged((sender, args) =>
{
if (args.PropertyName == propertyName)
{
action(lastValue);
action(lastValue = (TI)args.NewValue);
}
});
obj.PropertyChanged += handler;
return new DisposeDelegate(() => obj.PropertyChanged -= handler);
}В случае коллекции нас интересует не само изменение коллекции, а возможность подписаться/отписаться на изменения элементов коллекции. Код очевиден:
public static IDisposable ObserveList<TI>(IObservableList<TI> list, Func<TI, IDisposable> subscribeItem)
{
var subscribed = list.Select(subscribeItem).ToList();
var handler = new CollectionChanged((sender, args) =>
{
switch (args.Action)
{
case ActionType.Clear:
subscribed.All(d => { d.Dispose(); return true; });
subscribed.Clear();
break;
case ActionType.Insert:
subscribed.Insert(args.Index, subscribeItem(list[args.Index]));
break;
case ActionType.Remove:
subscribed[args.Index].Dispose();
subscribed.RemoveAt(args.Index);
break;
case ActionType.Set:
subscribed[args.Index].Dispose();
subscribed[args.Index] = subscribeItem(list[args.Index]);
break;
}
});
list.Changed += handler;
return new DisposeDelegate(
() => list.Changed -= handler,
() => subscribed.All(d => { d.Dispose(); return true; })
);
}Переменная subscribed хранит список "подписок", код внутри switch заботится о синхронизации подписок и наблюдаемой коллекции. Функция отписывания просто отписывается от всех членов коллекции.
Собираем все вместе
Используя приведенные методы можно решить исходную задачу, а именно представить метод Observe в виде:public IDisposable Observe(Page page, Action<Rectangle> invalidateHandler)
{
return new DisposeDelegate(new[] {
Observe(page, p => p.Size, value => invalidateHandler(new Rectangle(Point.Empty, value))),
Observe(page, p => p.Items, list =>
ObserveList(list, block => new DisposeDelegate(new[] {
Observe(block, i => i.Rect, invalidateHandler),
Observe(block, i => i.Items, items =>
ObserveList(items, text => Observe(text, t => t.Rect, invalidateHandler)))
})
)
)});
}Попытка выполнить простейший тест показывает что это решение не работает для вложенных свойств, а первый же сеанс отладки показывает что управление не доходит до подписки на коллекцию. Причина в том что метод Observe для одиночного свойства инициирует действие только при изменении самой коллекции.
Эту неприятность можно обойти так: подписывание/отписывание будем считать изменением и будем вызывать "действие":
public static IDisposable Observe<T, TI>(T obj, Expression<Func<T, TI>> getterExpr, Action<TI> action)
where T : ObjectRoot
{
...
var lastValue = getter(obj);
action(lastValue);
...
return new DisposeDelegate(
() => obj.PropertyChanged -= handler,
() => action(lastValue));
}Теперь тесты проходят, но появился побочный эффект - в момент подписывания "летят" уведомления об изменениях. В прилагаемом исходном коде я решаю проблему блокировкой уведомлений на момент подписывания в теле Observe, но можно решить на более низком уровне если разделить понятие подписки на "листовое" свойство, то есть свойство влияющее на регион и "свойство-контейнер", которое прямо или опосредованно ссылается на "листовые" свойства. В первом случае при подписке не надо вызывать уведомление, во втором - нужно.
Заключение
К полученному решению можно добавить "синтаксического сахара" (см. Step3 и класс ObserveExtensions), но и без этого код достаточно нагляден.Таким образом, применение функциональной декомпозиции позволило найти простое и компактное решение не самой тривиальной задачи.
Приложение: исходный код
Для сборки проекта понадобятся библиотеки NUnit и Moq.Исходный код: DeepSubscribe-step3.zip
Решение содержит три последовательные реализации в модулях Step1, Step2 и Step3. Тесты для всех трех решений содержатся в файле ObserverTests (набор тестов один, но применяется ко всем решениям).
Комментариев нет:
Отправить комментарий