Skip to main content

new-control-api

Instructions for adding new public APIs (properties, methods, events, delegates) to existing WinForms controls or components. Covers API issue tracking, PublicAPI file maintenance, property/event conventions, CodeDOM serialization, design-time attributes, and XML documentation.

소스 정보

저장소
dotnet/winforms
최근 소스 활동
2026년 7월 24일 08:39
감지된 SKILL.md 언어
영어
스타
4,867
포크
1,122

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
new-control-api
description
Instructions for adding new public APIs (properties, methods, events, delegates) to existing WinForms controls or components. Covers API issue tracking, PublicAPI file maintenance, property/event conventions, CodeDOM serialization, design-time attributes, and XML documentation.
metadata
{"author":"dotnet-winforms","version":"1.0"}
# Adding New APIs to an Existing WinForms Control or Component These rules apply when **adding new public or protected members** — properties, methods, events, delegates, enums, or interfaces — to an existing WinForms control or component. For creating entirely new controls, or for general coding standards, see the `coding-standards` skill instead. > **Golden rule:** Every new public API must be tracked, reviewed, documented, > and serialization-safe before it ships. --- ## 1. API Issue Tracking Every new public API surface change requires a corresponding **API proposal issue** in the `api-suggestion` format, which will be reviewed by the .NET API review board. ### 1.1 Check for an existing issue Before creating anything, search the **upstream** repository (`dotnet/winforms`) for an existing issue with the `api-suggestion` label that covers the planned API change. ### 1.2 Environment-aware issue management * **Copilot CLI / Copilot Web-UI (CCA):** You have permission to interact with the GitHub API. Follow the rules below to check, create, or update issues. * **VS Code / Visual Studio:** Do **not** attempt to create or modify issues yourself — the necessary permissions may not be available. Instead, remind the user to verify and manage the API issue manually. ### 1.3 Creating or updating the API issue > ⚠️ **NEVER create a new issue in the upstream repo** (`dotnet/winforms`). 1. **If the issue exists upstream and you can modify it** (you are acting on behalf of a WinForms team member with sufficient permissions), update it in-place. 2. **If you cannot modify the upstream issue**, check whether a corresponding issue exists in the **origin** fork. If yes, update it there. If no, create it there. 3. **If no issue exists at all**, create it in the **origin** fork using the format described below. ### 1.4 Required issue content The API issue **must** contain all of the following sections. If any information is missing, **stop and ask the user** before proceeding with implementation. **Section 1 — Background and motivation:** Why is this API needed? What scenario does it enable? **Section 2 — API Proposal** (C# code block with full public signature, no method bodies): ```csharp namespace System.Windows.Forms; public partial class ExistingControl { public Color NewProperty { get; set; } public event EventHandler? NewPropertyChanged; protected virtual void OnNewPropertyChanged(EventArgs e); } ``` **Section 3 — API Usage** (C# code block showing consumption): ```csharp var control = new ExistingControl(); control.NewProperty = Color.Red; control.NewPropertyChanged += (s, e) => Console.WriteLine("Changed!"); ``` **Section 4 — Alternative Designs:** Other approaches considered. **Section 5 — Risks:** Breaking changes, perf regressions, etc. **Section 6 — Will this feature affect UI controls?** Designer support, accessibility impact, localization needs. --- ## 2. PublicAPI File Tracking All new **public** and **protected** members must be recorded in the PublicAPI text files so the Roslyn analyzer can enforce API compatibility. ### 2.1 During development — `PublicAPI.Unshipped.txt` Add entries for every new API surface to the **Unshipped** file of the project that contains the new code. For `System.Windows.Forms` controls this is: ``` src\System.Windows.Forms\PublicAPI.Unshipped.txt ``` ### 2.2 Before final release — `PublicAPI.Shipped.txt` Before snapping for a release, entries are moved from `Unshipped` to `Shipped`. During development, only touch `Unshipped`. ### 2.3 Entry format Entries use the **Roslyn PublicAPI format** — one line per accessor, fully qualified, sorted alphabetically. Key patterns: ```text # Property (getter + setter on separate lines) System.Windows.Forms.Control.DataContext.get -> object? System.Windows.Forms.Control.DataContext.set -> void # Virtual / override / abstract modifiers prefix the line virtual System.Windows.Forms.Control.DataContext.get -> object? virtual System.Windows.Forms.Control.DataContext.set -> void # Events — handler type as return System.Windows.Forms.Control.DataContextChanged -> System.EventHandler? # Protected virtual On-methods virtual System.Windows.Forms.Control.OnDataContextChanged(System.EventArgs! e) -> void # Methods with parameters System.Windows.Forms.Control.SomeMethod(int count, string! name) -> bool # Constructors System.Windows.Forms.MyComponent.MyComponent() -> void # Enum members System.Windows.Forms.MyEnum.Value1 = 0 -> System.Windows.Forms.MyEnum System.Windows.Forms.MyEnum.Value2 = 1 -> System.Windows.Forms.MyEnum ``` **Nullable annotations:** `?` = nullable reference, `!` = non-nullable reference. Value types do not carry these markers unless `Nullable<T>`. ### 2.4 New `override` members must be tracked too The PublicAPI analyzer (RS0016) treats a **newly introduced `override`** of a public or protected member as new API surface — even though the base member is already public. Whenever you **add an `override` that did not previously exist on that type**, add a line for it to `PublicAPI.Unshipped.txt` with the `override` prefix. This is easy to miss for paint/lifecycle overrides added to support a feature. Examples: ```text override System.Windows.Forms.CheckBox.OnPaint(System.Windows.Forms.PaintEventArgs! pevent) -> void override System.Windows.Forms.CheckBox.Dispose(bool disposing) -> void override System.Windows.Forms.ButtonBase.OnVisualStylesModeChanged(System.EventArgs! e) -> void ``` > **CI catches this, a plain `dotnet build` may not.** RS0016 is enforced as an **error** under > the CI/Arcade build (`build.cmd`); a single-project `dotnet build` can report it as 0 warnings. > Always re-verify API tracking with `build.cmd` (see the `building-code` skill's build tenet). ### 2.5 Related pitfalls when adding members to a control * **Hiding an inherited member (CS0114):** if your new member intentionally hides an inherited one (e.g. a `private new bool ShouldSerializePadding()` shadowing `Control.ShouldSerializePadding()`), you **must** use the `new` keyword, or the CI build fails. * **`cref` to internal types in another assembly (CS1574):** XML-doc `<see cref="..."/>` cannot resolve a type that is `internal` in a *different* assembly (even via `InternalsVisibleTo`). Use `<c>TypeName</c>` (plain code font) instead of a `cref` for such references. ### 2.6 Publicly accessible interfaces If a new **public or protected interface** is introduced (or an existing one gains new members), every member that is publicly accessible must also appear in the PublicAPI file and be part of the API review scope. --- ## 3. New Property Conventions ### 3.1 Property backing storage — PropertyStore (mandatory) WinForms controls **do not use regular backing fields** for public properties. Instead, all property values are stored in the control's `Properties` collection (a `PropertyStore` instance). This is critical for two reasons: 1. **Memory efficiency** — only properties that are explicitly set consume space. A form with hundreds of controls does not multiply per-property field overhead. 2. **Constructor ordering** — in an inherited control, the base class constructor runs first, and virtual methods like `CreateParams` are called **before** the derived class's constructor body executes. If a property getter relied on a backing field initialized in the derived constructor, it would read an uninitialized value. The PropertyStore avoids this because `GetValueOrDefault` safely returns a default when no value has been stored. **Declare a static key** for each new property (one per property, shared across all instances): ```csharp private static readonly int s_myPropertyProperty = PropertyStore.CreateKey(); ``` **Value-type property** (int, bool, Color, enum, struct): ```csharp public Color MyProperty { get => Properties.GetValueOrDefault(s_myPropertyProperty, Color.Empty); set { if (Properties.GetValueOrDefault(s_myPropertyProperty, Color.Empty) != value) { Properties.AddOrRemoveValue(s_myPropertyProperty, value, defaultValue: Color.Empty); OnMyPropertyChanged(EventArgs.Empty); } } } ``` `AddOrRemoveValue` automatically removes the entry when the value equals the default, keeping the store lean. **Reference-type property** (object, string, Image): ```csharp public Image? MyImage { get => Properties.GetValueOrDefault<Image>(s_myImageProperty); set { if (Properties.GetValueOrDefault<Image>(s_myImageProperty) != value) { Properties.AddOrRemoveValue(s_myImageProperty, value); OnMyImageChanged(EventArgs.Empty); } } } ``` ### 3.2 CodeDOM serialization strategy — mandatory Every new public property **must** have a serialization strategy so the WinForms Designer can persist it correctly. Use **one** of these approaches: | Approach | When to use | |---|---| | `[DefaultValue(...)]` | Simple value-type properties with a constant default | | `[DesignerSerializationVisibility(Hidden)]` | Properties that must not be serialized (e.g., runtime-only, bound) | | `ShouldSerialize` + `Reset` methods | Complex defaults, reference-type defaults, or ambient properties | **ShouldSerialize / Reset pattern** (uses PropertyStore): ```csharp // These methods MUST be private — the Designer finds them by convention. private bool ShouldSerializeMyProperty() => Properties.ContainsKey(s_myPropertyProperty); private void ResetMyProperty() => Properties.RemoveValue(s_myPropertyProperty); ``` ### 3.3 Change notification — `On[Property]Changed` + event Unless explicitly stated otherwise, every new public property requires: 1. A **`protected virtual void On[Property]Changed(EventArgs e)`** method. 2. A corresponding **`[Property]Changed`** event of type `EventHandler?`. **Event delegate storage** also uses a static key / centralized collection — see [Section 4.3](#43-event-delegate-storage) below. ```csharp private static readonly object s_myPropertyChangedEvent = new(); /// <summary> /// Occurs when the value of <see cref="MyProperty"/> changes. /// </summary> [SRCategory(nameof(SR.CatPropertyChanged))] [SRDescription(nameof(SR.ControlOnMyPropertyChangedDescr))] public event EventHandler? MyPropertyChanged { add => Events.AddHandler(s_myPropertyChangedEvent, value); remove => Events.RemoveHandler(s_myPropertyChangedEvent, value); } /// <summary> /// Raises the <see cref="MyPropertyChanged"/> event. /// </summary> /// <param name="e">An <see cref="EventArgs"/> that contains the event data.</param> [EditorBrowsable(EditorBrowsableState.Advanced)] protected virtual void OnMyPropertyChanged(EventArgs e) { if (Events[s_myPropertyChangedEvent] is EventHandler handler) { handler(this, e); } } ``` ### 3.4 Design-time attributes Decorate new properties with attributes to support the **Properties window** and **IntelliSense**. Check existing properties on the same control for precedent: | Attribute | Purpose | |---|---| | `[SRCategory(nameof(SR.CatXxx))]` | Groups the property in the Properties window | | `[SRDescription(nameof(SR.XxxDescr))]` | Tooltip in the Properties window | | `[Browsable(true/false)]` | Show/hide in Properties window | | `[Bindable(true)]` | Marks the property as data-bindable | | `[EditorBrowsable(...)]` | Controls IntelliSense visibility | | `[Localizable(true)]` | Marks the property value as localizable | **Resource strings:** Category and description strings go in `SR.resx` with localization-ready keys — follow the naming conventions already present (e.g., `CatAppearance`, `CatBehavior`, `CatData`, `ControlOnXxxDescr`). ### 3.5 Naming conventions * Check the target control and its base classes for naming precedent. * Property names should follow the same naming style as peer properties. * If similar properties exist on related controls (e.g., WPF's `DataContext`), match the established name where it makes sense. --- ## 4. New Event Conventions ### 4.1 Standard events Use the standard `EventHandler` delegate and `EventArgs.Empty` when no additional data is needed: ```csharp public event EventHandler? SomethingHappened; ``` ### 4.2 Events with custom data — dedicated EventArgs + Delegate When the event carries data beyond what `EventArgs` provides, **do not use generics** (`EventHandler<T>`). Instead, create: 1. A **dedicated `EventArgs` subclass** (e.g., `MyActionEventArgs`). 2. A **dedicated delegate** (e.g., `MyActionEventHandler`). ```csharp /// <summary> /// Provides data for the <see cref="Control.MyAction"/> event. /// </summary> public class MyActionEventArgs : EventArgs { public MyActionEventArgs(string detail) => Detail = detail; /// <summary> /// Gets the detail information associated with this event. /// </summary> public string Detail { get; } } /// <summary> /// Represents the method that will handle the <see cref="Control.MyAction"/> event. /// </summary> public delegate void MyActionEventHandler(object? sender, MyActionEventArgs e); ``` ### 4.3 Event delegate storage Just as properties use `PropertyStore`, event delegates use the inherited `Events` collection (`EventHandlerList` from `Component`) with **static key objects** — one object per event, shared across all instances. This avoids allocating a delegate field per instance for events that are rarely subscribed. **Declare a static key object** for each event: ```csharp private static readonly object s_myActionEvent = new(); ``` **Declare the event** using custom `add`/`remove` accessors: ```csharp public event MyActionEventHandler? MyAction { add => Events.AddHandler(s_myActionEvent, value); remove => Events.RemoveHandler(s_myActionEvent, value); } ``` **Raise the event** in the `On` method by retrieving the delegate from the collection: ```csharp protected virtual void OnMyAction(MyActionEventArgs e) { if (Events[s_myActionEvent] is MyActionEventHandler handler) { handler(this, e); } } ``` > **Exception:** Components with only a single event (e.g., `Timer.Tick`) may > use a regular field-backed delegate instead. For controls — which inherit > dozens of events from `Control` — always use the `Events` collection. --- ## 5. New Method Conventions ### 5.1 Argument validation Use throw helpers — never hand-roll null or range checks: ```csharp ArgumentNullException.ThrowIfNull(parameter); ArgumentOutOfRangeException.ThrowIfNegative(value); ``` ### 5.2 Virtual methods When adding a method that derived controls should be able to override, make it `protected virtual`. Follow the naming conventions already in use on the control's class hierarchy. --- ## 6. XML Documentation
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기