четверг, 9 декабря 2010 г.

Проблема "глубокой" подписки (часть 1)

Итак, проблема в упрощенной формулировке: есть объектная модель страницы с текстом, есть метод отрисовки страницы на экране. Модель уведомляет об изменении своих свойств через событие PropertyChanged.
Задача состоит в перерисовке только той части части страницы, которую затронули изменения модели. Проблема же состоит в том, что мы не можем изменить код модели и внедрить в нее механизм уведомления, структура модели является иерархической, а коллекции могут быть изменены или заменены целиком.
Например мы подписаны на изменение размера всех элементов первого параграфа, а параграф затем заменяется другим. Нам необходимо отписаться от всех элементов "старого" параграфа, и подписаться на все элементы "нового" параграфа, а также инвалидировать области всех элементов как старого, так и нового параграфа.

Итак модель:
class Page:ObjectRoot
{
 private Size _pageSize;
 private IList<Block> _items;

 public Size Size
 {
  get { return _pageSize; }
  set { UpdateProperty("Size", ref _pageSize, value); }
 }

 public IList<Block> Items
 {
  get { return _items; }
  set { UpdateProperty("Items", ref _items, value); }
 }
}

class Block:ObjectRoot
{
 private Rectangle _rect;
 private IList<TextItem> _items;

 public Rectangle Rect
 {
  get { return _rect; }
  set { UpdateProperty("Rect", ref _rect, value); }
 }

 public IList<TextItem> Items
 {
  get { return _items; }
  set { UpdateProperty("Items", ref _items, value); }
 }
}

class TextItem : ObjectRoot
{
 private Rectangle _rect;
 private string _text;

 public Rectangle Rect
 {
  get { return _rect; }
  set { UpdateProperty("Rect", ref _rect, value); }
 }

 public string Text
 {
  get { return _text; }
  set { UpdateProperty("Text", ref _text, value); }
 }
}


В этой модели ObjectRoot предоставляет обобщенный механизм уведомлений об изменении значений свойств:
class ObjectRoot
{
 public event PropertyChanged PropertyChanged;

 protected void UpdateProperty<T>(string name, ref T holder, T newValue)
 {
  if (Equals(newValue, holder)) return;
  var oldValue = holder;
  holder = newValue;

  RaisePropertyChanged(new PropertyChangedArgs(name, oldValue, newValue));
 }

 protected void RaisePropertyChanged(PropertyChangedArgs args)
 {
  OnPropertyChanged(args);
 }

 protected virtual void OnPropertyChanged(PropertyChangedArgs args)
 {
  var handler = PropertyChanged;
  if(handler != null)
   handler(this, args);
 }
}

public delegate void PropertyChanged(object sender, PropertyChangedArgs args);

public class PropertyChangedArgs : EventArgs
{
 public PropertyChangedArgs(string name, object oldValue, object newValue)
 {
  PropertyName = name;
  OldValue = oldValue;
  NewValue = newValue;
 }

 public string PropertyName { get; private set; }
 public object OldValue { get; private set; }
 public object NewValue { get; private set; }
}


Наиболее очевидное, "наивное" решение проблемы состоит в создании обертки для каждого класса модели следующего вида:

class NaiveObserver
{
 private readonly Page _page;
 private readonly Action<Rectangle> _invalidateHandler;

 public NaiveObserver(Page page, Action<Rectangle> invalidateHandler)
 {
  _page = page;
  _invalidateHandler = invalidateHandler;

  page.PropertyChanged += PageSizeHandler;
 }

 private void PageSizeHandler(object sender, PropertyChangedArgs args)
 {
  if (args.PropertyName == "Size")
  {
   _invalidateHandler(new Rectangle(Point.Empty, (Size) args.OldValue));
   _invalidateHandler(new Rectangle(Point.Empty, (Size) args.NewValue));
  }
 }

 public void Stop()
 {
  _page.PropertyChanged -= PageSizeHandler;
 }
}


Здесь мы отслеживаем изменение размера страницы и инвалидируем всю область страницы до и после изменения. Довольно много кода, а учтено только изменение одного свойства. Если брать во внимание коллекции, изменение состава коллекций, необходимость переподписывания на изменения при замене одного объекта другим, то решение, даже в таком простом случае как наша исходная модель, становится крайне громоздким.

Далее мы определим протокол уведомления об изменении коллекций и приступим к построению решения.

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