воскресенье, 16 января 2011 г.

Работа над ошибками

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

Например в этом фрагменте кода:
var some = page.Observe(
    p => p.PageHeader, block => block.Observe(b => b.Rect, invalidateHandler));

В этом же фрагменте кода настораживает тот факт что block.Observe возвращается функцию отписывания от наблюдения (через DisposeDelegate), однако значение просто-напросто не используется, то есть наш код продолжает "наблюдать" вложенные объекты, которые не входят в состав составного "наблюдаемого" объекта. Помимо этого следует ожидать утечки памяти.

Доработка функции Observe

Другим недостатком функции Observe из предыдущей статьи является то, что она не позволяет отслеживать прежнее и новое значение свойства (что может быть необходимо в общем случае с учетом того что мы разрабатываем общее решение). Устраняем этот недостаток введением двух различных делегатов для старого и нового значений:
public static IDisposable Observe<T, TI>(this T obj, Expression<Func<T, TI>> getterExpr, Action<TI> actionEnter, Action<TI> actionLeave)
 where T : ObjectRoot
{
 var memberAssignment = (MemberExpression)getterExpr.Body;
 var propertyName = memberAssignment.Member.Name;
 var getter = getterExpr.Compile();

 var lastValue = getter(obj);
 actionEnter(lastValue);

 var handler = new PropertyChanged((sender, args) =>
 {
  if (args.PropertyName == propertyName)
  {
   actionLeave(lastValue);
   actionEnter(lastValue = (TI)args.NewValue);
  }
 });
 obj.PropertyChanged += handler;

 return new DisposeDelegate(
  () => obj.PropertyChanged -= handler,
  () => actionLeave(lastValue));
}

Здесь actionEnter и actionLeave вызываются для нового и старого значений соответственно.
Исходная функция при этом реализуется следующим образом (нам интересен этот код с точки зрения обратной совместимости и для простоты использования):
public static IDisposable Observe<T, TI>(this T obj, Expression<Func<T, TI>> getterExpr, Action<TI> action)
 where T : ObjectRoot
{
 return Observe(obj, getterExpr, action, action);
}

Ну и наконец, последний шаг - это функция подписки на изменение контейнера:
public static IDisposable Observe<T, TI>(this T obj, Expression<Func<T, TI>> getterExpr, Func<TI,IDisposable> subscribe)
 where T : ObjectRoot
{
 IDisposable lastDisposable = null;

 return Observe(obj, getterExpr,
  arg => lastDisposable = subscribe(arg),
  i => lastDisposable.Dispose());
}
Само по себе изменение контейнера нам неинтересно - напротив, важные свойства расположены к контейнере, поэтому параметром нового метода является метод подписки на свойства контейнера. При изменении контейнера функция отписывается от старого контейнера (при этом произойдут уведомления для всех наблюдаемых свойств "старого" контейнера, то есть будут вызваны actionEnter для всех вложенных свойств) и подписывается на новый с аналогичными уведомлениями обо всех новых свойствах.

Проверка, тестирование и выводы

Мы предоставили метод Observe с исходной сигнатурой, поэтому старые код компилируется. Кроме того удивительным образом C# распознает тип делегата в третьем параметре функции Observe и использует новую версию Observe для свойства PageHeader в следующем примере:
var some = page.Observe(
    p => p.PageHeader, block => block.Observe(b => b.Rect, invalidateHandler));

Для тестирования обнаруженной проблемы добавлены тесты StopsWatchingOldItems и StopsWatchingOldItemsAndNested.

На основе обнаруженной неполадки следует сделать как минимум следующие выводы:
1. неиспользуемое значение функции, которое несет исключительно важный смысл, нельзя игнорировать
2. необходимо изначально писать тесты для подобных ключевых сценариев
3. чудес не бывает и внезапная работоспособность Observe для простого и составного случая должна была насторожить.

Исходный код с исправлениями: DeepSubscribe-step4.zip

Комментариев нет: