вторник, 17 марта 2009 г.

Многопоточность в Windows Forms. Control.Invoke().

Человек даже немного поработав с Windows Forms наверняка сталкивался с замечательным исключением System.InvalidOperationException с таким описанием: {"Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on."} Этим сообщением среда выполнения недвусмысленно дает понять, что обращаться к элементам управления пользовательского интерфейса можно только из того потока, который создал этот элемент управления. Такое поведение обусловлено деталями реализации, в частности использованием однопоточной модели аппартаментов (Single-threaded Apartment, STA) и механизма обработки оконных сообщений. Вот простенький пример кода, который приводит к генерации такого исключения:

public partial class Form1 : Form

{

    public Form1()

    {

        InitializeComponent();

        //на форме расположен TextBox и Button.

        //В TextBox будет выводиться количество сработок таймера,

        //таймер запускается по нажатию на кнопку Button

        timer = new Timer(AsyncHandler);

    }

 

    private void button1_Click(object sender, EventArgs e)

    {

        timer.Change(1000, 1000);

    }

    private void AsyncHandler(object data)

    {

        tickCount++;

        textBox1.Text = tickCount.ToString();

    }

 

    private readonly System.Threading.Timer timer;

    private int tickCount;

}

Существует вполне простой и понятный способ решения этой проблемы, путем проверки свойства элемента управления InvokeRequired с последующим вызовом метода Invoke.
Вот самый простой и примитивный способ:

private void AsyncHandler(object data)

{

    tickCount++;

    Action<int> action = DoChangeTicks;

    if (InvokeRequired)

    {

        Invoke(action, tickCount);

    }

    else

    {

        action(tickCount);

    }

 

}

private void DoChangeTicks(int count)

{

    textBox1.Text = tickCount.ToString();

}

Способ простой, но не совсем удобный. В данном случае вспомогательный метод только увеличивает связность и усложняет понимает решаемой задачи. Если воспользоваться новшествами C#3.0 в виде лямбда-выражений, можно сделать несколько более удобную реализацию следующего вида:

private void AsyncHandler(object data)

{

    tickCount++;

    Action action = () => textBox1.Text = tickCount.ToString();

    if (InvokeRequired)

    {

        Invoke(action);

    }

    else

    {

        action();

    }

}

Уже лучше. Мы избавились от необязательного метода, который неразрывно связан с тем действием, которое выполняет метод AsyncHandler. Но, все же, еще есть над чем подумать.
Следующий вариант решения был честно подсмотрен в неплохой книжке: Bill Wagner "More Effective C#".

/// <summary>

/// Расширения облегчающие работу с элементами управления в многопоточной среде.

/// </summary>

public static class ControlExtentions

{

    /// <summary>

    /// Вызов делегата через control.Invoke, если это необходимо.

    /// </summary>

    /// <param name="control">Элемент управления</param>

    /// <param name="doit">Делегат с некоторым действием</param>

    public static void InvokeIfNeeded(this Control control, Action doit)

    {

        if (control.InvokeRequired)

            control.Invoke(doit);

        else

            doit();

    }

    /// <summary>

    /// Вызов делегата через control.Invoke, если это необходимо.

    /// </summary>

    /// <typeparam name="T">Тип параметра делегата</typeparam>

    /// <param name="control">Элемент управления</param>

    /// <param name="doit">Делегат с некоторым действием</param>

    /// <param name="arg">Аргумент делагата с действием</param>

    public static void InvokeIfNeeded<T>(this Control control, Action<T> doit, T arg)

    {

        if (control.InvokeRequired)

            control.Invoke(doit, arg);

        else

            doit(arg);

    }

}

С помощью этого вспомогательного класса, реализация метода, взаимодействующего с элементами управления из других потоков, будет следующей:

private void AsyncHandler(object data)

{

    tickCount++;

    this.InvokeIfNeeded(

        () => textBox1.Text = tickCount.ToString()

            );

}

вторник, 3 марта 2009 г.

Полезные ссылки от Джона Роббинса

Вот решил собрать полезные ссылки, инструменты и статьи, которые советует Джон Роббинс в своей книге "Отладка приложений для Microsoft .Net".
СТАТЬИ
Оптимизация загрузки приложений Windows Forms
1. Practical Tips For Boosting The Performance Of Windows Forms Apps http://msdn.microsoft.com/en-us/magazine/cc163630.aspx
2. CLR Inside Out. Improving Application Startup Time http://msdn.microsoft.com/en-us/magazine/cc163655.aspx
Пользовательский интерфейс и юзабилити
1. Windows Vista User Experience Guidelines. http://msdn.microsoft.com/en-us/library/aa511258.aspx
2. The Joel Test: 12 Steps to Better Code. http://www.joelonsoftware.com/articles/fog0000000043.html
О финализаторах
1. Keep Your Code Running with the Reliability Features of the .NET Framework. http://msdn.microsoft.com/en-us/magazine/cc163716.aspx
О вреде разработки ПО под учетной записью с правами администратора:
2. MakeMeAdmin -- temporary admin for your Limited User account http://blogs.msdn.com/aaron_margosis/archive/2004/07/24/193721.aspx
Разное
1. SUPERASSERT.NET SUPERASSERT Goes .NET http://msdn.microsoft.com/en-us/magazine/cc188701.aspx
2. Автоматизация тестирования пользовательского интерфейса. Build Quick and Easy UI Test Automation Suites with Visual Studio .NET. http://msdn.microsoft.com/en-us/magazine/cc188784.aspx
4. SOS metadata commands. Drill Into .NET Framework Internals to See How the CLR Creates Runtime Objects. http://msdn.microsoft.com/ru-ru/magazine/cc163791(en-us).aspx
5. How do I debug assembles in the Global Assembly Cache (GAC)? http://blogs.msdn.com/junfeng/archive/2005/12/13/503059.aspx
6. Document Your Code in No Time At All with Macros in Visual Studio. http://msdn.microsoft.com/ru-ru/magazine/cc163757(en-us).aspx
8. Speeding up the C# Source Code Editor http://weblogs.asp.net/rweigelt/archive/2006/05/15/446536.aspx
9. Let The CLR Find Bugs For You With Managed Debugging Assistants. http://msdn.microsoft.com/ru-ru/magazine/cc163606(en-us).aspx

БЛОГИ

1. Блог Майкла Столла CLR Debugging Team: http://blogs.gotdotnet.com/jmstall/
2. Блог Марка Руссиновича: http://blogs.technet.com/markrussinovich/
3. Блог Джо Даффи: http://www.bluebytesoftware.com/blog/
4. Блог Tess Ferrandez: http://blogs.msdn.com/tess/
5. Блог Jeff Atwood: http://www.codinghorror.com/blog/
6. Блог Aaron Margosis "Non-Admin" WebLog: http://blogs.msdn.com/aaron_margosis/
7. Блоги Wintellectuals' Blogs: http://www.wintellect.com/cs/blogs/
ИНСТРУМЕНТЫ, УТИЛИТЫ
1. Reflector Лутца Родера
1.1. Сам рефлектор: http://www.aisto.com/roeder/dotnet/
1.2. Denis Bauer Reflector.FileDisassembler. Позволяет дизасемблировать целиком сборку и сохранить ее исходный код. http://www.denisbauer.com/NETTools/FileDisassembler.aspx
1.3. Reflector Diff AddIn. http://www.codingsanity.com/diff.htm
1.4. Kevin Dente's Running Assembly Reflector Add-in. http://Weblogs.asp.net/kdente/articles/438539.aspx На этом перечень AddIn-ов к рефлектору не заканчивается и в интернете можно свободно найти достаточное количество полезных дополнений.
2. Иструменты от Sysinternals. Process Explorer, Process Monitor, DebugView и многое другое. http://technet.microsoft.com/en-us/sysinternals/default.aspx
3. Автономная утилита анализа кода FxCop msdn.microsoft.com/en-us/library/bb429476(VS.80).aspx
4. Debugging Tools for Windows. WinDBG, SOS, ADPlus. www.microsoft.com/whdc/devtools/debugging/ Очень полезные инструменты для отладки, исследования памяти, поиска deadlock-ов и многих других задач.
6. NCover - A test code coverage tool for C# .NET http://ncover.sourceforge.net/
7. Dependency Walker from http://www.dependencywalker.com/
8. Набор инструментов от Sells Brothers. RegexDesigner.NET и другие. http://www.sellsbrothers.com/tools/
9. CopySourceAsHtml (CSAH). Встраиваемый модуль в Visual Studio, который позволяет копировать куски кода виде html. www.jtleigh.com/CopySourceAsHtml/
10. Windows Installer XML (WIX). http://wix.sourceforge.net/ Должно быть интересно посмотреть на бесплатный инструмент от Майкрософт, с помощью которого создаются инсталляционные пакеты для Microsoft Office, Visual Studio и SQL Server. wix.sourceforge.net.
1. GhostDoc. Free add-in for Visual Studio that automatically generates XML documentation comments for C#. http://www.roland-weigelt.de/ghostdoc/
14. Инструменты автоматизации тестирования пользовательского интерфейса: Mercury WinRunner, Rational Robot
15. PInvoke.Net. Do Interop the WIKI way. http://www.pinvoke.net/
17. Regex Kit: Regular Expression Visualizers for VS 2005 http://weblogs.asp.net/rosherove/archive/2005/11/26/AnnoucingRegexKit10.aspx

четверг, 26 февраля 2009 г.

Книга Фредерика Брукса "Мифический человеко-месяц"

Вот уже более 30 лет прошло с момента выхода первого издания легендарной книги Фредерика Брукса ''Мифический человеко-месяц''. Немного перефразируя самого автора можно сказать так: ''Мифический человеко-месяц'' остается книгой, с которой все еще считаются в современной практике программирования. Ее читательская аудитория выходит за пределы сообщества программистов-разработчиков, она все еще порождает статьи, цитаты и письма, причем не только разработчиков программ, но и юристов, врачей, психологов, социологов. Эта книга, написанная 30 лет назад об опыте разработки программ, имевшем место 40 лет назад, остается актуальной и даже полезной.

Причина заключается в том, что ''история человечества - это пьеса, в которой сюжеты постоянны, сценарии медленно меняются с развитием культуры, а декорации меняются непрерывно. Поэтому в ХХ веке мы узнаем себя в Шекспире, Гомере и Библии. Поэтому в той мере, в какой ''МЧ-М'' написан о людях, он устаревает медленно>.

Автор излагает материал очень красочно, сочно, в целом абсолютно не возникает мысли о том, что читаешь техническую литературу. А насколько актуальны его мысли! Ведь мы и теперь любим свою профессию за те же самые радости, мы печалимся от тех же самых проблем, наши проекты все также погрязают в смоляных ямах, мы остаемся все такими же оптимистами и точно так же ошибаемся в своих оценках, надеясь ''что все будет идти хорошо''. Мы точно также осуществляем ''свои построения на основе чистого мышления'', точно так же робеем в своих оценках перед заказчиком и точно также читаем ''Мифический человеко-месяц'' в ''попытке проложить какие-то мостки через это болото''.

Я не хочу приводить весь перечень цитат, которые произвели на меня впечатление, потому что они займут с десяток страниц. Читать книгу очень интересно еще и по той причине, что на страницах автор заложил очень многое, что стало практикой программирования только сегодня. Так, автор описывает самодокументирование программ, которое позже мы увидим в работах МакКоннелла, Ханта и Томаса. Он говорит о важности слияния документации и файлов с исходными текстами. Это именно то, о чем говорит Мейер, и что мы видим в современных средах разработки. Брукс также поднимает вопросы сложности программных систем, которые вытекают из самой природы программного обеспечения, а также модель пошагового создания программных систем. Это именно те проблемы, которые в последствие неоднократно поднимут Буч, Джейкобсон и Рамбо в своих работах. Он уже тогда говорит о необходимости постоянного уточнения требований, о том, что пользователь не сможет, даже если захочет, предоставить полные и непротиворечивые требования, а также о пользе макетирования. Об этом позднее будут говорить Шаллоуей и Тротт и многие другие авторы.

Естественно есть и достаточное количество архаизмов. Все-таки, слишком многое изменилось в мире программного обеспечения за это время. Но это нисколько не раздражает и не расстраивает, а наоборот придает некоторый особый шарм чтению. Ведь только подумайте, книга основана на опыте разработки программ, имевшем место в 60-х годах, написана в середине 70-х, в главе 16 перепечатывается статья ''Серебряной пули нет'', датированная серединой 80-х, второе издание вышло в середине 90-х, а сейчас уже вторая половина следующего десятилетия. Таким образом, мы можем проследить историю отрасли за последние 40 лет!

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

понедельник, 23 февраля 2009 г.

Книга Джона Роббинса "Отладка приложений для Microsoft .NET"

imageНе зависимо от используемого языка программирования и операционной системы, среды разработки и методологии, вашего опыта и профессионализма вашей команды, вы рано или поздно столкнетесь с одной из самых неприятных сторон нашей работы – с ошибками.

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

«Ошибки – это круто! Они помогают залезть в самую глубину и понять, как работают вещи. Мы все попали в этот бизнес, потому что нам нравится учиться, выслеживание ошибок – неотъемлемая часть обучения… Ведь так здорово бывает найти и исправить ошибку! Конечно же, самые хорошие ошибки – это те, которые обнаруживаются до того, как заказчик увидит ваш продукт. Таким образом, вы должны успевать сделать свою работу и найти ошибки до того, как это сделают ваши заказчики. Видеть, как заказчики обнаруживают ошибки, - это совершенно не круто».

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

Во второй главе, «Подготовка к отладке», автор описывает основополагающие вещи, без которой не то что отладка, а сам процесс разработки практически невозможен. Здесь речь идет о системах управления версиями и отслеживания ошибок, о важности модульных и дымовых (smoke) тестов, о важности отладочных символов и хранилища символов, о предупреждениях компилятора и средствах анализа кода. Все это (или многое) уже давно известно многим из нас. Но это позволит критическим взглядом окинуть ваш процесс разработки и сделать соответствующие выводы.

Третья глава посвящена «упреждающему программированию» или «отладке во время кодирования», а именно утверждениям, трассировке и комментированию кода. Наиболее интересным является обзор и реализация инструмента SUPERASSERT.NET, который способен в значительной степени облегчить процесс последующего поиска ошибок.

Далее, автор касается чрезвычайно широкого спектра вопросов. Это и отладка запуска служб, и решение проблем, связанных с многопоточностью и отладкой взаимоблокировок, и реализация обработки исключений, и решение проблем с загрузкой сборок. Также описаны расширенные возможности отладки с использованием Visual Studio, утилиты WinDBG, SOS, FxCop и даже написание собственных правил Code Analysis.

Автор дает ответы на многие вопросы, связанные с разработкой и отладкой программного обеспечения. Но что еще больше впечатляет, так это перечень тем, которые автор поднимает на страницах своей книги. Книга не отличается глубиной изложения, в ней не рассматривается во всех подробностях IL, работа компилятора или внутренности среды CLR. Самым полезным является расширение вашего кругозора, возможность проанализировать собственный опыт и более подробно изучить хромающую область.

Омрачает эту светлую картину только одна проблема – перевод. В книге хватает как откровенных ляпов, так и просто некорректно переведенных терминов или фраз. В книге встречаются переходные разработчики (intermediate developer), собственные приложения (native applications), вползание функций (feature creep), язык посредника (intermediate language) и многое другое. Нельзя сказать, что подобных проблем слишком много или что они сильно сказываются на восприятии материала, но все же издательству следует подходить к таким вопросам более серьезно.

Несмотря на это книга, безусловно, стоит затраченных средств и усилий и будет полезна большинству разработчиков. И самое главное, «помните, что отладчик – это всего лишь инструмент, как, например, отвертка. Он делает только то, что вы приказываете ему делать. Настоящий отладчик находится у вас в голове».

пятница, 20 февраля 2009 г.

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_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% случаев. Поэтому я предпринял попытку обойти это ограничение и реализовать более универсальный визуализатор, предназначенный для работы со списками как сериализируемых, так и не сериализируемых объектов.