Самописный дизайнер форм (WinForms) для VS Code

в 5:08, , рубрики: .net, C#, DesignSurface, form designer, vscode, vscode extension, WinForms

Хочу поделиться проектом, который может оказаться полезным тем, кто всё ещё разрабатывает/поддерживает десктопные .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.DesignerREADME.md". И есть ряд моментов, проработанных не до конца - см. раздел "Grey area" там же. Но в целом - работает, Студию мне заменяет. Надеюсь, что кому-нибудь тоже окажется полезен. Использовать можно и как standalone приложение, и как VSIX-расширение для среды VS Code(тут рекомендуется ознакомиться с файлом "UiTools.WinForms.DesignerUiTools.WinForms.Designer.VsCodeExtensionREADME.md").

Репозиторий — здесь

Самописный дизайнер форм (WinForms) для VS Code - 1

Автор: a-yumashin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js