Custom Controls for WinForm’s Out-Of-Process Designer

Klaus Loeffelmann

WinForms is a success story that is now in its 20th year. Developers werethrilled from the beginning with how quickly even complex user interfaces couldbe built – often with considerably less coding effort compared to any other UIstack. The popularity of WinForms would not have grown so widely over the yearshowever, if not for two critical success factors: First, the simple way in whichdevelopers can create additional UI Controls with comparatively little effort,including professional design-time support. And secondly the resulting hugeecosystem of WinForms user control libraries for almost every imaginabledomain-specific purpose.

Creating user controls for .NET Core/.NET 5-8 (which we call just .NET fromhere on) or migrating them from .NET Framework works with close to no effort –at least when it is about the runtime functionality. WinForms applicationswritten in .NET can benefit from 3 – 5x performance improvements in manyareas(which again increased in .NET7).Additionally, migrated applications and control libraries profit from much moremodest memory consumption when rendering elements from GDI+. That said, there isone aspect of a custom control or a control library which needs somewhat moreeffort to target the .NET runtime: The new Out-Of-Process WinForms Designer.

The Out-Of-Process WinForms Designer

Until we added support in Visual Studio for .NET Core WinForms applications,there was only a single process, devenv.exe, that both the Visual Studioenvironment and the WinForms application being designed ran within. But .NETFramework and .NET Core can’t both run together within the devenv.exe process,and as a result we had to take part of the designer out of process to support.NET applications. Since .NET Core 3.0 started to support the WinForms .NETRuntime, a new WinForms designer was needed to support .NET applications. Thiswork required a near-complete re-architect of the designer as we responded tothe differences between .NET and the .NET Framework based WinForms designereveryone knows and loves. Developers need to see their Forms in the designerlooking precisely the way it will at runtime (WYSIWYG). Whether it is about aTextBox and its PlaceholderText property, Button and its Command, or thelayout of a form with the desired default font – in order for it to generate theWinForms code which sets up the form or UserControl at runtime inInitializeComponent, the CodeDom Serializer must run in the context of theversion of .NET the project is targeting. And we naturally can’t do that, if theCodeDom serialization is running in the same .NET Framework process as VisualStudio. To stick with the example: In .NET Framework, TextBox does not have aPlaceHolder property (introduced in .NET Core 3.0), and Button is missingthe Command property, which has been introduced in .NET7.The correct version of those .NET types simply cannot be resolved in the .NETFramework process. To solve this, we run the designer out-of-process in a new.NET (Core) process called DesignToolsServer. The DesignToolsServer processruns the same version of .NET and the same bitness (x86, x64 or ARM64) as yourapplication.

To handle the interaction with Visual Studio, we have introduced proxy classes(.NET Framework ObjectProxy) for the components and controls on a form whichare created along with the real components and controls on the form in theDesignToolsServer process. For each component or control on the form, an ObjectProxy is created. While the real controls live in the DesignToolsServer process,the Object Proxy instances live in the client – the Visual Studio process.Object Proxies then talk with their actual .NET counter parts across theprocesses.

You can read more about the details about the concept of the two processes inthis blog postaboutthe state of the WinForms Designer.

For the design-time support, this has consequences. One principle of WinFormscontrols is that each control brings its own Designer: For a TextBox controlthere is TextBoxDesigner class which provides special TextBox design-timesupport. For the ListBox control, there is the ListBoxDesigner class, whichprovides special design-time support for that control, and so on. Design-timesupport often means that there is UI-related functionality which the Designerneeds to provide. For example, when the user picks an item from a list for acontrol’s property in the Property Browser, the control’s designer has toprovide that list as a Type Converter. For more sophisticated scenarios,control designers can also provide custom painting at design time. The handlingof Action Lists, which provide a quick access to special design-timefunctionalities or properties (the feature image of this blog post is such anAction List) is one additional Designer feature. Ultimately, they can alsoprovide real complex UIs, which are modal dialogs, shown in the context ofVisual Studio.

Migrating existing Control Designers with almost no effort

Depending on what type of UI-based Design time functionality your Designerprovides, the coding effort compared to .NET Framework maybe the same or almost thesame. Here is what that means exactly:

  • If your control requires a UI which is based on a type-converter andtherefore, shown in the context of the Property Browser (like Enums or dedicateditems to show in a property grid’s property grid cell ComboBox), your UI willbe supported by the new designer model out of the box.

  • If your control requires a UI, which shows up as part of the control (likecustom painted adorners or Action Lists) at design time, then you would needto write your control library against the WinForms Designer SDK, but you don’tneed to take care about round-tripping data to the Server process. Everythingfrom the Developer’s perspective seems to actually be done server-side, andyou can reuse most of the existing control designer Code.

    Note though, that in those cases you need to target the control Designeragainst the WinForms Designer SDK. That means, you need to reference theMicrosoft.WinForms.Designer.SDK NuGetpackage fromyour control library. In addition, you need to refactor the namespaces fromSystem.ComponentModel.Design to Microsoft.DotNet.DesignTools.Designers,where it applies. Some classes are more finely granulated when it comes tonamespace definitions, so for example the DesignerActionList base class,which is originally located in System.ComponentModel.Design in the runtime,you’d find in Microsoft.DotNet.DesignTools.Designers.Actions. It’s easiestto let Visual Studio do the namespace lookup in those refactoring cases:

Screenshot showing how to look up WinForms Designer-SDK namespaces in Visual Studio

If your control designer however needs to provide a custom Type Editor for aproperty whose type’s value need a more complex user interaction, then there issome additional effort for the different processes, which you need to take intoaccount.

Note: This blog post provides a couple of samples, which you can use as aguidance to either create new .NET based or migrate your existing.NET Framework-based control designers to .NET. Section Two .NET samples, and theWhy describes those scenarios in more detail.

Recap: Type Editors for special control properties

While more simple control designer scenarios like type converters, Action Listsor CodeDom Serializers don’t need any substantial rewrites, TypeEditors,which are responsible for a complex dialog-based UI experience, are a differentbeast altogether. But before we get into that, let’s recap what Type Editors inthe context of WinForms Control Designers are and what purpose they serve.

Technically speaking, a Type Editor is a mechanism which provides the front endfor a user to set the value for a certain control property via some data entrydevice. The simplest one: A string editor for a control’s property of typestring, whose value is entered via the keyboard in the Property Browser.

Entering hexadecimal RGB values for a Background property in the Property Browser

But not all properties are of type string. So, for most of the cases, a typeconverter willextend the functionality of the build-in string editor by providing a mechanismto convert the characters you types to the actual type’s value. For example, thecharacters ‘#202020’ are assumed to be hexadecimal representations for theRGB-color values ‘Red: 32, Green: 32, Blue: 32’ and thus converted to a color ofvery dark gray for a background. Unfortunately, not all properties have ameaningful way to convert to its native value based on a string. Or, it is waymore useful to interact on a visual representation of a type’s value. For aproperty which represents a picture for example, the respective Type Editor mustprovide the UI to select a picture from disk, serialize the picture, store it asa resource or file in the project, and generate the required code to assign thatpicture at runtime.

Let’s see how such a Type Editor works for a .NET Framework control in theIn-Proc-WinForms Designer, and then, why that approach does no longer work inthe context of the out-of-process WinForms Designer.

The Demo scenario – the WinForms TileRepeater Control

In our Template and Samples GitHub Repo, you find a sample WinFormsappfor this scenario. It is a picture browser, which shows the jpeg-Images of afolder in a special way:

Animated gif showing the opening of a folder and then a preview of jpegs of different dog.

In our example app, those are items holding JPeg-image filenames of a folder ondisk. On the UI-side you have a user control which represents the template foreach element of the data source. At runtime, for each element in the data sourceone tile based on this user control template gets created. Each item of the datasource is then data-bound to the instance of that user control, and the usercontrol is responsible for rendering the content. To this end, ourTileRepeater control has an ItemTemplate property. This property determines,which type of an element of the data source results in what user controltemplate type in the UI. And to make this approach even more flexible,TileRepeater also has a SeparatorTemplate property: It determines another typefor the items, which would serve as a visible separator at runtime.

So, the data source list which holds the pictures in the list is not completelyhomogenous. Rather, we find two types of elements in it – but we still can use ageneric list, since the types built an inheritance type hierarchy.

Data source list of picture and separator elements in the Auto tool window of Visual Studio

DISCLAIMER: The TileRepeater control is a demo for the specific .NETWinForms Designer scenario – it lacks a virtual rendering mode. It works finefor up to 100 elements, but more than that drains the available resources toomuch, and a virtual mode would be necessary to show a large number of itemsreliably.

How a Type Editor works in the In-Process WinForms Designer

Here is what happens behind the scenes at design-time when the user sets thevalues for ItemTemplate or SeparatorTemplate. When they determine which ofthe element types in the data source list should lead to what user control type(derived from TileRepeaterTemplate) for the actual rendering container atruntime, this happens behind the scenes.

  1. The user wants to show the Type Editor for the TemplateAssignment type of theItemTemplate property of the TileRepeater control. To that end, they eitheruse a command from the Action List or the Property Browser of Visual Studio(by clicking on the button in the grid for that property).

User calling Type Editor of ItemTemplate property via Action List of the TileRepeater control designer

  1. Since the TemplateAssignment type is annotated with the EditorAttribute,the Type Editor TemplateAssignmentEditor is found and instantiated. Sinceits GetEditStyle() method returns UITypeEditorEditStyle.Modal, theWinForms Designer knows that this Type Editor as a modal WinForms Dialog. TheDesigner calls the EditValue method and passes the type descriptor context,the service provider and the actual value to edit. The type descriptor contextprovides information about the context in which the Type Editor has beencalled, e.g. if that has been the Property Browser or an Action List. TheService Provider allows the Editor to acquire required Visual Studio services – for example the WindowsFormsEditorService to show the actual Dialog in thecontext of Visual Studio.

Class code annotated with the EditorAttribute

  1. The UI of the Type Editor is controlled by a ViewModel(TemplateAssignmentViewModel). That means that code for displaying the UIelements and the code for figuring out what to display are separated fromeach other. This is the recommended architectural practice. The Dialog UI(TemplateAssignmentDialog) is then instantiated and passed the ViewModel.

  2. The Type Editor dialog is shown on the screen by passing it to theShowDialog method of Visual Studio’s editor service. In that dialog, theuser can now pick the two types which needs to be assigned to each other:

The Type Editor of the TileRepeater control allowing to assign data source item and user control template.

  1. When the user commits the new setting with OK, the dialog calls theOKClick method in the ViewModel to actually create the new value for theproperty in its TemplateAssignment property. The code flow returns to theEditValue method, where the new property value is taken from thatTemplateAssignment property and returned to the Designer, which theneventually assigned the new value to the TileRepeater control.

The challenge of Type Editors in the Out-Of-Process Designer

To make the TileRepeater control work at design time in the new out-of-processDesigner, we need to add the support for the process separation. When we want toshow the Type Editor as a modal Dialog, which allows the user to pick the typesfor the data template assignment, we have to deal with the different processes:The Visual Studio Process runs in .NET Framework. But the actual control, forwhich we are showing the custom UI, runs in the dedicated .NET server process.If your WinForms project using the control, is targeting .NET Core 6.0, thenthat process runs .NET Core 6.0. If your target is 7.0, the Server Process runsagainst .NET 7.0, and so on. That is necessary, because you need and use typesonly the specific version of .NET knows about. The Visual Studio and .NETFramework-based client process is simply not able to discover or handle those.NET types. From that fact arises the real challenge: Since the controldesigner’s dialogs are running in the context of .NET Framework, it cannotsimply search for the types (in our example neither both the items to bind andthe resulting UserControl types) it is supposed to offer the user in thatdialog. Rather, the .NET Framework part of the Type Editor needs to ask the .NETProcess for those types and then use transport classes to get those typescross-process back to the .NET Framework based Visual Studio process. It can nowprocess the user’s input and send the results back to the server process. Andyes, that is a necessary breaking change from the previous .NET Framework-onlycontrol designers, and it involves some refactoring of the design time code. Butthis refactoring is needed only if there is an actual UI which needs to beshown on top of the UI that is presented in the context of your actual control,as we already discussed in the section [Control Designers with almost nomigration effort] (#control-designers-with-almost-no-migration-effort).

  • If you have custom Type Editors, which are displaying dedicated modal dialogs,then there is some rewriting effort involved for round-tripping the requireddata between the two processes.

  • If you have Type Editors which are derived from existing Type Editors (likeColorEditor or FileNameEditor) for editing certain types of values forexisting controls in .NET Framework, then you also need the client/serverapproach. That said, your control designer solution most probably doesn’t needto have additional communication code to exchange data between the server andthe client process in those cases. As long as you do not change the type theoriginal editor is handling, the Designer handles the necessary communicationbehind the covers. But: That communication is still required to happen, and themodified (inherited) editor types still need to be run in the context of VisualStudio – which at this time is either the 32-Bit .NET Framework process (VS 2019) orthe 64-bit .NET Framework process (VS 2022).

  • If you however just use the editors (which again need to be provided by theclient process), a server-only control designer suffices. In that case though,you need to state the types in the required attributes as strings, and cannotuse typeof (in C#) or GetType (in VB). It would look something like this:

[Editor("System.Windows.Forms.Design.FileNameEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a",
        "System.Drawing.Design.UITypeEditor, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a")]
public string? Filename { get; set; }

Two .NET samples, and the Why

The example that you’ve seen earlier showed the approach of controldesigner functionality and Type Editors in .NET Framework. When we either portthose designers from .NET Framework to .NET, or develop new control designersfor .NET altogether, we’ve already discussed that different controls designersmight need different kind of UIs. When you use only stock Type Editors editors,or you just need your control designers to provide custom Adorner painting,Action Lists or custom CodeDOM serializers, a server-only version for theDesigner will do, and which is pretty much the way, you’ve developed in control designers in .NETFramework. Only if your Control or your Control Library needs custom UI TypeEditors, then you need a Client/Server-solution for your Control library.

That is the reason, the sample folder for this blog post contains yet anotherversion of the sample:

We won’t got to much into this sample – it’s pretty much self-explanatory as theonly difference to a .NET Framework version is that it targets the WinFormsDesigner SDK instead of the APIs in System.ComponentModel of the runtime asexplained in section [Existing Control Designers with almost no migrationeffort] (#existing-control-designers-with-almost-no-migration-effort).

The second sample, however, is the full TileRepeater control version portedfrom .NET Framework to .NET. Before we take a look at that solution, let’s learnmore about about the differences for Type Editors between In-Process andOut-Of-Process WinForms Designer, and what the easiest way is to port a TypeEditor for a .NET Framework Control (or control library) to .NET – or start authoringa .NET Control Library from scratch.

Type Editor templates for creating the out-of-process Designer Projects

A Type Editor for the out-of-process WinForms Designer needs to be built fromseveral projects. The section Using the Type EditorTemplate gives all the necessary backgroundinformation in detail. What’s important for now: The Type Editor SolutionTemplates are based on two sample solutions, one in C# and one in Visual Basic.These solutions are located in the path

.\\winforms-designer-extensibility\\Templates\\TypeEditor\\src\\TemplateSolutions*.

Overview of the projects in their solution folders divisions

The actual Solution Templates (which are NuGet packages) are built from these solutions by a batch file (more about that below).

These solutions provide:

  • A WinForms .NET 6 custom control project named CustomControlLibrary whichholds the actual custom control. The custom control’s only purpose is torender the content of a composite type named CustomPropertyStore, which isjust the composition of a bunch of properties of different types.

Custom Control of the Type Editor template in WinForms Designer, showing the expanded action list

  • Three projects which make up the control’s designer:

    • The CustomControlLibrary.Client project which targets the same .NETFramework version as Visual Studio (in the sample .NET Framework 4.7.2). Itholds the actual WinForms Type Editor, the Type Editor’s UI, and theclient-side ViewModel.
    • The CustomControlLibrary.Server project, which targets .NET 6. It holdsthe actual control designer, along with a custom CodeDom serializer whichdetermines the necessary steps to generate custom property code forInitializeComponent. The project also contains a designer action list implementation for thecontrol (see screenshot above). Finally, it includes a couple of methods in theserver-side ViewModel, which are called by the client to control aspects ofthe UI.
    • The CustomControlLibrary.Protocol project which holds all the classeswhich are necessary to handle the communication between the client and theserver process.
  • A Package project named CustomControlLibrary.Package, which packs thebinaries of all those projects in a special structure as a NuGet project, sothey can be loaded by the WinForms Designer in the individual client andserver processes.

  • A .NET 6 WinForms project named CustomTypeEditorTest to test the control andits design-time functionality.

The procedure for building the actual templates from the template solutions isas follows:

  • Make sure the template solutions work as planned if you’ve made any modifications.
  • In the command line, change the current directory to Templates.
  • Run the prepareTemplates.bat batch file.

This copies the relevant project files from the template solution to thetemplates folder. The batch file then calls dotnet pack to create the solutiontemplate package and also installs the package with dotnet new install. Youshould see the result of that operation:

D:\Git\winforms-designer-extensibility\Templates\TypeEditor\src\Templates>dotnet pack
MSBuild version 17.4.0-preview-22470-08+6521b1591 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
C:\Program Files\dotnet\sdk\7.0.100-rc.2.22477.23\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.RuntimeIdentifierInference.targets(257,5): message NETSDK1057: You are using a preview version of .NET. See: https://aka.ms/dotnet-support-policy [D:\Git\winforms-designer-extensibility\Templates\TypeEditor\src\Templates\CustomTypeEditorTemplatePack.csproj]
  CustomTypeEditorTemplatePack -> D:\Git\winforms-designer-extensibility\Templates\TypeEditor\src\Templates\bin\Debug\netstandard2.0\CustomTypeEditorTemplatePack.dll
  Successfully created package 'D:\Git\winforms-designer-extensibility\Templates\TypeEditor\src\Templates\bin\Debug\Microsoft.WinForms.Designer.TypeEditorTemplate.1.1.0-prerelease-preview3.nupkg'.

D:\Git\winforms-designer-extensibility\Templates\TypeEditor\src\Templates>dotnet new install .\bin\Debug\Microsoft.WinForms.Designer.TypeEditorTemplate.1.1.0-prerelease-preview3.nupkg
The following template packages will be installed:
   D:\Git\winforms-designer-extensibility\Templates\TypeEditor\src\Templates\bin\Debug\Microsoft.WinForms.Designer.TypeEditorTemplate.1.1.0-prerelease-preview3.nupkg

Success: Microsoft.WinForms.Designer.TypeEditorTemplate::1.1.0-prerelease-preview3 installed the following templates:
Template Name                   Short Name          Language  Tags
------------------------------  ------------------  --------  ---------------------------------------------------------
WinForms .NET Custom Type E...  WinFormsTypeEditor  [C#],VB   WinForms/Designer/TypeEditor/ActionList/CodeDomSerializer

Using the Type Editor Template

After building the templates they are ready to use from the CLI as well as fromVisual Studio.

Creating a new Type Editor Solution within Visual Studio

  • Start Visual Studio, and click on Create a new Project.
  • In the New-Project-Dialog, type “winforms” in the filter textbox.
  • Pick one of the newly available Type Editor templates, either for C# orVisual Basic.

The VS new project dialog showing the Type Editor templates

  • Click Next.

  • In the Configure your Project page, specify the following options:

    • Project name: This will become the base name of the Project.
    • Location: This is the path where the solution and the respectiveprojects will be created.
    • Solution: You can choose here if the projects should be created anew oradded to an existing solution.
    • Solution name: The name of the solution.
  • Click Next.

  • On the Additional Information page, specify the following options:

    • Framework: Pick the .NET Version you want the server components (controllibrary, control designer project) targeted against. NOTE: At this pointthe client projects will always target .NET Framework 4.7.2, as this is theVisual Studio target framework version. The Type Editor templates support.NET Versions from 6.0 on.
    • PropertyTypeName: This is the name of the individual custom property theType Editor will offer the editing functionality for. In the sample projectthe templates are based on, this is the CustomPropertyStore type. Everyreference to this type name or file name will be renamed to the class nameyou enter here.
    • Type Editor Name: This is the name of the Type Editor. In the sampleproject the templates are based on, this is the CustomTypeEditor type.Every reference to this type name or file name will be renamed to the classname you enter here.
    • CustomControlName: This is the name of the custom control. In the sampleproject the templates are based on, this is the CustomControl type. Andagain, every reference to this type name or file name will be renamed to theclass name you’re entering here.
  • Click Create to create the solution.

Creating a new Type Editor Solution from the dotnet CLI

After installing the templates, you use the Type Editor solution templateslike every other Visual Basic or C# templates from the CLI. Refer to the helpoption for the exact parameter names. The parameters are the same as in theadditional options description above.

Projects which the templates create

Setting all the aforementioned projects up manually means coordinating a lot ofmoving parts, and there is some potential that things go wrong. The individualprojects created by this template help to prevent falling into those traps. Thetemplates create a series of projects and important solution folders, dependingon your needs for both C# and Visual Basic. Let’s look at the projects whichare part of the template solution in detail:

  • _Solution Items: This is not really a project, but rather a solutionfolder, which holds the readme, the Directory.Build target which determinesthe NuGet package version for the WinForms Designer Extensibility SDK versionused, and the NuGet.config setting. If at any point you need to changethe Designer SDK version which is used throughout the solution, you would onlyneed to change them in this one spot.

  • CustomControlLibrary.Client This is a project of the same target frameworkversion as Visual Studio, and it holds the actual Type Editor UI running inthe context of Visual Studio. It also contains the client ViewModel, whichis a UI controller class. There are actually two ViewModel versions. One inthe client, and one in the server. Only the latter has access to the actualserver types, while only the client one has direct access to the UI – that’swhy both are needed. Both are communicating with each other, so that theclient ViewModel can control the UI based on that.

  • CustomControlLibrary.Server: This project holds every aspect of thecontrol designer, which needs to be executed in the context of the serverprocess. Those are:

    • The server-side ViewModel, which provides the necessary data to theclient-side ViewModel.
    • The factory class, which generates the server-side ViewModel.
    • A custom CodeDom serializer for the custom property type the Type Editor ishandling, should one be needed.
    • A custom designer action list which can be accessed at design time throughthe designer action glyph of the control. Please note, that although theseclasses are hosted purely in the server-side designer assembly, the UI forthe respective action list is still shown in the context of Visual Studio.The necessary communication with the client is done completely behind thescenes by the out-of-process WinForms Designer.
    • The actual control designer, which among other things, paints the adornersfor the controls. Although it looks like this rendering is done in thecontext of Visual Studio, it is not. The rendering of the Form and all itscomponents at design time is done by the DesignToolsServer process andprojected on the client-area of Visual Studio Design surface. Althoughrendered on the server-side, there is no direct interaction with the messagequeue of the server. Every keyboard- and mouse-input is still received inthe Visual Studio-based client process, and then communicated to the server.This is the key to how the WinForms Designer ensures that no deadlocks canoccur due to competing message queues of different processes.
  • CustomControlLibrary.Protocol: This project contains all the classeswhich are necessary for the communication between the client and the serverprocess via JSON-RPC. The Designer SDK providesa series of base classes which are optimized for transferringWinForms-typical data types between the client- and the server-process. Atypical protocol library for a control designer builds on those classes.

  • CustomControlLibrary: This is the project, which contains your actualcustom control(s).

  • CustomControlLibrary.Package: This is the project which creates thecontrol library’s NuGet package. This NuGet package organizes the individualcontrol designer components for the DesignToolsServer process and the VisualStudio client process in respective folders, so that the required parts areavailable for the processes at design time.

Invoking Type Editors, In-Process vs. Out-Of-Process

The differences between the in-process and the out-of-process WinForms Designerare very fundamental. So, especially when you need to migrate a classic TypeEditor to work in the out-of-process Designer, understanding where and how tomake the necessary adjustments is paramount.

In the in-process WinForms Designer the invoking of a Type Editor in .NETFramework is a straightforward procedure, and from the .NET Framework version ofthe TileRepeater project, you already know how Type Editors are getting invokedin the In-Proc-WinForms Designer.

Type Editors in the Out-of-Process WinForms Designer

Here now is the all-important difference compared to the in-process Designerscenario: When the Property Browser asks the Type Editor to display the visualrepresentation of the value, that value’s type is not available in the contextof Visual Studio. The Property Browser runs in a process targeting a different.NET version than the process that defines the type. Visual Studio runs against.NET Framework 4.7.2 while the custom control library you are developing is e.g. targeting .NET 7. So, instead of giving the UITypeEditor the control’scustom/special property’s value directly, it’s handing it via the Object Proxy.

The concept of Object Proxies in the client (Visual Studio) process does requirea special infrastructure for handling user inputs in custom Type Editors. Let’slook at what infrastructure components of the Designer we need to understand,before we talk about the workflow for setting the value in the out-of-processscenario:

  • Using ViewModels: ViewModels are for controlling aspects of a UI withouthaving a direct reference to the UI specific components – we have seen thatconcept already in the .NET Framework sample. Don’t confuseview models we use here with ViewModels you might know from XAML languages:While they are also controlling the UI without having any directdependencies on the UI technology, in contrast to XAML, they are not doingthis by direct data binding. Here, they are used to synchronize certainaspects of the UI between the client and the server process. The classCustomTypeEditorVMClient provides a static method Create, which is thededicated way to create a ViewModel. You pass the service provider and alsothe Object Proxy representing the instance of the property’s value, whichthe client-side Type Editor just got from the Property Browser, to edit tothe Create method.

  • Sessions and the DesignToolsClient: For the communication with theDesignToolsServer process the Designer needs a sending and a receivingendpoint. The DesignToolsClient class represents the client-side sendingendpoint and provides the basic mechanisms for communication with theserver. To separate the concerns of each WinForms document within VisualStudio which has been opened, each designer document is associated with asession and its related session ID. The Create method in the sampledemonstrates how to retrieve a session and the DesignToolsClient throughthe Service Provider, and use those objects how to subsequently talk to theserver – in this case to create the respective server-side ViewModel.

  • Object Proxy classes: These classes solve the basic challenge ofrepresenting objects of server-side .NET version types which are not knownto the .NET Framework based client. If you select a component in theDesigner, what the property browser “sees” is an Object Proxy whichrepresents the real object in the server process. A value of a property of acomplex type is also represented by a proxy object, since – again – its typeonly exists on the server, because it’s targeting a different .NET version.And remember: the server-side ViewModel returned from the server isnot the actual ViewModel instance, but rather its corresponding ObjectProxy.

  • Data transport and remote procedure calls: The communication betweenclient and server is always synchronous, in other words, blocking. You defineendpoints in the server-process, which the client calls. The client waitsuntil the server has finished processing those remote procedure calls.Basically, each endpoint needs three dedicated classes:

    • A Request class, defined in the Protocol project (see below), whichtransports necessary data to the DesignToolsServer.
    • A Response class, which transports result data back to the client process– also defined in the Protocol project.
    • A Handler class, which is the server-side remote-procedure to call, if youwill. This class is defined in the Server-Project, since it will most likely use the actual Control types.

In the Type Editor solution template, two endpoints are already predefined:CreateCustomTypeEditorVM creates the server-side ViewModel, whose instance isthen hosted as a proxy-object in the client-side ViewModel. The communicationand data exchange can be simplified over those two instances. And then there isalso the TypeEditorOKClick endpoint: This method is called when the userclicked the OK button of the Type Editor’s dialog during design time toindicate that they finished changing the value passed by the Property Browser.Since the custom property type only exists in the DesignToolsServer, the clientcan only pass over the individual data fragments from what the user entered inthe dialog to the server process. But it is the server which then creates theactual instance of the value passed from the client. And iteventually assigns that value to the property of the user control.

Now, with these important basics in mind, here is the internal workflow for setting aproperty value via a Type Editor in the out-of-process Designer scenario indetail:

  1. As in the classic in-process-Scenario, the user wants to set a value for acustom property. And again, a Type Editor for that property type is defined bythe EditorAttribute (see class CustomPropertyStore in the template project).The first important difference: Since the type in question might not beavailable in the client process’ target framework, the type can only be definedas a string. Also as before, the custom Type Editor class is instantiated, whenthe user clicks on the button in the property’s cell of the Property Browser.Now, here is a first exciting challenge that the modern WinForms control developer faces: When thecustom control lives only in the server process, and the actual Type Editorlives only in the client, how does the WinForms Designer finds the Type Editoron the client side? This is where an important component in the client designerproject comes into play: the TypeRoutingProvider. It holds a table ofTypeRoutingDefinition objects and assigns the names of the editors to theactual types. That means, if you were ever to add additional Type Editors forother property types or controls to your control library solution, this tablemust be maintained accordingly. It’s best practice to use the EditorNamesdefinitions in the Protocol project to that end, since it minimizes typos byproviding IntelliSense support.

  2. The Property Browser now calls the EditValue method of the Type Editor andpasses the value of the property to set. But, again, the value is not theactual value of the property. Instead, it is the Object Proxy, which points tothe instance of the property type in the server process. This alsomeans the processing of the value must happen in the server-process. To thisend, the two ViewModel types to control the edit procedure need now to beused: one on the client side (CustomTypeEditorVMClient), and one on theserver side (CustomTypeEditorVM). The template creates both classes for you,along with the infrastructure methods to set them up.

  3. The static Create method in the client ViewModel has now all theinformation to create the actual ViewModel instance and to that end, it cancall the CreateViewModelClient method of the Designer service provider.

  4. The Type Editor’s main task is to edit the value of typeCustomPropertyStore. To keep the example simple, this is just a compositetype, composed of a string, a DateTime, a list of string elements and acustom Enum. As a reminder: since this type only exists server-side, the UI(living in the context of Visual Studio) cannot use this type. This is wherethe Protocol project/assembly comes into play. The Protocol project definesall the transport classes, which can be used in either process. It is definedas a .NET Standard 2.0 library, so all its types can be projected and used inboth .NET and .NET Framework projects. We mirror the CustomPropertyStoretype with a special data class we define in the Protocol project namedCustomPropertyStoreData, so that it becomes available on both sides. Thistype also provides the necessary methods to convert the data it’s hosting intothe JSON format and back from it, which is needed to transport it across theprocess’s boundaries. The response class for the endpoint to create theserver-side ViewModel not only takes the proxy of the server-side ViewModel,but also the original values of the types which the custom property type iscomposed of. And this data we now use to populate the Type Editor’s dialogclient side.

  5. The user now edits the values.

  6. When the user clicks OK, we validate the data on the client inside theCustomTypeEditorDialog. If that validation passes, the dialog returnsDialogResult.OK, and we call the ExecuteOKCommand method of the clientViewModel to kick of the data transfer to the server. This method now sendsthe CustomTypeEditorOKClickRequest to the server passing the individualretrieved data from the user’s input of the dialog along. The endpoint’shandler gets those data and passes – in turn – that data to the server-sideViewModel. That again calls its OnClick method, composes the actual instanceof the custom control’s property type, and stores it in the PropertyStoreproperty of the server-side ViewModel. And with that, the call chain seems tobe finished. So, the server-side ViewModel now holds the edited and committedresult. One question remains: How does the ViewModel property find the wayback to the control’s property, which we see in the Designer and – through theObjectProxy – reflected in the property browser? That last step is doneclient-side, and it’s kind of subtle. Remember? When the client-side viewmodel got created, it not only triggered the creation of the server-side viewmodel. It also requested the proxy of that ViewModel to be returned to theclient side. On the client, the client-side ViewModel holds the reference tothe server-side ViewModel’s PropertyStore property over a proxy object. Whenthe user clicks OK in the editor, that code flow is returned to the TypeEditor (running in the context of Visual Studio), which had opened the modaldialog to begin with. Now, back in the actual Type Editor class, it is wherethe assignment from this ViewModel to the actual property of the controlhappens:

    var dialogResult = editorService.ShowDialog(_customTypeEditorDialog);
    if (dialogResult == DialogResult.OK)
    {
        // By now, the UI of the Editor has asked its (client-side) ViewModel
        // to run the code which updates the property value. It passes the data to
        // the server, which in turn updates the server-side ViewModel.
        // When it's time to return the value from the client-side ViewModel back to the
        // Property Browser (which has called the Type Editor in the first place), the client-side
        // ViewModel accesses its PropertyStore property, which in turn gets the required PropertyStore
        // proxy object directly from the server-side ViewModel.
        value = viewModelClient.PropertyStore;
    }

The PropertyStore property of the ViewModelClient doesn’t have a dedicatedbacking field to hold the value. Rather, it uses the infrastructure of the proxyto communicate with the server-side ViewModel to get the just created proxy ofthe server-side ViewModel’s PropertyStore content directly. And the proxyobject is what we need here. Again, since the client doesn’t know the type, itcan only deal with the proxy objects which point and represent the server typesinstead.

Migrating the TileRepeater sample from .NET Framework to .NET

With the knowledge of how the Type Editor (which is created by the solutiontemplate) works, the migration of a Type Editor from .NET Framework to .NET is astraight forward process, despite the fact that there is some effort needed towrite the respective boiler-plate code.

The principle approach applied to move a .NET Framework control designer is this:

  • If you haven’t yet, separate the UI inside your Type Editor(s) from thecontrolling logic by introducing ViewModels. This is important, because thenew WinForms-SDK Type Editor base classes use the concept of ViewModels tocontrol the UI both from the client and the server-side. Separating aUI-Controller of a Type Editor’s Dialog into client and server part is wayeasier, when you already have a UI-independent ViewModel that holds all theUI-logic.

  • Use the solution template to create the necessary projects. For our themigration of our TileRepeater-example, we use TileRepeater as solution name.For entering the project details, we use these settings for naming therespective solution items:

    • TemplateAssignment for PropertyTypeName,
    • TemplateAssignmentEditor for TypeEditorName,
    • TileRepeater for CustomControlName,
    • and we Target the solution to either .NET 6 or .NET 7.

Naming the Type Editor solution template elements for the framework-to.net migration

The most important thing in the migration process is to figure out whichcomponents need to go to to what side and why. Obviously, when you’re startingoff with a brand new control designer solution, there are already components inplace, which need to be replaced. This is what you can use as guidance for theirreplacements by your actual components.

Note: The completely migrated solution you also find in the new WinFormsDesigner Extensibility GitHubrepo.You can always check against its solution explorer structure to make sure you“got” all necessary adjustments.

As you know from the previous description of the scenario that the templatescreates, these are the major area that needs rework:

  • The actual Custom Control needs to be replaced. So, everything which is placedin the Controls solution folder of the template generated solution should beaddressed first. That would already enable the TileRepeater control atruntime. We discuss this in Migrating runtimefunctionality.
  • The ViewModel needs to be split into server and client part. We discuss thisin [Migrating the ViewModel to server and client areas](#migrating-the-view-model-to-server-and-client-areas).
  • We need communication classes which serve as proxies for types .NET Frameworkcannot use from the server process. We discuss this in Providing the protocolclasses for communication between client andserver.
  • We need to modify the endpoints so that actions, which get initiated client-side, are ultimately executed in the server process, and then return the respective result back to the client. We discuss this in Implementing endpoints for server-side functionality.

Migrating runtime functionality

In most cases, migrating the actual control from .NET Framework to .NET, orcreating a control in .NET from scratch will not engender additional effort. Onthe contrary: running in .NET means way better performance, more consideratememory consumption and just broader options due to a way bigger .NET runtimeAPI. To migrate the control’s runtime part of our .NET Framework TileRepeaterversion to the solution create by the Type Editor .NET template…

  • We would need to swap out the class code for TemplateAssignment andTileRepeater.
  • In addition to those files, we’d need the classes Tile and TileContent,and we can copy them just over from the .NET Framework solution.
  • Our TileRepeater doesn’t have a custom Enum, so wouldn’t need that code file(TemplateAssignmentEnum) at all, and can delete it.

Showing in solution explorer, which classes to migrate for the control's runtime

Migrating the ViewModel to server and client areas

When looking at the ViewModel, we stumble over the core problem, due to which wehad to take the Designer itself out of the Visual Studio process in the firstplace. Let’s take a look at the following code of the ViewModel in the .NETFramework source version:

using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Linq;

namespace WinForms.Tiles.Designer
{
    internal partial class TemplateAssignmentViewModel 
    {
        private const string SystemNamespace = "System";
        private const string MicrosoftNamespace = "Microsoft";
        private const string AccessibilityNamespace = "Accessibility";

        private ITypeDiscoveryService _typeDiscoveryService;

        private TemplateAssignmentViewModel(IServiceProvider serviceProvider)
        {
            _typeDiscoveryService = (ITypeDiscoveryService)serviceProvider.GetService(typeof(ITypeDiscoveryService));
        }

        .
        .
        .

        private TypeInfoData[] GetTemplateTypelist()
        {
            var types = _typeDiscoveryService.GetTypes(typeof(object), true)
                .Cast<Type>()

        .
        .
        .

            return types.ToArray();
        }
    }
}

The GetTemplateTypeList method of the TemplateAssignmentViewModel classshows how the list of all available types within the target framework is firstdetermined via Visual Studio’s TypeDiscoveryService and then filtered for thetypes we’re interested in. And it is exactly this which would not work for .NET as targetframework: If the ViewModel ran in the context of Visual Studio, then of courseonly those types would be returned from the .NET Framework version that wouldcorrespond to that of Visual Studio. And that would be .NET Framework 4.7.2. Butwe need the type list based on .NET, and therefore the determination of thetypes must be done server-side. So, we need a second ViewModel running in theserver process.

Thus the ViewModel class needs to be split in two parts:

  • The project TileRepeater.Designer.Client will have all the Type Editor classeslike before and the client part of the ViewModel.

  • The project TileRepeater.Designer.Server will have the server-side viewmodel classes. It will also have a server-side Factory of that ViewModel,which is part of the WinForms Designer infrastructure:

Splitting of the .NET Framework ViewModel classes into server/client-version in solution explorer.

The original .NET Framework ViewModel class now needs some refactoring.

  • First of all, it needs to use the inbuilt ViewModel infrastructure of theOut-of-process designer, and that means the client-side ViewModel needs to beinherited from Microsoft.DotNet.DesignTools.Client.Views.ViewModelClient. Italso needs to use a special internal procedure to create the ViewModel, andhas to implement a respective factory to that end. Thirdly, the clientViewModel needs to have access to the server-side ViewModel, which of coursealso must be created. For that reason, the definition and initialization ofthe client ViewModel changes into this:
using Microsoft.DotNet.DesignTools.Client;
using Microsoft.DotNet.DesignTools.Client.Proxies;
using Microsoft.DotNet.DesignTools.Client.Views;
using System;
using WinForms.Tiles.ClientServerProtocol;
using WinForms.Tiles.Designer.Protocol;
using WinForms.Tiles.Designer.Protocol.Endpoints;

namespace WinForms.Tiles.Designer.Client
{
    internal partial class TemplateAssignmentViewModelClient : ViewModelClient
    {
        [ExportViewModelClientFactory(ViewModelNames.TemplateAssignmentViewModel)]
        private class Factory : ViewModelClientFactory<TemplateAssignmentViewModelClient>
        {
            protected override TemplateAssignmentViewModelClient CreateViewModelClient(ObjectProxy? viewModel)
                => new(viewModel);
        }

        private TemplateAssignmentViewModelClient(ObjectProxy? viewModel)
            : base(viewModel)
        {
            if (viewModel is null)
            {
                throw new NullReferenceException(nameof(viewModel));
            }
        }

        /// <summary>
        ///  Creates an instance of this ViewModelClient and initializes it with the ServerTypes 
        ///  from which the Data Sources can be generated.
        /// </summary>
        /// <param name="session">
        ///  The designer session to create the ViewModelClient server side.
        /// </param>
        /// <returns>
        ///  The ViewModelClient for controlling the NewObjectDataSource dialog.
        /// </returns>
        public static TemplateAssignmentViewModelClient Create(
            IServiceProvider provider,
            object? templateAssignmentProxy)
        {
            var session = provider.GetRequiredService<DesignerSession>();
            var client = provider.GetRequiredService<IDesignToolsClient>();

            var createViewModelEndpointSender = client.Protocol.GetEndpoint<CreateTemplateAssignmentViewModelEndpoint>().GetSender(client);

            var response = createViewModelEndpointSender.SendRequest(new CreateTemplateAssignmentViewModelRequest(session.Id, templateAssignmentProxy));
            var viewModel = (ObjectProxy)response.ViewModel!;

            var clientViewModel = provider.CreateViewModelClient<TemplateAssignmentViewModelClient>(viewModel);
            clientViewModel.Initialize(response.TemplateServerTypes, response.TileServerTypes);

            return clientViewModel;
        }

        private void Initialize(TypeInfoData[] templateServerTypes, TypeInfoData[] tileServerTypes)
        {
            TemplateServerTypes = templateServerTypes;
            TileServerTypes = tileServerTypes;
        }

There are a series of changes to observe, which always need to become part of the migration effort for a Type Editor.

Note: It’s important at this point, that you are comfortable with thefundamentals of ViewModels, sessions and proxy classes in thecontext of the Out-of-process Designer. You may review the section Type Editorsin the Out-of-Process WinFormsDesigner for moredetails.

  • Since the Designer must be notified that there is now a new client-sideViewModel, the correlating factory class Factory needs to be a) implemented,b) derived from ViewModelClientFactory<TemplateAssignmentViewModelClient>and c) annotated with theExportViewModelClientFactory(ViewModelNames.TemplateAssignmentViewModel)attribute.

  • The constructor of our client-side ViewModel class is private. The reason forthis is: Only the factory class should be able to create it. The constructoralso takes an argument of type ObjectProxy, and that is the proxy class ofthe server-side ViewModel. To get that, the constructor of our factory classcalls the static method Create of our client-side ViewModel which initiatesthe following:

    • It gets a DesignerSession and the DesignToolsClient to be able tocommunicate with the DesignToolsServer.
    • It gets the Endpoint CreateTemplateAssignmentViewModelEndpoint, so that itcan call a method server-side in which the server-side ViewModel will becreated.
    • In the CreateTemplateAssignmentViewModelResponse class, that ViewModelproxy is then returned, and can be used to create the actual client sideViewModel.

Now, obviously, the types that we need to display in the Dialog cannot be theactual Type class instances. And, as mentioned before, the reason is theclient-side ViewModel runs against .NET Framework 4.7.2 while the types it needsto display are .NET types.

For that reason, we also need to have the server-side ViewModel, which looks like this:

using Microsoft.DotNet.DesignTools.ViewModels;
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using WinForms.Tiles.ClientServerProtocol;
using WinForms.Tiles.Designer.Protocol.Endpoints;

namespace WinForms.Tiles.Designer.Server
{
    internal partial class TemplateAssignmentViewModel : ViewModel
    {
        private ITypeDiscoveryService? _typeResolutionService;

        private const string SystemNamespace = "System";
        private const string MicrosoftNamespace = "Microsoft";
        private const string AccessibilityNamespace = "Accessibility";

        private static readonly Type s_tileContentType = typeof(TileContent);
        private static readonly string[] s_systemAssembliesFilter = new[]
        {
            AccessibilityNamespace,
            $"{SystemNamespace}.",
            $"{MicrosoftNamespace}."
        };

        public TemplateAssignmentViewModel(IServiceProvider provider)
            : base(provider)
        {
        }

        public CreateTemplateAssignmentViewModelResponse Initialize(object templateAssignment)
        {
            // Here in the Server process, we first get the list of potential template types...
            TemplateTypeList = GetTemplateTypelist();

            // ...and then every type which is derived from 'Tile'.
            TileContentTypeList = GetTileTypeList();

            this.TemplateAssignment = (TemplateAssignment)templateAssignment;

            return new CreateTemplateAssignmentViewModelResponse(this, TemplateTypeList, TileContentTypeList);
        }

        private TypeInfoData[] GetTemplateTypelist()
        {
            _typeResolutionService ??= GetRequiredService<ITypeDiscoveryService>();

            var types = _typeResolutionService.GetTypes(typeof(object), true)
                .Cast<Type>()

            .
            .
            .

            return types.ToArray();
        }
        private TypeInfoData[] GetTileTypeList()
        {
            _typeResolutionService ??= GetRequiredService<ITypeDiscoveryService>();

            .
            .
            .

            return types.ToArray();
        }

        // When we reach this, TemplateQualifiedTypename as well as
        // TileContentQualifiedTypename have been set by the Client-
        // ViewModel (see there).
        internal void OKClick()
        {
            // Create a new Instance of the TemplateAssignment:
            var templateType = Type.GetType(TemplateQualifiedTypename!);
            var tileContentType = Type.GetType(TileContentQualifiedTypename!);
            TemplateAssignment = new(templateType, tileContentType);
        }

        [AllowNull]
        public TypeInfoData[]? TemplateTypeList { get; private set; }

        [AllowNull]
        public TypeInfoData[]? TileContentTypeList { get; private set; }

        public string? TemplateQualifiedTypename { get; set; }
        public string? TileContentQualifiedTypename { get; set; }

        [AllowNull]
        public TemplateAssignment TemplateAssignment { get; set; }
    }
}

You can see that the actual domain-specific code has not changed at all. Theonly thing the server-side ViewModel has in addition to the .NET Frameworkversion is that the public properties providing the type-results-list are nolonger based on the actual type classes, but rather on the data transportclasses, which can be understood by both target framework versions.

So, from that the question derives: Where are those transport classes defined and how can we get the type-information from the server to the client side?

Providing the protocol classes for communication between client and server

This brings us to the point where the protocol classes take the stage. The protocol project contains the entire infrastructure to transfer required information between the two processes via JSON-RPC.

  • It holds transport classes which carry data for those types which can not beused by both target frameworks.
  • It provides the serialization mechanism for those classes from and to JSON.
  • It defines the endpoint definitions and the parameter classes (Request andResponse) for those endpoints.

To stay with our specific TileRepeater example: Instead of presenting the typeinformation as a string representation directly in the UI, the protocol projectprovides two classes named TypeInfoData and TemplateAssignmentItemData thatcontain necessary type information in a way that is compatible for both targetframework versions. By the way: this is also the reason why the protocolproject’s target framework is .NET Standard 2.0. The resulting assemblycontains only types that can be projected to both assembly target frameworkversions of client and server and thus can be used compatibly. Let’s look at theTypeInfoData class:

using Microsoft.DotNet.DesignTools.Protocol.DataPipe;
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;

namespace WinForms.Tiles.ClientServerProtocol
{
    [DebuggerDisplay("{" + nameof(GetDebuggerDisplay) + "(),nq}")]
    public partial class TypeInfoData : IDataPipeObject
    {
        [AllowNull]
        public string AssemblyFullName { get; private set; }

        [AllowNull]
        public string Namespace { get; private set; }

        [AllowNull]
        public string FullName { get; private set; }

        [AllowNull]
        public string AssemblyQualifiedName { get; private set; }

        public bool ImplementsINotifyPropertyChanged { get; private set; }

        [AllowNull]
        public string Name { get; private set; }

        public TypeInfoData()
        {
        }

        public TypeInfoData(
            string assemblyFullName,
            string @namespace,
            string fullName,
            string assemblyQualifiedName,
            bool implementsINotifyPropertyChanged,
            string name)
        {
            AssemblyFullName = assemblyFullName ?? throw new ArgumentNullException(nameof(assemblyFullName));
            Namespace = @namespace ?? throw new ArgumentNullException(nameof(@namespace));
            FullName = fullName ?? throw new ArgumentNullException(nameof(fullName));
            AssemblyQualifiedName = assemblyQualifiedName ?? throw new ArgumentNullException(nameof(assemblyQualifiedName));
            ImplementsINotifyPropertyChanged = implementsINotifyPropertyChanged;
            Name = name ?? throw new ArgumentNullException(nameof(name));
        }

        public static TypeInfoData CreateMissingTypeInfoData(string assemblyQualifiedName, string name)
            => new(
                assemblyFullName: string.Empty,
                @namespace: string.Empty,
                fullName: string.Empty,
                assemblyQualifiedName: assemblyQualifiedName,
                implementsINotifyPropertyChanged: false,
                name: name);

        public void ReadProperties(IDataPipeReader reader)
        {
            AssemblyFullName = reader.ReadString(nameof(AssemblyFullName));
            FullName = reader.ReadString(nameof(FullName));
            AssemblyQualifiedName = reader.ReadString(nameof(AssemblyQualifiedName));
            Namespace = reader.ReadString(nameof(Namespace));
            ImplementsINotifyPropertyChanged = reader.ReadBooleanOrFalse(nameof(ImplementsINotifyPropertyChanged));
            Name = reader.ReadString(nameof(Name));
        }

        public void WriteProperties(IDataPipeWriter writer)
        {
            writer.Write(nameof(AssemblyFullName), AssemblyFullName);
            writer.Write(nameof(FullName), FullName);
            writer.Write(nameof(AssemblyQualifiedName), AssemblyQualifiedName);
            writer.Write(nameof(Namespace), Namespace);
            writer.WriteIfNotFalse(nameof(ImplementsINotifyPropertyChanged), ImplementsINotifyPropertyChanged);
            writer.Write(nameof(Name), Name);
        }

        private string GetDebuggerDisplay()
            => $"{Name}: {Namespace}, {AssemblyFullName} INPC:{(ImplementsINotifyPropertyChanged ? "Yes" : "No")}";
    }
}

When implementing data classes to transport results back and forth from and tothe server-process, keep this in mind:

  • Transport classes need to implement the IDataPipeObject interface. Thisinterface ensures that the necessary methods for converting the content ofthe class from (ReadProperties(IDataPipeReader reader)) and to(WriteProperties(IDataPipeWriter writer)) JSON are implemented. You need tomake sure that the properties are written in the same order as they are beingread, otherwise the deserialization of an object’s content would fail.
  • You can only use data types which are part of .NET Standard 2.1.
  • If property types are not nullable, they should be annotated with theAllowNull attribute (thus allowing the compiler to accept a type to benull for a while, if you will). The reason is that every transport class needsa public, parameterless constructor. Technically, there would be a chance thatthe properties are not initialized. But the Designer takes care of that duringthe deserialization process. Still, you don’t want to make the propertiesnullable just because of that semantic convention.

Note: In some circumstances it is necessary that you display a renderedresult from a control or component, which is only available server-side. In thatcase, you would need to create an endpoint which renders that result into abitmap, and then implement a respective transport class, which would serializeand transfer the content of that bitmap between the processes by byte array.

Implementing endpoints for server-side functionality

When the transport classes are in place, the endpoints need to be implemented.The classes which pass the arguments from and to the endpoint handlers are partof the protocol assembly along with their endpoint definitions. The actualmethods which will be executed server-side are the endpoint handler classes, andbecause they need to have access to the actual control types and the control’sproperty types, they are part of the server-side designer assemblies, whichreference the control library.

  • For creating the server-side ViewModel, which also retrieves the list oftypes at the same time, we have theCreateTemplateAssignmentViewModelEndpoint. The Request and Responseclasses for that endpoint are defined in theTileRepeater.ClientServer.Protocol project.

    • CreateTemplateAssignmentViewModelRequest: This class needs to take thesession ID and the Object Proxy of the actual control, and passes it to theCreateTemplateAssignmentViewModelHandler class in the server.
    • CreateTemplateAssignmentViewModelResponse: This class gets theserver-side ViewModel, the list with template types and the list withTile-user-control types, and returns it to the client ViewModel.

Screenshot of the protocol and server endpoint classes in the solution explorer

  • For notifying the server ViewModel that the OK-Button of the Type Editor wasclicked and the selected Template/Tile-user-control combination now needs tobe committed as the new TemplateAssignment property value, we have theTemplateAssignmentEditorOKClickEndpoint.

    • TemplateAssignmentEditorOKClickRequest: This class just passes theserver-side ViewModel, so the handler knows, which ViewModel instance toexecute the OKClick method on.
    • TemplateAssignmentEditorOKClickResponse: This class doesn’t carry anyadditional parameter. But it must be there to fullfil the requiredconvention.

The actual methods that are getting executed are defined in the server-sideDesigner assembly, which is made from the projectTileRepeater.Designer.Server. They basically call the respective methods inthe server-side ViewModel. Let’s look at the implementation of how theCreateTemplateAssignmentViewModelEndpoint is generating the ViewModel andreturns the list of types back to Visual Studio’s client process (see commentsof listing):

using Microsoft.DotNet.DesignTools.Protocol.Endpoints;
using WinForms.Tiles.Designer.Protocol.Endpoints;

namespace WinForms.Tiles.Designer.Server.Handlers
{
    [ExportRequestHandler(EndpointNames.CreateTemplateAssignmentViewModel)]
    internal class CreateTemplateAssignmentViewModelHandler 
        : RequestHandler<CreateTemplateAssignmentViewModelRequest, CreateTemplateAssignmentViewModelResponse>
    {
        public override CreateTemplateAssignmentViewModelResponse HandleRequest(CreateTemplateAssignmentViewModelRequest request)
        {
            // We need the DesignerHost to have the Designer-infrastructure create our server-side ViewModel:
            // We get the DesignerHost for the session that is correlating to the open design document in VS
            // by the session ID:
            var designerHost = GetDesignerHost(request.SessionId);

            // With the designerHost instance, we're now able to have the server-side ViewModel created.
            var viewModel = CreateViewModel<TemplateAssignmentViewModel>(designerHost);

            // The ViewModel now not only creates the list of types we need to show "on the other side";
            // it also creates the necessary response class, which wraps everything - so with this one line,
            // we're returning the server-side ViewModel (as a proxy) and both of the type lists in one go.
            return viewModel.Initialize(request.TileRepeaterProxy!);
        }
    }
}

NOTE: Whenever you need to create a new Endpoint to execute an endpointhandler on the server-side, keep this checklist in mind:

  • In the protocol project, define the Endpoint, Request and Responseclasses.
  • Make sure, Request and Response class are inherited from those baseclasses.
  • As data transport properties in Request or Response classes, only usetypes which are defined by the .NET Standard 2.0 target framework. If youneed classes specific to your control which only run in the context of theserver target framework, you need create wrapper classes. These wrapper classesalso need to be defined in the project ClientServerProtocol, and mustimplement IDataPipeObject so the content can be serialized to JSON andtransported across the process-boundaries. Alternatively (or in addition),implement results which need to be rendered on the Type Editor UI by providingbitmaps, and transport those as byte-arrays back to the client, where they canconverted back to bitmaps and rendered on the surface of the Type Editordialog.
  • Implement the respective endpoint handler classes, which are representing theactual method to be executed in the context of the server-assembly (with fullaccess to the custom control’s types).
  • Make sure, you annotate each endpoint handler class with theExportRequestHandler(EndpointNames.EndpointName). See the code above for anexample.
  • Make sure to maintain the list of endpoint names in EndpointNames.cs, thelist of editors in EditorNames.cs and the names of the ViewModels inViewModelNames.cs.
  • And last but not least: Make sure the Type Routing for Type Editors isconfigured correctly. How that works is described in the following section.

Setting up the Type Routing where it applies

When you are converting Type Editors from .NET Framework to .NET, there is oneadditional aspect to keep in mind: Usually, the designer host (normally theWinForms In-process Designer) finds Type Editors for types which need adedicated UI by their EditorAttribute. In the out-of-process Designer, this isnot the case. The problem is that the Property Browser needs to examine the typeto detect if it contains the EditorAttribute. But it can’t, since the typeneeding that attribute is not a .NET Framework Type. To overcome this, we needto implement client-side type routing providers for each Type Editor like this:

using Microsoft.DotNet.DesignTools.Client.TypeRouting;
using System.Collections.Generic;
using WinForms.Tiles.Designer.Protocol;

namespace WinForms.Tiles.Designer.Client
{
    [ExportTypeRoutingDefinitionProvider]
    internal class TypeRoutingProvider : TypeRoutingDefinitionProvider
    {
        public override IEnumerable<TypeRoutingDefinition> GetDefinitions()
        {
            return new[]
            {
                new TypeRoutingDefinition(
                    TypeRoutingKinds.Editor, 
                    nameof(EditorNames.TemplateAssignmentEditor), 
                    typeof(TemplateAssignmentEditor)),
            };
        }
    }
}

This is important. If that routing is missing, the client-side Designer processisn’t able to establish the relation to the server-side definition of a type’seditor, and the adherence on the client side won’t take place: The property gridcell would be missing the button to call the type editor, which couldnever be called.

Creating of the Designer/Control Library NuGet package and using it in the Demo App

During Designer startup, Visual Studio’s Designer-Startup-Service checks forout-of-process Control Designer NuGet packages that need to be loaded into thecontext of the two Designer processes. The structure of these NuGet packages isan implementation detail defined by the Type Editors’ Solution Templates. (Ifyou are interested in exactly how this structure looks like, just examine agenerated NuGet more closely: Since a NuGet is basically nothing more than acollection of files in a certain order structure packaged as a ZIP package, youcan simply rename the extension of a NuGet package to .zip, and then see thefolder structure in plain text.)

What’s important to know when you’re developing a control library: It’s likelythat you want to test immediately after a change to a control or its designerfunctionality whether the changes had the desired effect. To do this, you needanother project in the solution that consumes the control library, and you needto make sure of the following:

  • Never reference the control library from your test project directly as aproject reference. Always and only reference the NuGet package.

  • The solution template sets up the generation of the NuGet package in a way soit can be easily detected from a local package source. This definition is donein the solution folder Solution_Items with the file NuGet.Config. It pointsto the folder which will contain the respective next version of the NuGetPackage after a successful build:

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <packageSources>
        <add key="local" value=".\NuGet\BuildOut" />
      </packageSources>
      <disabledPackageSources />
    </configuration>

    It’s important to know in this context that every new build will generate aNuGet package with a new version. And this is important, since your test appwill pick up a new version of the NuGet package, when the version number hasactually changed. If the version number has not changes, the NuGet packagewill not be reloaded, even if the content of the package has changed and thebuild date of the NuGet package is newer.

  • For that reason, it is also important to not use a fixed version of the NuGetpackage during development. You should reference the latest version of theNuGet package in the consuming project, like this:

      <ItemGroup>
        <PackageReference Include="TileRepeater.Package" Version="*" />
      </ItemGroup>
  • And then lastly, please make sure to setup the build dependenciescorrectly:

    • Client- and Server Projects should reference the Protocol library.
    • The Server-project needs to reference the actual Control Library inaddition, since it needs to handle those types in the server-side Designerprocess.
    • The NuGet-Package-Project should have dependencies on all the involvedprojects, so a rebuild of the NuGet package would trigger a rebuild of allthe involved projects.
    • And then your test project which consumes Control Library and its ControlDesigners should only reference the NuGet package, and also shouldn’t haveany further build dependencies. This is important, so that a new versionwill only be pulled in, when you rebuild the NuGet-Package. And you need tobuild NuGet package and test app always only independently of each other.

Final thoughts

Migrating a .NET Framework Control Designer to .NET is an easy and straightforward process in most of the aspects of a Control Designer’s functional areas.That said, this doesn’t necessarily count for Type Editors which need more workto make them integrate in the client-server infrastructure. The template shouldhelp in the process, and as soon as the first type editor of a control librarycan be used successfully, the migration or integration of all following typeeditors for further types of the library is much faster.

As always: Feedback about the subject matter is really important to us, soplease let us know your thoughts, and about your experiences with migratingexisting .NET Framework Control Libraries to .NET or creating new ones fromscratch. Also, we’re interested about what additional topics of this area youwould like hear about from us. Please also note that the WinForms .NET runtimeis open source, and you can contribute! If you have ideas, encountered bugs, oreven want to take on PRs around the WinForms runtime, have a look at theWinForms Github repo. If you havesuggestions around the WinForms Designer, feel free to file new issues there aswell.

Happy coding!