Сообщений 0    Оценка 105        Оценить  
Система Orphus

List Visualizer и сериализация с использованием суррогатов

Автор: Сергей Тепляков
ООО НПП Кронос

Источник: RSDN Magazine #1-2009
Опубликовано: 30.04.2009
Исправлено: 10.12.2016
Версия текста: 1.0
Введение
Архитектура визуализаторов
Создание простого визуализатора
Сериализация с использованием суррогатов
Реализация визуализатора списка объектов, не поддерживающих сериализацию
Выводы

ListVisualizer Binaries
ListVisualizer Sources

Введение

Отладчик Visual Studio предоставляет множество полезных инструментов, без которых сложно себе представить разработку сложных коммерческих приложений. Одним из главных инструментов в процессе отладки являются окна семейства Watch, предназначенные для отображения и редактирования текущего состояния объектов. С его помощью вы можете увидеть значение любого поля или свойства, независимо от того, насколько сложным является объект. Но, как и любой механизм общего назначения, окна семейства Watch содержат ряд ограничений, существенно усложняющих процесс отладки.. Для просмотра и редактирования сложных объектов, разработчики отладчика Visual Studio создали механизм визуализаторов (Visualizer), способных представлять данные объектов в их естественной форме. В комплекте Visual Studio поставляются визуализаторы строковых типов данных (Text Visualizer, Xml Visualizer и Html Visualizer), а также визуализаторы контейнеров ADO.NET (DataSet Visualizer, DataTable Visualizer, DataView Visualizer и DataViewManager Visualizer). Но значительно более важным является возможность добавления собственных визуализаторов для создания в отладчике альтернативных представлений данных в удобном пользовательском интерфейсе.

Архитектура визуализаторов

Архитектура визуализаторов основана на том, что в процессе отладки участвуют две составляющие: сторона отладчика (Debugger Side) – код, работающий под управлением Visual Studio (окна Watch, DataTips, QuickWatch и др.) и отлаживаемая сторона (Debuggee Side) – код, который вы отлаживаете (ваша программа).

Алгоритм работы визуализатора следующий.

Вначале отладчик должен загрузить классы визуализаторов, которые располагаются в одном из двух каталогов: каталог_установки_Visual_studio\Common7\ Packages\Debugger\Visualizers, для загрузки визуализаторов, доступных всем пользователям; \Documents and Setting\%profile%\My Documents\Visual Studio\Visualizers, для загрузки визуализаторов, доступных только текущему пользователю. Отладчик узнает, что сборка содержит визуализатор, когда в сборке есть хотя бы один атрибут DebuggerVisualizerAttribute. Этот атрибут сообщает отладчику класс визуализатора, класс, ответственный за передачу данных между Debuggee Side и Debugger Side, тип объекта, предназначенного для отображения и редактирования, а также описание визуализатора.

Когда в окне семейства Watch выводится значение, для типа которого определен визуализатор, то в столбце Value будет находиться значок увеличительного стекла. Если щелкнуть на нем, отладчик выберет и запустит последний визуализатор, который использовался для данного класса (рисунок 1).


Рисунок 1 – Визуализатор класса string

После активации визуализатора отладчик сериализует объект на отлаживаемой стороне с использованием класса, указанного в атрибуте DebuggerVisualizerAttribute. Обычно для этих целей используется класс VisualizerObjectSource, который для сериализации/десериализации использует BinaryFormatter. Затем состояние объекта в сериализованной форме передается стороне отладчика, где он десериализуется и отображается в окне пользовательского интерфейса. Если визуализатор предназначен не только для отображения, но и для изменения объекта, этот процесс повторяется в обратном порядке, после чего измененный объект передается на отлаживаемую сторону и заменяет исходный объект.

Создание простого визуализатора

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

[assembly: DebuggerVisualizer(
  //Класс визуализатораtypeof(ListVisualizer.SerializableListVisualizer), //Класс, осуществляющий передачу данных между Debuggee Side и Debugger Sidetypeof(VisualizerObjectSource), 
  //Тип объекта, предназначенного для отображения 
  // и редактирования визуализатором
  Target = typeof(List<>), 
  //Текстовое описание, которое будет видеть пользователь 
  //при выборе вашего визуализатора
  Description = "List Visualizer (for serializable data ONLY!)"
  )]
namespace ListVisualizer
{
  /// <summary>/// Получает данные от отлаживаемой программы. Отображает их./// "Отправляет" измененные данные обратно./// </summary>publicclass SerializableListVisualizer : DialogDebuggerVisualizer
  {
    protectedoverridevoid Show(
IDialogVisualizerService windowService, 
      IVisualizerObjectProvider objectProvider)
    {
      IList list = (IList)objectProvider.GetObject();

      Debug.Assert(list != null, "list != null");

      if (list != null)
      {
        using (var form =
            new ListVisualizerForm(list, objectProvider.IsObjectReplaceable))
        {
          if (windowService.ShowDialog(form) == DialogResult.OK)
          {
            if (objectProvider.IsObjectReplaceable)
            {
              var ms = new MemoryStream();
              VisualizerObjectSource.Serialize(ms, form.List);
              objectProvider.ReplaceData(ms);
            }
          }
        }
      }

    }

    /// <summary>/// Предназначен для тестирования. Может быть использован в/// модульных тестах, консольных приложениях etc./// </summary>/// <param name="objectToVisualize">
    /// Данные, необходимые для визуализации</param>publicstaticvoid TestListVisualizer(object objectToVisualize)
    {
      var visualizerHost = new VisualizerDevelopmentHost(objectToVisualize, 
                                 typeof(SerializableListVisualizer));
      visualizerHost.ShowVisualizer();
    }
  }
}

Вверху файла находится атрибут DebugerVisualizerAttribute, который отладчик ищет в момент загрузки визуализатора. Как уже отмечалось выше, данный атрибут содержит 4 параметра: класс визуализатора, класс, предназначенный для поддержки сериализации, тип объекта, для которого предназначен данный визуализатор, а также описание визуализатора.

ПРИМЕЧАНИЕ

В качестве свойства Target атрибута DebuggerVisualizerAttribute необходимо указывать класс объекта, предназначенного для редактирования и отображения визуализатором. В таком случае визуализатор будет доступен для объектов указанного класса, а также для всех объектов производных классов. В свойстве Target нельзя указать тип интерфейса. В нашем примере следующее значение свойства Target недопустимо: Target = typeof(IList<>).

Сам класс визуализатора, являющийся наследником DialogDebuggerVisualizer, содержит единственный метод Show, который и реализует всю работу визуализатора. В первой строке вызывается метод objectProvider.GetObject() с помощью которого визуализатор получает данные, необходимые для отображения. Затем создается форма, которая отображается с использованием интерфейса IDialogVisualizerService после чего проверяется возможность редактирования данных с помощью свойства IsObjectReplaceable интерфейса IVisualizerObjectProvider, и если такая возможность присутствует – вызываю метод ReplaceData, для замены данных в отлаживаемой программе.

Второй метод класса – SerializableListVisualizer TestListVisualizer предназначен для упрощения задачи тестирования визуализатора, и может вызываться из консольного приложения или модульного теста.

После копирования сборки визуализатора (со всеми зависимостями) в одну из соответствующих папок (речь о которых шла выше) данный визуалитор можно будет использовать в любом проекте Visual Studio в последующих сеансах отладки.

Поскольку SerializableListVisualizer для передачи данных между процессами использует VisualizerObjectSource, который (как уже говорилось выше) в свою очередь использует BinaryFormatter для сериализации/десериализации объектов, то данный визуализатор будет работать только с объектами, помеченными атрибутом SerializableAttribute. Однако при попытке использовать данный визуализатор с классом, не помеченным атрибутом SerializableAttribute (и не реализующим интерфейс ISerializable), вы получите исключение, в котором говорится о том, что указанный класс не является сериализуемым.

Для тестирования работы визуализатора воспользуемся следующим тестовым классом:

[Serializable]
publicclass SomeSerializableClass
{
  publicstring S1 { get; set; }
  publicstring S2 { get; set; }
  publicint    I1 { get; set; }
}


Рисунок 2. List Visualizer для сериализиуемых данных.

Сериализация с использованием суррогатов

Хотя класс SerializableListVisualizer является полноценным визуализатором списка объектов, его практическое применение слишком ограничено. Мало кто согласится добавить атрибут SerializableAttribute к своему классу только для того, чтобы объекты этого класса можно было посмотреть в красивом виде. Поэтому необходимо как-то обойти это досадное ограничение, и все же реализовать возможность отображения и редактирования списков несериализуемых объектов.

Архитектура визуализаторов предусматривает возможность вмешаться в процесс сериализации и десериализации путем создания наследника от VisualizerObjectSource и указания этого типа в атрибуте DebuggerVisualizerAttribute. Таким образом, решение задачи отображения и редактирования несереализуемых объектов по сути своей, сводится к решению задачи сериализации и десериализации несериализируемых объектов.

Инфраструктура сериализации в .Net Framework предусматривает возможность «делегирования» полномочий по сериализации некоторого объекта другим объектам. Для этого необходимо определить «суррогатный тип» («surrogate type»), который возьмет на себя операции сериализации и десериализации существующего типа (путем реализации интерфейса ISerializationSurrogate). Затем необходимо зарегистрировать экземпляр суррогатного типа в форматирующем объекте, сообщая ему, какой тип подменяется суррогатным. Когда форматирующий объект обнаруживает, что выполняется сериализация или десериализация экземпляра существующего типа, он вызывает методы, определенные в соответствующем суррогатном типе.

Предположим, существует некоторый несериализуемый класс следующего вида:

      public
      class NonSerializableClass
{
    publicint    Id   { get; set; }
    publicstring Name { get; set; }
}

Класс не помечен атрибутом SerializableAttrubute и не реализует интерфейс ISerializable, т.е. не предусматривает сериализацию своих экземпляров. Это ограничение можно обойти, создав суррогатный тип, который возьмет на себя ответственность за сериализацию и десериализацию экземпляров указанного типа. Для этого нужно создать класс, реализующий интерфейс ISerializationSurrogate, который определен следующим образом:

      public
      interface ISerializationSurrogate
{
  void GetObjectData(object obj,
    SerializationInfo info, StreamingContext context);

  object SetObjectData(object obj,
    SerializationInfo info, StreamingContext context,
    ISurrogateSelector selector);
}

Этот интерфейс аналогичен интерфейсу ISerializable. Отличие состоит в том, что методы интерфейса ISerializationSurrogate принимают дополнительный параметр – ссылку на реальный объект, подлежащий сериализации.

Поскольку сам класс NonSerializableClass достаточно прост, то и реализация соответствующего суррогата будет простой. В методе GetObjectData первый параметр нужно привести к соответствующему типу и сохранить все поля в объекте SerializationInfo. Для десериализации объекта вызывается метод SetObjectData, при этом ссылка на десериализуемый объект возвращается статическим методом GetUnitializedObject, принадлежащим FormatterServices. Т.е. все поля объекта перед десериализацией пусты и для объекта не вызван никакой конструктор. Задача метода SetObjectData – инициализировать поля объекта, получая значения из объекта SerializationInfo.

      public
      class NonSerializableClassSurrogate : ISerializationSurrogate
{
  publicvoid GetObjectData(
    object obj, SerializationInfo info, StreamingContext context)
  {
    var nonSerializable = (NonSerializableClass)obj;
    info.AddValue("Id", nonSerializable.Id);
    info.AddValue("Name", nonSerializable.Name);
  }

  publicobject SetObjectData(
    object obj, SerializationInfo info, 
StreamingContext context, ISurrogateSelector selector)
  {
    var nonSerializable = (NonSerializableClass)obj;
    nonSerializable.Id = info.GetInt32("Id");
    nonSerializable.Name = info.GetString("Name");
    return obj;
  }
}

Единственная проблема, которая может возникнуть при создании суррогатных типов даже для простых объектов – это создание суррогатов для value-типов. Проблема в том, что первый параметр метода SetObjectData относится к типу Object, т.е. value-тип будет передан в упакованном виде, а в таких языках программирования как C# и Visual Basic просто не предусмотрена возможность изменения свойств непосредственно в упакованном объекте. Единственный способ сделать это – воспользоваться механизмом рефлексии (reflection) следующим образом:

      public
      object SetObjectData(
  object obj, SerializationInfo info, 
StreamingContext context, ISurrogateSelector selector)
{
  typeof(NonSerializableClass).GetProperty("Id").SetValue(
obj, info.GetInt32("Id"), null);
  typeof(NonSerializableClass).GetProperty("Name").SetValue(
obj, info.GetString("Name"), null);
  return obj;
}

Использование суррогатного типа следующее:

      //Создание объекта, подлежащего сериализации
      var ns1 = new NonSerializableClass { Id = 47, Name = "TestName" };
var formatter = new BinaryFormatter();
var ss = new SurrogateSelector();
// Зарегистрировать суррогатный класс
ss.AddSurrogate(typeof(NonSerializableClass),
    new StreamingContext(StreamingContextStates.All),
    new NonSerializableClassSurrogate());
// Указать селектор
formatter.SurrogateSelector = ss;

using (var ms = new MemoryStream())
{
    //Сериализирую объект класса NonSerializableClass
    formatter.Serialize(ms, ns1);
    //Устанавливаю в 0 позицию в потоке MemoryStream
    ms.Position = 0;
    //Десериализирую объект класса NonSerializableClassvar ns2 = (NonSerializableClass)formatter.Deserialize(ms);
    //Осталось проверить правильность сериализации и десериализации
    Assert.AreEqual(ns1.Id, ns2.Id);
    Assert.AreEqual(ns1.Name, ns2.Name);
}

Теперь перейдем к реализации суррогатного типа, осуществляющего сериализацию/десериализацию несериализируемых типов.

Основная работа по сериализации объекта осуществляет функция SerializeFields. Ее реализация основана на использовании механизма рефлексии, с помощью которого я получаю все поля объекта и, если поле является сериализуемым, добавляю значение поля в объект SerializationInfo. Поскольку я получаю только поля объекта, объявленные в текущем типе, функцию SerializeFields нужно вызвать рекурсивно для всех базовых классов сериализуемого объекта. Рекурсия останавливается при достижении класса Object.

Десериализация осуществляется с помощью функции DeserializeFields и ее реализация является аналогичной.

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

      /// <summary>
      /// "Суррогат" сериализирует все сериализируемые поля объекта
      /// </summary>
      public
      class NonSerializableSurrogate : ISerializationSurrogate
{
  publicvoid GetObjectData(
    object obj, SerializationInfo info, StreamingContext context)
  {
    SerializeFields(obj, obj.GetType(), info);
  }

  publicobject SetObjectData(
    object obj, SerializationInfo info, 
StreamingContext context, ISurrogateSelector selector)
  {
    DeserializeFields(obj, obj.GetType(), info);
    return obj;
  }

  privatestaticvoid SerializeFields(
    object obj, Type type, SerializationInfo info)
  {
    // Попытка сериализации полей типа Object 
    // является ограничением рекурсииif (type == typeof(object))
      return;

    // Получаю все экземплярные поля, 
    // объявленные в объекте текущего классаvar fields = type.GetFields(Flags);
    foreach (var field in fields)
    {
      // Игнорирую все несериализируемые поляif (field.IsNotSerialized)
        continue;

      var fieldName = type.Name + "+" + field.Name;
      // Добавляю значение поля в объект SerializationInfo
      info.AddValue(fieldName, field.GetValue(obj));
    }
    // Сериализирую базовую составляющую текущего объекта
    SerializeFields(obj, type.BaseType, info);
  }

  privatestaticvoid DeserializeFields(
    object obj, Type type, SerializationInfo info)
  {
    // Попытка сериализации полей типа Object 
    // является ограничением рекурсииif (type == typeof(object))
      return;

    // Получаю все экземплярные поля, объявленные в объекте текущего классаvar fields = type.GetFields(Flags);

    foreach (var field in fields)
    {
      // Игнорирую все несериализируемые поляif (field.IsNotSerialized)
        continue;
      var fieldName = type.Name + "+" + field.Name;
      // Получаю значение поля из объекта SerializationInfovar fieldValue = info.GetValue(fieldName, field.FieldType);
      // Устанавливаю значение соответствующего поля объекта
      field.SetValue(obj, fieldValue);
    }
    // Десериализирую базовую составляющую текущего объекта
    DeserializeFields(obj, type.BaseType, info);
  }

  privateconst BindingFlags Flags = BindingFlags.Instance 
| BindingFlags.DeclaredOnly
                                   | BindingFlags.NonPublic 
                                   | BindingFlags.Public;
}

Для простоты использования класса NonSerializableSurrogate создадим соответствующий селектор (класс, реализующий интерфейс ISurrogateSelector), который будет возвращать NonSerializableSurrogate только при попытке сериализации класса, не поддерживающего сериализацию.

      /// <summary>
      /// Реализует выбор необходимого суррогата
      /// </summary>
      public
      class NonSerializableSurrogateSelector : ISurrogateSelector
{
  publicvoid ChainSelector(ISurrogateSelector selector)
  {
    thrownew NotImplementedException();
  }

  public ISurrogateSelector GetNextSelector()
  {
    thrownew NotImplementedException();
  }

  public ISerializationSurrogate GetSurrogate(
Type type, StreamingContext context, out ISurrogateSelector selector)
  {
    //Для несерилазируемых типов возвращаю суррогат, который//сериализирует все сериализуемые поля объекта
    selector = null;
    if (type.IsSerializable)
      returnnull;
    selector = this;
    returnnew NonSerializableSurrogate();
  }

}

Пример использования классов NonSerializableSurrogate и NonSerializableSurrogateSelector:

      // Создание объекта, подлежащего сериализации
      var ns1 = new NonSerializableClass { Id = 47, Name = "TestName" };
var formatter = new BinaryFormatter();
formatter.SurrogateSelector = new NonSerializableSurrogateSelector();

using (var ms = new MemoryStream())
{
    // Сериализирую объект класса NonSerializableClass
    formatter.Serialize(ms, ns1);
    ms.Position = 0;

    // Десериализирую объект класса NonSerializableClassvar ns2 = (NonSerializableClass)formatter.Deserialize(ms);
    // Осталось проверить правильность сериализации и десериализации
    Assert.AreEqual(ns1.Id, ns2.Id);
    Assert.AreEqual(ns1.Name, ns2.Name);
}

Реализация визуализатора списка объектов, не поддерживающих сериализацию

Для реализации визуализатора списка объектов, не поддерживающих сериализацию, необходимо реализовать класс-наследник от VisualizerObjectSource, который с помощью суррогатного типа, определенного в предыдущем разделе, будет заниматься сериализацией/десериализацией списка объектов, не поддерживающих сериализацию.

      /// <summary>
      /// Предназначен для сериализации списка объектов
      /// </summary>
      public
      class ListVisualizerObjectSource : VisualizerObjectSource
{
  publicoverridevoid GetData(object target, System.IO.Stream outgoingData)
  {
    var list = target as IList;
    if (list == null)
      return;

    SerializeList(list, outgoingData);
  }

  publicoverrideobject CreateReplacementObject(
    object target, Stream incomingData)
  {
    return DeserializeList(incomingData);
  }

  publicstatic IList DeserializeList(Stream stream)
  {
    var formatter = new BinaryFormatter();
    formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
    return (IList)formatter.Deserialize(stream);
  }
  
  publicstatic Stream SerializeList(IList list)
  {
    var stream = new MemoryStream();
    SerializeList(list, stream);
    return stream;
  }

  publicstatic Stream SerializeList(IList list, Stream stream)
  {
    IFormatter formatter = new BinaryFormatter();
    formatter.SurrogateSelector = new NonSerializableSurrogateSelector();
    formatter.Serialize(stream, list);
    return stream;
  }

}

Реализовать визуализатор на основе уже разработанных классов совсем несложно.

[assembly: DebuggerVisualizer(
  // Класс визуализатораtypeof(ListVisualizer.ListVisualizer), 
  // Класс, осуществляющий передачу данных 
  // между Debuggee Side и Debugger Sidetypeof(ListVisualizer.ListVisualizerObjectSource), 
    // Тип объекта, предназначенного для отображения 
    // и редактирования визуализатором
    Target = typeof(List<>), 
      //Текстовое описание, которое будет видеть пользователь 
      // при выборе вашего визуализатора
      Description = "Cool List Visualizer" 
  )]

namespace ListVisualizer
{
  publicclass ListVisualizer : DialogDebuggerVisualizer
  {
    protectedoverridevoid Show(
IDialogVisualizerService windowService, 
      IVisualizerObjectProvider objectProvider)
    {
      IList list = ListVisualizerObjectSource.DeserializeList(
        objectProvider.GetData());

      Debug.Assert(list != null, "list != null");

      if (list != null)
      {
        using (var form =
            new ListVisualizerForm(list, objectProvider.IsObjectReplaceable))
        {
          if (windowService.ShowDialog(form) == DialogResult.OK)
          {
            if (objectProvider.IsObjectReplaceable)
            {
              objectProvider.ReplaceData(
                ListVisualizerObjectSource.SerializeList(form.List));
            }
          }
        }
      }

    }

    publicstaticvoid TestShowVisualizer(object objectToVisualize)
    {
      VisualizerDevelopmentHost visualizerHost = 
        new VisualizerDevelopmentHost(
objectToVisualize, typeof(ListVisualizer), 
          typeof(ListVisualizerObjectSource));

      visualizerHost.ShowVisualizer();
    }
  }
}

Осталось скопировать полученную сборку в папку визуализаторов и запустить отладку.

Для проверки работы визуализатора будем использовать несериализируемый класс следующего вида:

      public
      class NonSerializableClass
{
  public NonSerializableClass()
  {
    Time = DateTime.Now;
  }

  publicstring S1 { get; set; }
  publicstring S2 { get; set; }
  publicint I1 { get; set; }
  public DateTime Time { get; set; }
}



Рисунок 3 – List Visualizer для списков сериализуемых и несериализуемых объектов

Выводы

В этой небольшой статье я рассмотрел два, казалось бы, совершенно не связанных вопроса: реализация собственных визуализаторов и сериализацию с использованием суррогатов. Это связано с тем, что для работы визуализатора требуется сериализация/десериализация объектов между двумя процессами: процессом отладчика и процессом отлаживаемого кода. Наличие в арсенале разработчика визуализатора списка объектов может существенно упростить отладку и просмотр состояния объектов на этапе выполнения. Но ограничить себя просмотром и изменением только сериализуемых объектов – значит отказаться от этого инструмента в 90% случаев. Поэтому я предпринял попытку обойти это ограничение и реализовать более универсальный визуализатор, предназначенный для работы со списками как сериализируемых, так и не сериализируемых объектов.


Эта статья опубликована в журнале RSDN Magazine #1-2009. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 105        Оценить