使用 Visual Studio 2022 Designer 支持在 .NET 6+ 中创建 WinForms 自定义控件






4.83/5 (12投票s)
在 .NET 6 中使用智能标签和 UIEditor 创建自定义按钮
引言
这是一个在 .NET6 (Core) 中创建自定义按钮的简短演示。 将添加一个属性,该属性将打开一个空表单,并在 property
字段中写入 string
"test
"。
背景
正如 Klaus Löffelmann 所说,在 .NET Core 中,引入了新的 WinForms 设计器。 我使用他的 示例 编写了这篇文章,因为我找不到任何其他示例,而且很可能在未来会发生变化。 这是一个简化的示例,大部分内容从 Klaus Löffelmann 的示例 中复制/粘贴而来。
Using the Code
此示例使用 Visual Studio 2022 创建,需要四个类库项目和一个 Windows 控件库
- MyButtonControl - 控件实现,如属性、按钮继承
- MyButton.ClientServerProtocol - Windows 控件库,客户端和服务器之间的连接,在 .NET 4.7 和 6 中
- MyButton.Designer.Server - 智能标签实现
- MyButton.Designer.Client - 编辑器的实现、属性的行为,它仍然在 .NET 4.7 中
- MyButton.Package - 创建的控件的包,它必须是最后构建的
为项目 MyButton.ClientServerProtocol、MyButton.Designer.Server 和 MyButton.Designer.Client 安装 NuGet 包 Microsoft.WinForms.Designer.SDK
Install-Package Microsoft.WinForms.Designer.SDK
-Version 1.1.0-prerelease-preview3.22076.5
要进行调试,请附加到进程 DesignToolsServer.exe。 有时,需要清除 NuGet 缓存,尤其是在 MyButton.Designer.Client
发生更改时,如果您只需删除文件夹 C:\Users\userName\.nuget\packages\mybutton.package,就可以专门为这个项目完成此操作。
要测试控件,请首先在 NuGet 中添加包源,如 此处 所示。 然后,通过首先从下拉列表中选择包源来安装 NuGet。
第一部分 - MyButtonControl
- 创建一个新的 .NET 6 类库项目。 将 .csproj 更改为如下所示
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> </PropertyGroup> </Project>
- 添加三个文件
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;
}
}
}
第二部分 - MyButton.ClientServerProtocol
- 添加新的 Windows 控件库,删除
UserControl1
,并将 .CSPROJ 更改为如下所示<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFrameworks>net6.0-windows;net472</TargetFrameworks> <UseWindowsForms>true</UseWindowsForms> <LangVersion>9.0</LangVersion> <Nullable>enable</Nullable> </PropertyGroup> </Project>
在 Visual Studio 中保存并重新加载项目。 - 安装 NuGet 包 Microsoft.WinForms.Designer.SDK
Install-Package Microsoft.WinForms.Designer.SDK -Version 1.1.0-prerelease-preview3.22076.5
- 添加六个文件
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);
}
}
第三部分 - MyButton.Designer.Server
- 创建一个新的 .NET 6 类库项目。 将 .csproj 更改为如下所示
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net6.0-windows</TargetFramework> <UseWindowsForms>true</UseWindowsForms> </PropertyGroup> </Project>
- 安装 NuGet 包 Microsoft.WinForms.Designer.SDK
Install-Package Microsoft.WinForms.Designer.SDK -Version 1.1.0-prerelease-preview3.22076.5
- 添加六个文件
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))
};
}
}
第四部分 - MyButton.Designer.Client
- 创建一个新的 .NET 6 类库项目。 将 .csproj 更改为如下所示
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop"> <PropertyGroup> <TargetFramework>net472</TargetFramework> <UseWindowsForms>true</UseWindowsForms> <LangVersion>9.0</LangVersion> </PropertyGroup> </Project>
- 安装 NuGet 包 Microsoft.WinForms.Designer.SDK
Install-Package Microsoft.WinForms.Designer.SDK -Version 1.1.0-prerelease-preview3.22076.5
- 添加三个文件
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)
)
};
}
}
}
第五部分 - MyButton.Package
- 创建一个新的 .NET 6 类库项目,删除 Class1.cs。 将 .csproj 更改为如下所示
<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>
关注点
请注意,MyButton.Package 必须最后构建
历史
- 2022 年 9 月 6 日:初始版本
- 2023 年 4 月 4 日:修复了错误的格式
- 2023 年 4 月 13 日:构建顺序
- 2023 年 4 月 14 日:文章标题更新