Автор: Sergey Teplyakov

Binaries
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 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% случаев. Поэтому я предпринял попытку обойти это ограничение и реализовать более универсальный визуализатор, предназначенный для работы со списками как сериализируемых, так и не сериализируемых объектов.
Помогла статья? Оцените её!
0 из 5. Общее количество голосов - 0
 

You have no rights to post comments

Дмитрий Крикунов

Публикую статьи, обучающие курсы и новости по программированию: алгоритмам, языкам (С++, Java), параллельному программированию, паттернам и библиотекам (Qt, boost).