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.

Datos de origen

Repositorio
dotnet/winforms
Última actividad en el origen
24 de julio de 2026 a las 08:39
Idioma detectado de SKILL.md
inglés
Estrellas
4867
Forks
1122

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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
Ver en GitHub
Este SKILL.md es muy grande, por eso SkillsMP muestra aqui solo la primera seccion. Ver en GitHub