Click here to Skip to main content
15,881,938 members
Articles / Programming Languages / C#

Creating WinForms Custom Controls with Visual Studio 2022 Designer Support in .NET 6+

Rate me:
Please Sign up or sign in to vote.
4.83/5 (12 votes)
13 Apr 2023CPOL3 min read 23K   429   21   21
Making a custom button with smart tag and UIEditor in .NET 6
In this article, you will see a short demo of how to create a custom button in .NET6 Core.

Introduction

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

Background

As Klaus Löffelmann stated, in .NET Core, new WinForms designer was introduced. I wrote this article using his example since I could not find any other example, and most probably will be changed in the future. 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 four class library projects and one Windows Control Library needed:

  1. MyButtonControl - 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 - Smart tag implementation
  4. MyButton.Designer.Client - Implementation of editor, behaviour of the property, and it is still in .NET 4.7
  5. MyButton.Package - Package of the control created, it has to be last builded

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 a need to clear NuGet cache, especially when there is a 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 as explained here. Then install NuGet by first choosing package source from the dropdown list.

First Part - MyButtonControl

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

MyButton.cs

C#
using System.ComponentModel;
using System.Windows.Forms;

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

MyType.cs

C#
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

C#
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:
    XML
    <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

C#
#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

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

ViewModelNames.cs

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

MyButtonViewModelRequest.cs

C#
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

C#
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

C#
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 a new .NET 6 class library project. Change .csproj to look like:
    XML
    <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

C#
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

C#
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

C#
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

C#
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

C#
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

C#
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 a new .NET 6 class library project. Change .csproj to look like:
    XML
    <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

C#
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

C#
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

C#
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 a new .NET 6 class library project, delete Class1.cs. Change .csproj to look like:
    XML
    <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>

Points of Interest

Please notice that MyButton.Package has to be builded last

History

  • 6th September, 2022: Initial version
  • 4th April, 2023: Fixed bad formatting
  • 13th April, 2023: Build order
  • 14th April, 2023: Article title updated

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Germany Germany
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionLoading file into Visual Studios 20222 Pin
Member 1598699224-Apr-23 3:26
Member 1598699224-Apr-23 3:26 
QuestionI'm not able to use the sample project Pin
Cosmico7814-Apr-23 4:17
Cosmico7814-Apr-23 4:17 
AnswerRe: I'm not able to use the sample project Pin
Stanko Milošev14-Apr-23 20:41
Stanko Milošev14-Apr-23 20:41 
GeneralRe: I'm not able to use the sample project Pin
Cosmico7816-Apr-23 23:02
Cosmico7816-Apr-23 23:02 
GeneralRe: I'm not able to use the sample project Pin
Stanko Milošev16-Apr-23 23:13
Stanko Milošev16-Apr-23 23:13 
GeneralRe: I'm not able to use the sample project Pin
Cosmico7816-Apr-23 23:51
Cosmico7816-Apr-23 23:51 
QuestionArticle Title Pin
Graeme_Grant13-Apr-23 0:08
mvaGraeme_Grant13-Apr-23 0:08 
AnswerRe: Article Title Pin
Stanko Milošev13-Apr-23 21:01
Stanko Milošev13-Apr-23 21:01 
GeneralRe: Article Title Pin
Graeme_Grant13-Apr-23 21:23
mvaGraeme_Grant13-Apr-23 21:23 
QuestionBuild Order Pin
Victor_errdt9-Apr-23 19:41
Victor_errdt9-Apr-23 19:41 
AnswerRe: Build Order Pin
Stanko Milošev12-Apr-23 20:22
Stanko Milošev12-Apr-23 20:22 
QuestionBad formatting in the Package projec Pin
Victor_errdt3-Apr-23 14:16
Victor_errdt3-Apr-23 14:16 
AnswerRe: Bad formatting in the Package projec Pin
Stanko Milošev5-Apr-23 8:09
Stanko Milošev5-Apr-23 8:09 
QuestionQuestions Pin
LightTempler6-Sep-22 9:34
LightTempler6-Sep-22 9:34 
AnswerRe: Questions Pin
Stanko Milošev6-Sep-22 9:45
Stanko Milošev6-Sep-22 9:45 
GeneralRe: Questions Pin
LightTempler7-Sep-22 9:11
LightTempler7-Sep-22 9:11 
GeneralRe: Questions Pin
Đỗ Hồng Ngọc7-Sep-22 15:12
professionalĐỗ Hồng Ngọc7-Sep-22 15:12 
GeneralRe: Questions Pin
Stanko Milošev7-Sep-22 20:39
Stanko Milošev7-Sep-22 20:39 
GeneralRe: Questions Pin
LightTempler8-Sep-22 9:51
LightTempler8-Sep-22 9:51 
AnswerRe: Questions Pin
Peter Adam14-Apr-23 12:07
professionalPeter Adam14-Apr-23 12:07 
GeneralThe MS cycle Pin
LightTempler15-Apr-23 12:35
LightTempler15-Apr-23 12:35 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.