Хочу поделиться проектом, который может оказаться полезным тем, кто всё ещё разрабатывает/поддерживает десктопные .NET Framework приложения на WinForms.
В моей организации — как, наверное, и во многих других — среда разработки Microsoft Visual Studio оказалась под запретом, причём как её коммерческие версии, так и Community Edition. Всем было рекомендовано перейти на VS Code, которая хороша во всём, кроме полноценной поддержки WinForms-приложений. А именно - VS Code, в отличие от "обычной" Visual Studio, не имеет встроенного редактора (дизайнера) форм, без которого вёрстка сложных форм становится как минимум неудобной. Если с редактированием "code behind" файла проблем нет (Form1.cs, UserControl1.cs), то с файлом, описывающим "визуальщину" (Form1.designer.cs, UserControl1.designer.cs) - беда: в VS Code его можно править только на уровне кода, "WYSIWYG experience" тут недоступен.
Каких-либо доступных альтернатив штатному дизайнеру Microsoft Visual Studio я не нашёл: JetBrains Rider у нас тоже под запретом, SharpDevelop с 2020 года не развивается, а найденные на GitHub проекты меня не устроили по ряду причин:
один из авторов, вместо использования штатного класса
System.ComponentModel.Design.DesignSurface, зачем-то реализовал его эмуляцию, вплоть до ручной отрисовки рамки ("selection adorner") и её "sizing grips"; ну а отказавшись от классаDesignSurface- автор был вынужден всю сериализацию/десериализацию реализовывать сам, с кучей ограничений/хардкода/костылей (в первую очередь - в виде жёсткого списка поддерживаемых контролов и их свойств); в общем - "no go", однозначно;другой автор не смог решить проблему десериализации формы из кода "дизайнерского" файла (Form1.designer.cs), т.к. метод
Deserialize()классаSystem.ComponentModel.Design.Serialization.TypeCodeDomSerializerожидает на входе экземплярSystem.CodeDom.CodeTypeDeclaration, для получения которого (из исходного кода) какие-либо "out of the box" решения отсутствуют. Например, классMicrosoft.CSharp.CSharpCodeProvider, имея работающий методGenerateCodeFromType(CodeTypeDeclaration, TextWriter, ...), "зеркального" по смыслу (и работающего) метода - не имеет; точнее - метод-то у него есть (CodeCompileUnit Parse(TextReader)), но он не рабочий: его вызов приводит к вызову метода [этого же класса]ICodeParser CreateParser(), который помечен как[Obsolete], и тело которого содержит только "return null;". Иными словами,CSharpCodeProviderумеет играть только в одни ворота - генерировать исходный код из Code DOM, но не наоборот. В результате, этот автор решил сериализацию/десериализацию выполнять в/из XML, придумав для этого некий проприетарный формат; тоже "no go", по понятным причинам;ни одно из попавшихся мне решений не имело поддержки ресурсов (.resx файлов) и event binding (создания методов-обработчиков событий непосредственно из дизайнера).
В итоге, я решил написать дизайнер форм сам, поставив себе цель использовать только нативные решения Microsoft, не изобретая велосипедов и избегая какого-либо хардкода - чтобы получить решение, максимально близкое к штатному дизайнеру Microsoft Visual Studio. Забегая вперёд - достичь этой цели получилось, использовав из сторонних решений только Highlight.js для синтаксической подсветки кода, который при желании (и только для preview) можно сгенерировать из дизайнера.
Первым делом нужно было решить задачу создания Code DOM по исходному коду .designer.cs файла - без этого не получится использовать TypeCodeDomSerializer для загрузки формы/юзерконтрола в DesignSurface. Для этого было решено использовать open source платформу .NET Compiler Platform (aka "Roslyn"), доступную в виде подгружаемого пакета Microsoft.CodeAnalysis. Был написан класс DesignerRoslynToCodeDomConverter, преобразующий исходный код в Microsoft.CodeAnalysis.SyntaxTree, затем в Microsoft.CodeAnalysis.CSharp.Syntax.CompilationUnitSyntax и затем - самая объёмная и важная часть - в System.CodeDom.CodeTypeDeclaration. Последняя часть кое-где использует эвристический подход, поскольку на построение семантической модели я не стал замахиваться, и в паре мест различать элементы кода приходится буквально по naming convention (см. метод TryCreateTypeReferenceExpression()). В случае нераспознанных конструкций - создаётся CodeSnippetStatement, чтобы не потерять код и иметь возможность диагностировать проблему и доработать преобразование. Если какие-то доработки и потребуются, то с большой вероятностью именно в классе DesignerRoslynToCodeDomConverter; отдельным челленджем (по крайней мере - для меня) будет создание семантической модели для исключения эвристического подхода. Но с учётом весьма простого "типового" синтаксиса, используемого внутри метода InitializeComponent(), пока что проблем не наблюдается.
Другой интересной задачей было определить набор сервисов, подлежащих добавлению в "service container", ассоциированный с нашей DesignSurface. В одном случае, было достаточно использовать готовый сервис (из System.ComponentModel.Design), а во всех остальных - пришлось написать свой сервис "с нуля", причём иногда было достаточно реализовать интерфейс сервиса лишь частично. Не имея документации о том, какая внутренняя логика реализована "под капотом" классов DesignerSerializationManager и TypeCodeDomSerializer (и какие именно сервисы и для каких целей там используются), приходилось брести наощупь, изучая исходные коды Microsoft, проваливаясь в них в дебаге, строя/проверяя всякие гипотезы. В итоге - получился вот такой "букет" из сервисов:
Экспериментальным путём было выяснено, что без сервиса
System.ComponentModel.Design.Serialization.CodeDomComponentSerializationServiceдва других работать не будут, а именно: сервисSystem.ComponentModel.Design.UndoEngine(которому необходимо уметь сериализовать/десериализовать состояния компонент для операций Undo и Redo) и сервис, наследующийSystem.ComponentModel.Design.Serialization.IDesignerSerializationService(которому необходимо уметь делать то же самое для операций Cut, Copy и Paste);Сервис
System.ComponentModel.Design.Serialization.INameCreationServiceпришлось реализовать самому (см. классUiTools.WinForms.Designer.Core.MyNameCreationService) - без него компоненты, добавляемые наDesignSurface, не будут получать корректные имена ("button1", "button2", ...);System.ComponentModel.Design.Serialization.IDesignerSerializationServiceтоже реализован самостоятельно (классMyDesignerSerializationService) - именно он отвечает за операции Cut, Copy и Paste;System.ComponentModel.Design.IMenuCommandService- отвечает за контекстное меню, отображаемое по щелчку ПКМ на компонентахDesignSurface; реализован в классеMyMenuCommandService;System.ComponentModel.Design.UndoEngine- отвечает за операции Undo и Redo; реализован в классеMyUndoEngine;System.Drawing.Design.IToolboxService- отвечает за панель "Toolbox"; львиную долю его методов/свойств оказалось возможным оставить "пустыми" или возвращающимиnull; реализован в классеMyToolboxService;System.ComponentModel.Design.ITypeDiscoveryService- используется в некоторых компонентах/контролах для поиска требуемых типов; например - в контролеDataGridView, когда по клику на "Add column..." (в смарт-тэге "DataGridView Tasks") отображается диалогSystem.Windows.Forms.Design.DataGridViewAddColumnDialog, в котором комбобокс "Type" необходимо заполнить именами типов, производных отDataGridViewColumn(DataGridViewTextBoxColumn,DataGridViewCheckBoxColumnи т.д.) - для поиска этих типов "под капотом" используется, как оказалось, как разITypeDiscoveryService; реализован в классеMyTypeDiscoveryService;System.ComponentModel.Design.IEventBindingService- без этого сервиса не получится создавать подписки на события компонент; реализован в классеMyEventBindingService;System.ComponentModel.Design.IResourceService- отвечает за работу с ресурсами (.resx файлами); реализован в классеMyResourceService;System.ComponentModel.Design.ITypeResolutionService- отвечает за поиск типа или сборки по имени; классUiTools.WinForms.Designer.Core.MyTypeResolutionService, реализующий данный интерфейс - одно из самых важных мест проекта;4 сервиса, наследующих
System.ComponentModel.Design.DesignerOptionServiceи управляющих поведениемDesignSurfaceв части выравнивания контролов и наличия "сетки"; реализованы в классахDesignerOptionServiceSnapLines,DesignerOptionServiceGrid,DesignerOptionServiceGridWithoutSnappingиDesignerOptionServiceNoGuides.
Были и непонятные проблемы, которые приходилось решать "инвазивно", через reflection. Например, NRE, возникающее при наведении указателя мыши на контрол ToolStrip - точнее, на ToolStripTemplateNode (комбобокс с подсказкой "Type here", служащий для in-site создания новых элементов у ToolStrip). Как по мне - это явный баг в методе MiniToolStripRenderer.OnRenderArrow(), заключающийся в "потерянной" проверке на null:
if (e.Item?.DeviceDpi != ...) { previousDeviceDpi = e.Item.DeviceDpi; ... }- здесь обращение к e.Item.DeviceDpi приводит к NRE, хотя предшествующая проверка прошла нормально за счёт Elvis-оператора ("?."). Другое дело, что мне совершенно непонятна причина, по которой e.Item оказывается тут равным null; причём это имеет место только в случае включённой поддержки High DPI - когда в файле App.config есть строчка "<add key="DpiAwareness" value="PerMonitorV2"/>". Вылечить получилось с помощью грубого вмешательства через рефлексию - см. метод FixMenuStripDpiBug() в классе UiTools.WinForms.Designer.Program, если кому интересно. В итоге - NRE ушло, но "осадочек остался" (ибо понимание причины так и не появилось).
В целом, удалось создать дизайнер форм/юзерконтролов, сильно похожий на штатный дизайнер Microsoft Visual Studio, что для многих может оказаться плюсом. В нём есть ряд ограничений - см. разделы "Limitations" и "Specifics" в файле "UiTools.WinForms.Designer\README.md". И есть ряд моментов, проработанных не до конца - см. раздел "Grey area" там же. Но в целом - работает, Студию мне заменяет. Надеюсь, что кому-нибудь тоже окажется полезен. Использовать можно и как standalone приложение, и как VSIX-расширение для среды VS Code(тут рекомендуется ознакомиться с файлом "UiTools.WinForms.Designer\UiTools.WinForms.Designer.VsCodeExtension\README.md").
Репозиторий — здесь

Комментарии (22)

jaha33
04.03.2026 05:52Логика запрета студии не очень понятна. Если из соображений безопасности, то и vscode не то чтобы хорошо. Там всякая телеметрия собирается майкрософтом. И в IDE и в плагинах. Насколько оно полноценно отключаемо, не очень понятно.
Тогда уж используйте vscodium

kenomimi
04.03.2026 05:52Логика запрета студии не очень понятна.
Скорее всего, запретом неявно заставляют уходить с винды, прекращать разработку под нее. А вообще такие требования ИБ почти всегда опираются на приказы трехбуквенных ведомств. Если посмотреть в документы предприятия, то там всегда стоят ссылки на источник.

Kandimus
04.03.2026 05:52Идеальное решение это Notepad++.
Ну а если отбросить шутки, то запрет на райдер и студию конечно очень странный. Тем более, что на уровне предприятия легко обрубается телеметрия или же поднимается корпоративный РКН. Тут, у автора, видимо речь идет о демонстративных успехах по искоренению буржуинского софта из государственной сферы... Что ж, можем только посочувствовать автору. Но + в карму за плагин.
AccountForHabr
04.03.2026 05:52Notepad++ скорее всего тоже нельзя. В нем есть всякое

Ndochp
04.03.2026 05:52Вот как раз его точно не стоит. Там автор политически окрашенный, и несет эту краску в приложение.

lsoft
04.03.2026 05:52@a-yumashin это расширение опубликовано в visual studio marketplace? был бы рад поставить 5 звезд там.

a-yumashin Автор
04.03.2026 05:52Увы, не смог этого сделать. Для публикации расширения в VS Code Marketplace требуется, как я понял, Publisher Account, который тесно связан с учётной записью в Azure DevOps - и вот эту самую учётку я не смог создать (бан по локации, похоже). Может быть и есть какие-то обходные пути, но я их не нашёл.

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

QtRoS
04.03.2026 05:52Кажется, в статье не раскрыт важный технический момент - расширения предполагается писать на TypeScript насколько я помню, а у Вас почти весь код на C#. Как оно взаимодействует с приложением? Или, например, как рисуется UI - с помощью средств на TS или есть возможность получить нечто вроде DC и рисовать напрямую из C# стандартными средствами по типу System.Drawing? Мне кажется это было бы интересно раскрыть в статье или хотя бы комментарии.

a-yumashin Автор
04.03.2026 05:52Ну, всего в статье не охватишь, не хотелось превращать её в лонгрид, от которого все будут шарахаться :) Кому интересны детали, не изложенные в статье - можно почитать README.md на Гитхабе (их там целых два - "общий" и отдельно про расширение). Но мне несложно ответить на Ваши вопросы и здесь: На C# написан сам дизайнер (скриншот которого вы видите в этой статье), а VSIX-расширение написано на TypeScript (по-другому, собственно, и не получится). Расширение общается с запущенной копией дизайнера через Named Pipes; если же таковой не найдено, то расширение сначала запустит дизайнер через
child_process.spawn(). Насчёт того, как рисуется UI: никакой самодеятельности - всё делегировано стандартным механизмам от Microsoft, используемым, в том числе, и в штатном дизайнере Студии. Каждый WinForms-контрол имеет "дизайнер", назначенный с помощью атрибутаDesignerAttribute. Например, у контролаDateTimePickerэтоSystem.Windows.Forms.Design.DateTimePickerDesigner. Как правило, это наследник типаControlDesigner. Задача этих "дизайнеров" - адаптировать контрол для отображения наDesignSurface. Так что рисованием "по типу System.Drawing" заниматься не приходится. Можно, наверное, написать отдельную статью про то, как тут всё устроено "под капотом". Если это будет интересно многим - то могу заняться; истины "в последней инстанции" не обещаю (т.к. всё делалось, в основном, экспериментальным путём и с помощью изучения исходников), но на глубину собственного понимания - копнуть смогу.

xtraroman
04.03.2026 05:52VS Code задумывали как кросплатформ IDE странно туда тащить WinForms дизайнер. И странно видеть что кто то сегодня использует старичка WinForms.

a-yumashin Автор
04.03.2026 05:52За бугром вон Кобол искоренить не могут до сих пор - вот это действительно старичок! Так что WinForms, можно сказать, ещё пацан. На нём, однако, тоже много чего написано. В первых двух абзацах я описал сценарий, в котором "тащить" WinForms дизайнер в VS Code таки имеет смысл. Не для всех этот сценарий актуален - ну и хорошо, те могут просто пролистать статью :)

xtraroman
04.03.2026 05:52Запретили использовать студию? Какая то надуманная причина делать велосипед. Я бы не взялся. Бегите оттуда)
Винформс был придуман когда размеры экранов у всех были примерно одинаковые. Сегодня вашему приложению может достаться как старенький Моник, так и современный с большой плотностью пикселей.
Design Surface в visual studio имеет много возможностей по расширению. Скорее всего в вашей реализации они сломаны.

rPman
04.03.2026 05:52поддержка масштабирования и разных dpi была добавлена в winforms достаточно давно, но требует .net 5 (это грустно, потому что оно не установлено с ОС по умолчанию, нужно доустанавливать)

onets
04.03.2026 05:52В далеком 2007 я работал в гос учреждении и мы там тоже писали многие проекты на WinForms. Интересно как у них сейчас там с запретами.

Superzoos
04.03.2026 05:52Автору респект, а вот запрет студии это клиника. Я бы уволился сразу из такой дурки
georgeheadson
Я, наверное, скажу какую-то глупость, но вот эти прекрасные люди, которые запретили вам Студию... по-хорошему надо, наверное, у них требовать/спрашивать теперь инструменты для работы, не?
shpaker
А мне понравилось, что vsc зато можно юзать ¯\_(ツ)_/¯
А как выбирают что можно, а что нельзя? Святой рандом?
PS: по содержимому статьи вопросов нет - автор молодец :)
SukhovPro
Аргумент очень просто, vs code это открытый исходный код.
А дальше начинается тупик, если уже и пользоваться этим преимуществом, то копировать репозитарий, проверять исходники, чистить телеметрию, вдруг найдут и собирать из исходников.
А потом возникает вопрос исходников плагинов и их сборки.
Никто не даст на это деньги конечно, но с точки зрения СБ они выполнили свою роль и можно получать премию за искоренение проприетарной VS.
diakin
Так они ведь дадут! А работать-то тебе..