Введение
Отладчик 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).После активации визуализатора отладчик сериализует объект на отлаживаемой стороне с использованием класса, указанного в атрибуте DebuggerVisualizerAttribute. Обычно для этих целей используется класс VisualizerObjectSource, который для сериализации/десериализации использует BinaryFormatter. Затем состояние объекта в сериализованной форме передается стороне отладчика, где он десериализуется и отображается в окне пользовательского интерфейса. Если визуализатор предназначен не только для отображения, но и для изменения объекта, этот процесс повторяется в обратном порядке, после чего измененный объект передается на отлаживаемую сторону и заменяет исходный объект.
Вверху файла находится атрибут DebugerVisualizerAttribute, который отладчик ищет в момент загрузки визуализатора. Как уже отмечалось выше, данный атрибут содержит 4 параметра: класс визуализатора, класс, предназначенный для поддержки сериализации, тип объекта, для которого предназначен данный визуализатор, а также описание визуализатора.
Создание простого визуализатора
Теперь перейдем к реализации простого визуализатора, предназначенного для отображения списка объектов.[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();
}
}
}
ПРИМЕЧАНИЕ
В качестве свойства 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; }
}
Сериализация с использованием суррогатов
Хотя класс SerializableListVisualizer является полноценным визуализатором списка объектов, его практическое применение слишком ограничено. Мало кто согласится добавить атрибут SerializableAttribute к своему классу только для того, чтобы объекты этого класса можно было посмотреть в красивом виде. Поэтому необходимо как-то обойти это досадное ограничение, и все же реализовать возможность отображения и редактирования списков несериализуемых объектов. Архитектура визуализаторов предусматривает возможность вмешаться в процесс сериализации и десериализации путем создания наследника от VisualizerObjectSource и указания этого типа в атрибуте DebuggerVisualizerAttribute. Таким образом, решение задачи отображения и редактирования несереализуемых объектов по сути своей, сводится к решению задачи сериализации и десериализации несериализируемых объектов. Инфраструктура сериализации в .Net Framework предусматривает возможность «делегирования» полномочий по сериализации некоторого объекта другим объектам. Для этого необходимо определить «суррогатный тип» («surrogate type»), который возьмет на себя операции сериализации и десериализации существующего типа (путем реализации интерфейса ISerializationSurrogate). Затем необходимо зарегистрировать экземпляр суррогатного типа в форматирующем объекте, сообщая ему, какой тип подменяется суррогатным. Когда форматирующий объект обнаруживает, что выполняется сериализация или десериализация экземпляра существующего типа, он вызывает методы, определенные в соответствующем суррогатном типе. Предположим, существует некоторый несериализуемый класс следующего вида:public class NonSerializableClass
{
public int Id { get; set; }
public string Name { get; set; }
}
public interface ISerializationSurrogate
{
void GetObjectData(object obj,
SerializationInfo info, StreamingContext context);
object SetObjectData(object obj,
SerializationInfo info, StreamingContext context,
ISurrogateSelector selector);
}
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;
}
}
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);
}
/// <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;
}
/// <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();
}
}
// Создание объекта, подлежащего сериализации
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; }
}
Интересная возможность.
ОтветитьУдалитьЧестно говоря, не задумывался, что можно туда любой объект запихивать.
Пользовался только для DataTable & DataSet.
Но ИМХО оправдано использование таких приёмов только для часто используемых сложных классов, которые действительно сложно "обозреть" другими способами, например, тем же Trace.WriteLine в окно Output или в крайнем случае выводом в файл, которым я в подобных случаях пользуюсь.
Вообщем, задумался, а не написать ли мне такую штуку для семейства своих Rowset классов.
По поводу Trace.WriteLine посмотри сюда http://sergeyteplyakov.blogspot.com/2009/02/tracepoints.html. Тоже мог не знать о такой штуке.
ОтветитьУдалитьПо поводу своего визуализатора для собственных сложных классов - хорошая мысль. Поможет в отладке.
В моем сообщении почитай о суррогатах. Тоже интересно.
Сорри за оффтоп, но на скриншотах - это какая ОС или что за софт что так выглядят окна? Красиво :)
ОтветитьУдалитьЭто компоненты DevExpress. ОС: Windows XP. На висте или семерке - тоже будет так же красиво:)
ОтветитьУдалитьЯ имею в виду саму форму и кнопки, у них нестандартный вид. Форма же не девэкспрессовская?
ОтветитьУдалитьНестандартный только заголовок окна, который определяется виндовой темой (тема называется Zune).
ОтветитьУдалитьА все остальное - обыкновенная форма (WinForms), на которой расположено две панели DevExpress. На верхней панели - грид, на нижней - две кнопки. Плюс еще Bar от DevExpress. И все.
Интересная статья.
ОтветитьУдалитьИскал инфу по сериализации с использованием суррогатов, но нашел мало. Вот спасибо вам за статью.
Есть вопросы. Не понятно как происходит сериализация объекта если он содержит не сериализуемые проперти/филды. Я думал что надо проверять для каждой проперти/филда, что тип IsSerializable и если нет, то рекурсивно вызывать SerializeFields, передавая в качестве объекта это проперти/филд, но оказалось что и без этого все работает.
Могли бы вы обяснить (или дать ссылки) как сериализуются такие проперти в объекте. Почему происходит повторный вызов GetObjectData/SetObjectData для них. Я думал что GetObjectData/SetObjectData вызывается только один раз.
Подскажи еще почему Flags именно BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Public?
Еще раз спасибо.
@geo: сори за задержку с ответом.
ОтветитьУдалить> Не понятно как происходит, если объект содержит несериализируемые проперти/филды:
Все дело в том, что я реализовал так называемое "неглубокое копирование" (shallo copy) объектов. Т.е. в моем случае я сериализирую только поля "верхнего уровня" объекта и не стараюсь залазить в его внутренности слишком глубоко. Подходит этот вариант или нет, зависит от задачи, но я остановился на таком простом варианте по нескольким причинам. Во-первых, сериализация несериализируемых данных по определению задача не всегда хорошая. Ведь вы можете попытаться сериализовать объект, которых хранит дескриптор какого-то ресурса (типа IntPtr), в таком случае, побитовое копирование поля такого объекта может привести к непредсказуемым последствиям (поскольку два разных экземпляра попытаются освободить этот ресурс дважды). Во-вторых, корректная реализация обобщенного сериализатора дело сложное; для этого нужно значительно больше времени, чем у меня было для исследования этой темы и для решения моих практических задач.
>Почему происходит повторный вызов GetObjectData/SetObjectData
Ответ на этот вопрос тесно связан с вашим вторым вопросом:
>Подскажи еще почему Flags именно BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.NonPublic | BindingFlags.Public?
Все дело в том, что вы не можете напрямую, работая с производным типом получить с помощью рефлексии поля базового класса.
Предположим, у вас есть иерархия классов:
class Base {
private int field1 = 5;
}
class Derived {
private int field2 = 10;
}
Тогда, если у нас есть тип Derived, мы получим только поля, объявленные именно в этом типе, но не получим поля, объявленные в его базовом классе
var type = typeof(Derived);
type.GetFields(BindingFlags.Instance|BindingFlags.NonPublic|BindingFlags.Public);
Этот код возвращает описание только одного поля: field2, а чтобы получить описание еще и поля базового класса (т.е. field1), нужно вызвать метод GetFields еще и у базового типа, класса Derived: type.BaseType.GetFields(...).
Именно это и делает мой код, повторно вызывая SerializeFields для базового типа.
А указанные BindingFlags я использую, чтобы получить открытые/закрытые экземплярные (не статические) поля, объявленные в этом типе.
З.Ы. Я вроде бы повторно вызываю метод SerializeFields/DeserializeFields, а не GetObjectData/SetObjectData, но я надеюсь, что речь шла именно об этом.