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

Источник: rsdn
Сергей Тепляков

Введение

Отладчик 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 Side
  typeof(VisualizerObjectSource), 
  //Тип объекта, предназначенного для отображения 
  // и редактирования визуализатором
  Target = typeof(List<>), 
  //Текстовое описание, которое будет видеть пользователь 
  //при выборе вашего визуализатора
  Description = "List Visualizer (for serializable data ONLY!)"
  )]
namespace ListVisualizer
{
  /// <summary>
  /// Получает данные от отлаживаемой программы. Отображает их.
  /// "Отправляет" измененные данные обратно.
  /// </summary>
  public class SerializableListVisualizer : DialogDebuggerVisualizer
  {
    protected override void 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>
    public static void 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]
public class SomeSerializableClass
{
  public string S1 { get; set; }
  public string S2 { get; set; }
  public int    I1 { get; set; }
}


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

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

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

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

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

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

public class NonSerializableClass
{
    public int    Id   { get; set; }
    public string 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
{
  public void GetObjectData(
    object obj, SerializationInfo info, StreamingContext context)
  {
    var nonSerializable = (NonSerializableClass)obj;
    info.AddValue("Id", nonSerializable.Id);
    info.AddValue("Name", nonSerializable.Name);
  }

  public object 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;
    //Десериализирую объект класса NonSerializableClass
    var 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
{
  public void GetObjectData(
    object obj, SerializationInfo info, StreamingContext context)
  {
    SerializeFields(obj, obj.GetType(), info);
  }

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

  private static void 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);
  }

  private static void 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;
      // Получаю значение поля из объекта SerializationInfo
      var fieldValue = info.GetValue(fieldName, field.FieldType);
      // Устанавливаю значение соответствующего поля объекта
      field.SetValue(obj, fieldValue);
    }
    // Десериализирую базовую составляющую текущего объекта
    DeserializeFields(obj, type.BaseType, info);
  }

  private const BindingFlags Flags = BindingFlags.Instance 
                                   / BindingFlags.DeclaredOnly
                                   / BindingFlags.NonPublic 
                                   / BindingFlags.Public;
}

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

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

  public ISurrogateSelector GetNextSelector()
  {
    throw new NotImplementedException();
  }

  public ISerializationSurrogate GetSurrogate(
    Type type, StreamingContext context, out ISurrogateSelector selector)
  {
    //Для несерилазируемых типов возвращаю суррогат, который
    //сериализирует все сериализуемые поля объекта
    selector = null;
    if (type.IsSerializable)
      return null;
    selector = this;
    return new 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;

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

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

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

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

    SerializeList(list, outgoingData);
  }

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

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

  public static 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 Side
  typeof(ListVisualizer.ListVisualizerObjectSource), 
    // Тип объекта, предназначенного для отображения 
    // и редактирования визуализатором
    Target = typeof(List<>), 
      //Текстовое описание, которое будет видеть пользователь 
      // при выборе вашего визуализатора
      Description = "Cool List Visualizer" 
  )]

namespace ListVisualizer
{
  public class ListVisualizer : DialogDebuggerVisualizer
  {
    protected override void 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));
            }
          }
        }
      }

    }

    public static void TestShowVisualizer(object objectToVisualize)
    {
      VisualizerDevelopmentHost visualizerHost = 
        new VisualizerDevelopmentHost(
          objectToVisualize, typeof(ListVisualizer), 
          typeof(ListVisualizerObjectSource));

      visualizerHost.ShowVisualizer();
    }
  }
}

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

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

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

  public string S1 { get; set; }
  public string S2 { get; set; }
  public int I1 { get; set; }
  public DateTime Time { get; set; }
}


 


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

Выводы

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


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=21853