List Visualizer і сериализация з використанням сурогатів (исходники), Різне, Програмування, статті

Введення


Усунення несправностей 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_studioCommon7 PackagesDebuggerVisualizers, для завантаження візуалізаторів, доступних всім користувачам; Documents and Setting \% profile% My DocumentsVisual StudioVisualizers, для завантаження візуалізаторів, доступних тільки поточному користувачеві. Отладчик дізнається, що збірка містить визуализатор, коли в збірці є хоча б один атрибут 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”> / / / Дані, необхідні для візуалізації
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% випадків. Тому я зробив спробу обійти це обмеження і реалізувати більш універсальний визуализатор, призначений для роботи зі списками як серіалізіруемих, так і не серіалізіруемих об’єктів.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*