Introduction

This is as much as possible short demonstration of creating custom button in .NET6 (core). One property will be added which will open empty form, and write string "test" in the property field. 

Background

As Klaus Löffelmann stated, in .NET core new WinForms designer was introduced. This article I wrote using his example, since I could not find any other example, and most probably in the future will be changed. This is my simplified example, mostly copy/pasted from Klaus Löffelmann's example.

Using the code

This example was made using Visual Studio 2022 and there will be 4 class library projects and one Windows Control Library needed: 

  1. MyButtonControl - here will be control implementation like properties, button inheritance
  2. MyButton.ClientServerProtocol - Windows Control Library, connection between client and server, in both .NET 4.7 and 6.
  3. MyButton.Designer.Server - here will be smart tag implementation
  4. MyButton.Designer.Client - here is the implementation of editor, behaviour of the property, and it is still in .NET 4.7
  5. MyButton.Package - here will be the package of the control created.

Install NuGet package Microsoft.WinForms.Designer.SDK for projects MyButton.ClientServerProtocol, MyButton.Designer.Server and MyButton.Designer.Client:

Install-Package Microsoft.WinForms.Designer.SDK -Version 1.1.0-prerelease-preview3.22076.5

To debug attach to the process DesignToolsServer.exe. Sometimes there is need to clear NuGet cache, specially when there is change in the MyButton.Designer.Client, it can be done specifically for this one project if you just delete the folder C:\Users\userName\.nuget\packages\mybutton.package

To test the control first add package source in NuGet like here explained. Then install NuGet by first choosing package source from drop down list.

First part - MyButtonControl

  1. Create new .NET 6 class library project. Change .csproj to look like:
    <Project Sdk="Microsoft.NET.Sdk">
    	<PropertyGroup>
    		<TargetFramework>net6.0-windows</TargetFramework>
    		<UseWindowsForms>true</UseWindowsForms>
    	</PropertyGroup>
    </Project>
    
  2. Add three files:

MyButton.cs

using System.ComponentModel;
using System.Windows.Forms;

namespace MyButtonControl
{
    [Designer("MyButtonDesigner"),
     ComplexBindingProperties("DataSource")]
    public class MyButton : Button
    {
        public MyType MyProperty { get; set; }
    }
}

MyType.cs

using System.ComponentModel;
using System.Drawing.Design;

namespace MyButtonControl
{
    [TypeConverter(typeof(MyTypeConverter))]
    [Editor("MyButtonEditor", typeof(UITypeEditor))]
    public class MyType
    {
        public string AnotherMyProperty { get; set; }

        public MyType(string value)
        {
            AnotherMyProperty = value;
        }
    }
}

MyTypeConverter.cs

using System;
using System.ComponentModel;
using System.Globalization;

namespace MyButtonControl
{
    internal class MyTypeConverter : TypeConverter
    {
        public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
        {
            return true;
        }

        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            return true;
        }

        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
        {
            if (value is null)
            {
                return string.Empty;
            }
            return new MyType(value.ToString());
        }

        public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
        {
            return ((MyType)value)?.AnotherMyProperty;
        }
    }
}

Second part - MyButton.ClientServerProtocol

  1. Add new Windows Control Library, delete UserControl1, and change .CSPROJ like:
    <Project Sdk="Microsoft.NET.Sdk">
    	<PropertyGroup>
    		<TargetFrameworks>net6.0-windows;net472</TargetFrameworks>
    		<UseWindowsForms>true</UseWindowsForms>
    		<LangVersion>9.0</LangVersion>
    		<Nullable>enable</Nullable>
    	</PropertyGroup>
    </Project>		
    		
    Save and reload the project in Visual Studio
  2. Install NuGet package Microsoft.WinForms.Designer.SDK:
    Install-Package Microsoft.WinForms.Designer.SDK -Version 1.1.0-prerelease-preview3.22076.5
    
  3. Add six files:

AllowNullAttribute.cs

#if NETFRAMEWORK
namespace System.Diagnostics.CodeAnalysis
{
    [System.AttributeUsage(System.AttributeTargets.Field | System.AttributeTargets.Parameter | System.AttributeTargets.Property, Inherited = false)]
    public class AllowNullAttribute : Attribute
    { }
}
#endif

EndpointNames.cs

namespace MyButton.ClientServerProtocol
{
    public static class EndpointNames
    {
        public const string MyButtonViewModel = nameof(MyButtonViewModel);
    }
}

ViewModelNames.cs

namespace MyButton.ClientServerProtocol
{
    public static class ViewModelNames
    {
        public const string MyButtonViewModel = nameof(MyButtonViewModel);
    }
}

MyButtonViewModelRequest.cs

using Microsoft.DotNet.DesignTools.Protocol.DataPipe;
using Microsoft.DotNet.DesignTools.Protocol;
using Microsoft.DotNet.DesignTools.Protocol.Endpoints;
using System;

namespace MyButton.ClientServerProtocol
{
    public class MyButtonViewModelRequest : Request
    {
        public SessionId SessionId { get; private set; }
        public object? MyPropertyEditorProxy { get; private set; }

        public MyButtonViewModelRequest() { }

        public MyButtonViewModelRequest(SessionId sessionId, object? myProxy)
        {
            SessionId = sessionId.IsNull ? throw new ArgumentNullException(nameof(sessionId)) : sessionId;
            MyPropertyEditorProxy = myProxy;
        }

        public MyButtonViewModelRequest(IDataPipeReader reader) : base(reader) { }

        protected override void ReadProperties(IDataPipeReader reader)
        {
            SessionId = reader.ReadSessionId(nameof(SessionId));
            MyPropertyEditorProxy = reader.ReadObject(nameof(MyPropertyEditorProxy));
        }

        protected override void WriteProperties(IDataPipeWriter writer)
        {
            writer.Write(nameof(SessionId), SessionId);
            writer.WriteObject(nameof(MyPropertyEditorProxy), MyPropertyEditorProxy);
        }
    }
}

MyButtonViewModelResponse.cs

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

namespace MyButton.ClientServerProtocol
{
    public class MyButtonViewModelResponse : Response
    {
        [AllowNull]
        public object ViewModel { get; private set; }

        [AllowNull]
        public object MyProperty { get; private set; }

        public MyButtonViewModelResponse() { }

        public MyButtonViewModelResponse(object viewModel, object myProperty)
        {
            ViewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
            MyProperty = myProperty;
        }

        public MyButtonViewModelResponse(object viewModel)
        {
            ViewModel = viewModel ?? throw new ArgumentNullException(nameof(viewModel));
        }

        public MyButtonViewModelResponse(IDataPipeReader reader) : base(reader) { }

        protected override void ReadProperties(IDataPipeReader reader)
        {
            ViewModel = reader.ReadObject(nameof(ViewModel));
        }

        protected override void WriteProperties(IDataPipeWriter writer)
        {
            writer.WriteObject(nameof(ViewModel), ViewModel);
            writer.WriteObject(nameof(MyProperty), MyProperty);
        }
    }
}

MyButtonViewModelEndpoint.cs

using System.Composition;
using Microsoft.DotNet.DesignTools.Protocol.DataPipe;
using Microsoft.DotNet.DesignTools.Protocol.Endpoints;

namespace MyButton.ClientServerProtocol
{
    [Shared]
    [ExportEndpoint]
    public class MyButtonViewModelEndpoint : Endpoint<MyButtonViewModelRequest, MyButtonViewModelResponse>
    {
        public override string Name => EndpointNames.MyButtonViewModel;

        protected override MyButtonViewModelRequest CreateRequest(IDataPipeReader reader)
            => new(reader);

        protected override MyButtonViewModelResponse CreateResponse(IDataPipeReader reader)
            => new(reader);
    }
}

Third part - MyButton.Designer.Server

  1. Create new .NET 6 class library project. Change .csproj to look like:
    <Project Sdk="Microsoft.NET.Sdk">
    	<PropertyGroup>
    		<TargetFramework>net6.0-windows</TargetFramework>
    		<UseWindowsForms>true</UseWindowsForms>
    	</PropertyGroup>
    </Project>
    
  2. Install NuGet package Microsoft.WinForms.Designer.SDK:
    Install-Package Microsoft.WinForms.Designer.SDK -Version 1.1.0-prerelease-preview3.22076.5
    
  3. Add six files:

MyButtonDesigner.cs

using Microsoft.DotNet.DesignTools.Designers;
using Microsoft.DotNet.DesignTools.Designers.Actions;

namespace MyButton.Designer.Server
{
    internal partial class MyButtonDesigner : ControlDesigner
    {
        public override DesignerActionListCollection ActionLists
            => new()
            {
                new ActionList(this)
            };
    }
}

MyButtonViewModel.cs

using Microsoft.DotNet.DesignTools.ViewModels;
using System;
using System.Diagnostics.CodeAnalysis;
using MyButton.ClientServerProtocol;
using MyButtonControl;

namespace MyButton.Designer.Server
{
    internal partial class MyButtonViewModel : ViewModel
    {
        public MyButtonViewModel(IServiceProvider provider) : base(provider)
        {
        }

        public MyButtonViewModelResponse Initialize(object myProperty)
        {
            MyProperty = new MyType(myProperty.ToString());
            return new MyButtonViewModelResponse(this, MyProperty);
        }

        [AllowNull]
        public MyType MyProperty { get; set; }
    }
}

MyButton.ActionList.cs

using Microsoft.DotNet.DesignTools.Designers.Actions;
using System.ComponentModel;
using MyButtonControl;

namespace MyButton.Designer.Server
{
    internal partial class MyButtonDesigner
    {
        private class ActionList : DesignerActionList
        {
            private const string Behavior = nameof(Behavior);
            private const string Data = nameof(Data);

            public ActionList(MyButtonDesigner designer) : base(designer.Component)
            {
            }

            public MyType MyProperty
            {
                get => ((MyButtonControl.MyButton)Component!).MyProperty;

                set =>
                    TypeDescriptor.GetProperties(Component!)[nameof(MyProperty)]!
                        .SetValue(Component, value);
            }

            public override DesignerActionItemCollection GetSortedActionItems()
            {
                DesignerActionItemCollection actionItems = new()
                {
                    new DesignerActionHeaderItem(Behavior),
                    new DesignerActionHeaderItem(Data),
                    new DesignerActionPropertyItem(
                        nameof(MyProperty),
                        "Empty form",
                        Behavior,
                        "Display empty form.")
                };

                return actionItems;
            }
        }
    }
}

MyButtonViewModelHandler.cs

using Microsoft.DotNet.DesignTools.Protocol.Endpoints;
using MyButton.ClientServerProtocol;

namespace MyButton.Designer.Server
{
    [ExportRequestHandler(EndpointNames.MyButtonViewModel)]
    public class MyButtonViewModelHandler : RequestHandler<MyButtonViewModelRequest, MyButtonViewModelResponse>
    {
        public override MyButtonViewModelResponse HandleRequest(MyButtonViewModelRequest request)
        {
            var designerHost = GetDesignerHost(request.SessionId);

            var viewModel = CreateViewModel<MyButtonViewModel>(designerHost);

            return viewModel.Initialize(request.MyPropertyEditorProxy!);
        }
    }
}

MyButtonViewModel.Factory.cs

using Microsoft.DotNet.DesignTools.ViewModels;
using System;
using MyButton.ClientServerProtocol;

namespace MyButton.Designer.Server
{
    internal partial class MyButtonViewModel
    {
        [ExportViewModelFactory(ViewModelNames.MyButtonViewModel)]
        private class Factory : ViewModelFactory<MyButtonViewModel>
        {
            protected override MyButtonViewModel CreateViewModel(IServiceProvider provider)
                => new(provider);
        }
    }
}

TypeRoutingProvider.cs

using Microsoft.DotNet.DesignTools.TypeRouting;
using System.Collections.Generic;

namespace MyButton.Designer.Server
{
    [ExportTypeRoutingDefinitionProvider]
    internal class TypeRoutingProvider : TypeRoutingDefinitionProvider
    {
        public override IEnumerable<TypeRoutingDefinition> GetDefinitions()
            => new[]
            {
                new TypeRoutingDefinition(
                    TypeRoutingKinds.Designer,
                    nameof(MyButtonDesigner),
                    typeof(MyButtonDesigner))
            };
    }
}

Fourth part - MyButton.Designer.Client

  1. Create new .NET 6 class library project. Change .csproj to look like:
    <Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
    	<PropertyGroup>
    		<TargetFramework>net472</TargetFramework>
    		<UseWindowsForms>true</UseWindowsForms>
    		<LangVersion>9.0</LangVersion>
    	</PropertyGroup>
    </Project>
    
  2. Install NuGet package Microsoft.WinForms.Designer.SDK:
    Install-Package Microsoft.WinForms.Designer.SDK -Version 1.1.0-prerelease-preview3.22076.5
    
  3. Add three files:

MyButtonViewModel.cs

using System;
using Microsoft.DotNet.DesignTools.Client.Proxies;
using Microsoft.DotNet.DesignTools.Client;
using Microsoft.DotNet.DesignTools.Client.Views;
using MyButton.ClientServerProtocol;

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

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

        public static MyButtonViewModel Create(
            IServiceProvider provider,
            object? templateAssignmentProxy)
        {
            var session = provider.GetRequiredService<DesignerSession>();
            var client = provider.GetRequiredService<IDesignToolsClient>();

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

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

            var clientViewModel = provider.CreateViewModelClient<MyButtonViewModel>(viewModel);

            return clientViewModel;
        }

        public object? MyProperty
        {
            get => ViewModelProxy?.GetPropertyValue(nameof(MyProperty));
            set => ViewModelProxy?.SetPropertyValue(nameof(MyProperty), value);
        }
    }
}

MyButtonEditor.cs

using System;
using System.ComponentModel;
using System.Drawing.Design;
using System.Windows.Forms;
using System.Windows.Forms.Design;

namespace MyButton.Designer.Client
{
    public class MyButtonEditor : UITypeEditor
    {

        public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
            => UITypeEditorEditStyle.Modal;

        public override object? EditValue(
            ITypeDescriptorContext context,
            IServiceProvider provider,
            object? value)
        {
            if (provider is null)
            {
                return value;
            }


            Form myTestForm;
            myTestForm = new Form();
            var editorService = provider.GetRequiredService<IWindowsFormsEditorService>();
            editorService.ShowDialog(myTestForm);

            MyButtonViewModel viewModelClient = MyButtonViewModel.Create(provider, "test");
            return viewModelClient.MyProperty;
        }

    }
}

TypeRoutingProvider.cs

using Microsoft.DotNet.DesignTools.Client.TypeRouting;
using System.Collections.Generic;

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

Fifth part - MyButton.Package

  1. Create new .NET 6 class library project, delete Class1.cs. Change .csproj to look like:
    <Project Sdk="Microsoft.NET.Sdk">
    	<PropertyGroup>
    		<TargetFramework>net6.0</TargetFramework>
    		<IncludeBuildOutput>false</IncludeBuildOutput>
    		<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
    		<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
    		<TargetsForTfmSpecificContentInPackage>$(TargetsForTfmSpecificContentInPackage);_GetFilesToPackage</TargetsForTfmSpecificContentInPackage>
    		<RunPostBuildEvent>Always</RunPostBuildEvent>
    	</PropertyGroup>
    	<Target Name="_GetFilesToPackage">
    		<ItemGroup>
    			<_File Include="$(SolutionDir)\MyButtonControl\bin\$(Configuration)\net6.0-windows\MyButtonControl.dll"/>
    			<_File Include="$(SolutionDir)\MyButton.Designer.Client\bin\$(Configuration)\net472\MyButton.Designer.Client.dll"
    			       TargetDir="Design/WinForms"/>
    			<_File Include="$(SolutionDir)\MyButton.Designer.Server\bin\$(Configuration)\net6.0-windows\MyButton.Designer.Server.dll"
    			       TargetDir="Design/WinForms/Server"/>
    
    			<_File Include="$(SolutionDir)\MyButton.ClientServerProtocol\bin\$(Configuration)\net472\MyButton.ClientServerProtocol.dll" TargetDir="Design/WinForms" />
    			<_File Include="$(SolutionDir)\MyButton.ClientServerProtocol\bin\$(Configuration)\net6.0-windows\MyButton.ClientServerProtocol.dll" TargetDir="Design/WinForms/Server" />
    		</ItemGroup>
    		<ItemGroup>
    			<TfmSpecificPackageFile Include="@(_File)"
    			                        PackagePath="$(BuildOutputTargetFolder)/$(TargetFramework)/%(_File.TargetDir)"/>
    		</ItemGroup>
    	</Target>
    </Project>