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:
- MyButtonControl - Control implementation like properties, button inheritance
- MyButton.ClientServerProtocol - Windows Control Library, connection between client and server, in both .NET 4.7 and 6
- MyButton.Designer.Server - Smart tag implementation
- MyButton.Designer.Client - Implementation of editor, behaviour of the property, and it is still in .NET 4.7
- 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.
- Create a 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>
- Add three files:
using System.ComponentModel;
using System.Windows.Forms;
namespace MyButtonControl
{
[Designer("MyButtonDesigner"),
ComplexBindingProperties("DataSource")]
public class MyButton : Button
{
public MyType MyProperty { get; set; }
}
}
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;
}
}
}
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;
}
}
}
- 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. - Install NuGet package Microsoft.WinForms.Designer.SDK:
Install-Package Microsoft.WinForms.Designer.SDK
-Version 1.1.0-prerelease-preview3.22076.5
- Add six files:
#if NETFRAMEWORK
namespace System.Diagnostics.CodeAnalysis
{
[System.AttributeUsage(System.AttributeTargets.Field |
System.AttributeTargets.Parameter |
System.AttributeTargets.Property, Inherited = false)]
public class AllowNullAttribute : Attribute
{ }
}
#endif
namespace MyButton.ClientServerProtocol
{
public static class EndpointNames
{
public const string MyButtonViewModel = nameof(MyButtonViewModel);
}
}
namespace MyButton.ClientServerProtocol
{
public static class ViewModelNames
{
public const string MyButtonViewModel = nameof(MyButtonViewModel);
}
}
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);
}
}
}
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);
}
}
}
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);
}
}
- Create a 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>
- Install NuGet package Microsoft.WinForms.Designer.SDK:
Install-Package Microsoft.WinForms.Designer.SDK
-Version 1.1.0-prerelease-preview3.22076.5
- Add six files:
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)
};
}
}
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; }
}
}
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;
}
}
}
}
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!);
}
}
}
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);
}
}
}
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))
};
}
}
- Create a 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>
- Install NuGet package Microsoft.WinForms.Designer.SDK:
Install-Package Microsoft.WinForms.Designer.SDK
-Version 1.1.0-prerelease-preview3.22076.5
- Add three files:
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);
}
}
}
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;
}
}
}
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)
)
};
}
}
}
- Create a 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>
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