Skip to content

refactor: dead code cleanup: generated partial hooks - #318

Open
damyanpetev wants to merge 7 commits into
masterfrom
dpetev/dead-code-cleanup
Open

refactor: dead code cleanup: generated partial hooks#318
damyanpetev wants to merge 7 commits into
masterfrom
dpetev/dead-code-cleanup

Conversation

@damyanpetev

@damyanpetev damyanpetev commented Aug 6, 2026

Copy link
Copy Markdown
Member

Removes the generated partial void extensibility hooks from src/components/Blazor/. They were a code-generation affordance: a seam for hand-written partials to intercept generated code. Now that the source is hand-maintained, the seam has no reason to exist — a hook with no implementation is unreachable code, and a hook with one is an indirection that reads better inlined at the call point.

After this series there are zero partial void members in src/. One commit per family so each can be reviewed on its own. Continues #316 (OnCreated* constructor hooks).

Commit Family Declarations Call sites Implementations Lines
1 SerializeCoreIgb<X> 141 141 0 -696
2 OnEventUpdating<Prop> 16 16 0 -64
3 On<Prop>Changing 538 46 1 (inlined) -666
4 FindByName<X> 141 141 5 (inlined) -2686
5 OnHandling<Event> 121 121 2 (renamed) -569
6 OnIgbTab*, GetSerializableTabsCollection 3 3 1 (inlined) -12

Commits 1–6 total 149 files, +137 / -4830.


Commit 1 — SerializeCoreIgb<X>

Every generated type that overrides SerializeCore declared a hook and called it as the first statement after base.SerializeCore(ser):

partial void SerializeCoreIgbAccordion(RendererSerializer ser);

internal override void SerializeCore(RendererSerializer ser)
{
    base.SerializeCore(ser);

    SerializeCoreIgbAccordion(ser);

    if (IsPropDirty("SingleExpand"))
    { ser.AddBooleanProp("singleExpand", this._singleExpand); }
    ...
}

141 declarations, 141 call sites, 0 implementations repo-wide, including tests/ and stories/. Unlike a bare declaration, which the compiler erases for free, each of these also occupied a line inside the per-render property snapshot path.

Change Count
partial void SerializeCoreIgb<X>(RendererSerializer ser); declarations removed 141
SerializeCoreIgb<X>(ser); call sites removed 141
SerializeCore overrides in classes with no declared serializable properties left with nothing to do, removed entirely 22

Commit 2 — OnEventUpdating<Prop>

The inbound half of two-way binding. When the client raises the change event for a bound property, the generated handler converts the payload, called this hook to let a partial adjust the value in flight, then wrote it back and invoked <Prop>Changed:

newValueSelected = (bool)(args.Detail);
;
OnEventUpdatingSelected(this._selected, ref newValueSelected);
if (UseDirectRender)
{
    this.Selected = newValueSelected;
}

Also removed: the 16 stray empty statements (; alone on a line)


Commit 3 — On<Prop>Changing

The generator declared one of these next to every property backing field, and the setter called it so a partial could rewrite the incoming value before the property stored it:

private IgbChatOptions? _options;

partial void OnOptionsChanging(ref IgbChatOptions? newValue);

[Parameter]
public IgbChatOptions? Options
{
    get { return this._options; }
    set
    {
        OnOptionsChanging(ref value);
        MarkPropDirty("Options");
        ...
    }
}
Change Count
partial void On<Prop>Changing(ref T newValue); declarations removed 538
of which had no call site whatsoever 492
On<Prop>Changing(ref value); call sites removed 45
implementations folded into the setter 1

The one implementation

IgbChat.OnOptionsChanging lived in src/componentsBase/WebInputs/Chat.cs and normalized the incoming options object. Moved into the Options setter it was called from, which is now self-explanatory:

set
{
    // Never store a null options object, and input attachments are not supported yet.
    value ??= new IgbChatOptions();
    value.DisableInputAttachments = true;
    MarkPropDirty("Options");
    ...
}

Commit 4 — FindByName<X>

Name-based ref resolution: the client sends {"refType": "name", "id": "…"} and FindByName maps it back to the .NET instance. Every generated type declared a hook and an override that tried base, then the hook, then gave up:

partial void FindByNameAvatar(string name, ref object item);
public override object FindByName(string name)
{
    var baseResult = base.FindByName(name);
    if (baseResult != null) { return baseResult; }

    object item = null;
    FindByNameAvatar(name, ref item);
    if (item != null) { return item; }

    return null;
}

With the hook unimplemented that whole override reduces to return base.FindByName(name), so 135 of the 141 were forwarding-only and are removed entirely.

The 5 implementations (Accordion, Dropdown, Select, TileManager, Tree) were five byte-identical copies of the same ContentItems scan sitting in componentsBase/WebInputs. Each is now inlined into its own override, and the ref-parameter dance becomes a direct return:

foreach (var item in ContentItems)
{
    if (item.Name == name || item.ContainerId == name)
    {
        return item;
    }
}

IgbTabs is the sixth case — it keeps its override for the ActualTabsCollection lookup, minus the hook.


Commit 5 — OnHandling<Event>

Every event setter wrapped the hook in an onArgs lambda handed to SetHandler:

this.SetHandler<IgbExpansionPanelComponentEventArgs>(this.Name, "Opening", value, (args) =>
{
    OnHandlingOpening(args);

});

104 of the 121 lambdas contained nothing but the dead call. onArgs is an optional parameter that SetHandler null-checks before invoking (BaseRendererControl.cs:2952), so the argument is dropped outright rather than left as an empty lambda — one less closure allocated per wired event:

this.SetHandler<IgbExpansionPanelComponentEventArgs>(this.Name, "Opening", value);

The other 15 lambdas carry two-way binding write-back and keep their bodies, minus the hook call.

The 2 implementations become ordinary private methods named for what they do, called from the same point:

Was Now
IgbTabs.OnHandlingChange SyncSelectedTab
IgbInputBase.OnHandlingInputOcurred RaiseValueChanging

Both stay in componentsBase/WebInputs beside the EnsureXHandled helpers they belong with. Only the hook indirection is gone — the call site now names a real method instead of dispatching to a partial that may or may not exist.


Commit 6 — the last three

  • IgbTab.OnIgbTabInitializing / OnIgbTabDisposing — declared and called in OnInitializedAsync and Dispose, never implemented.

  • IgbTabs.GetSerializableTabsCollection — declared and implemented in the same generated file, so the partial bought nothing. Its body assigned ActualTabsCollection unconditionally, which made the caller's read of _tabsCollection dead code:

    // was
    { var coll = this._tabsCollection; GetSerializableTabsCollection(ref coll); ser.AddCollectionProp("tabsCollection", coll); }
    // now
    { ser.AddCollectionProp("tabsCollection", ActualTabsCollection); }

Found while sweeping, not fixed here

IgbTab.Dispose() adds the tab to its parent collection instead of removing it (src/components/Blazor/Tab.cs:59):

public void Dispose()
{
    if (TabsParent != null)
    {
        var sv = (IgbTabs)TabsParent;
        sv.ContentTabsCollection.Add(this);   // Add, in Dispose
    }
}

OnInitializedAsync immediately above it does the identical Add. Compare IgbExpansionPanel.Dispose() in src/componentsBase/WebInputs/Accordion.cs, which calls ContentItems.Remove(this). This looks like a copy-paste fault in the generator template — a disposed tab is re-added to ContentTabsCollection, so the collection accumulates dead entries across re-renders. Left alone deliberately: it is a behaviour change that wants its own test and its own commit, not a line smuggled into a dead-code sweep.
Edit: #319

damyanpetev and others added 6 commits August 6, 2026 16:29
Every generated type overriding SerializeCore declared a
`partial void SerializeCoreIgb<X>(RendererSerializer ser)` hook and called it
right after `base.SerializeCore(ser)`. Repo-wide that is 141 declarations and
141 call sites with zero implementations -- dead weight on the per-render
property serialization path, not just in the declaration list.

In 22 types the hook call was the only statement in the override, leaving it
forwarding to base and nothing else, so the override goes too. Those types have
no serializable properties of their own; the only callers of SerializeCore are
the virtual dispatch sites in BaseRendererControl and BaseRendererElement.

Follow-up to the `OnCreated*` sweep (#316).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The inbound half of two-way binding called
`OnEventUpdating<Prop>(this._field, ref newValue)` to let a partial adjust the
value in flight before it was written back and `<Prop>Changed` was invoked.
16 declarations, 16 call sites, zero implementations, so the ref never changed
and the call was a no-op on the value written back.

Also drops the 16 stray empty statements the generator emitted directly above
those call sites -- there are exactly 16 in src/, one per site, so none are
left behind.

The path is covered by the branch's binding contracts, which assert the inbound
write-back for 19 .Bind pairs across 15 components.

Follow-up to the `SerializeCoreIgb*` sweep and the `OnCreated*` sweep (#316).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The generator declared one of these beside every property backing field so a
partial could rewrite the incoming value in the setter. 538 declarations, but
only 46 call sites -- the call was emitted only for reference-typed properties
(attached children, event-args details, RenderFragment templates), so 492 of
the declarations were unreachable by construction rather than merely
unimplemented.

45 of the 46 call sites dispatched to nothing. The 46th, IgbChat.OnOptionsChanging,
had the only implementation in the family; its two statements now run inline at
the same point in the Options setter, so the normalization is visible where it
happens instead of in a partial two files away.

With this the generated hook surface is down to the families that are actually
used: FindByName* (5 implementations) and OnHandling* (2), plus
GetSerializableTabsCollection.

Follow-up to the `OnEventUpdating*` and `SerializeCoreIgb*` sweeps and to #316.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every generated type declared a `FindByName<X>(string name, ref object item)`
hook and an override that tried base, then the hook, then gave up. With the hook
unimplemented the override reduced to `return base.FindByName(name)`, so 135 of
them were forwarding-only and are gone entirely; `FindByName` stays reachable
through the virtual in BaseRendererControl/BaseRendererElement, unchanged for
callers.

The 5 implementations (Accordion, Dropdown, Select, TileManager, Tree) were five
copies of the same ContentItems scan sitting in componentsBase/WebInputs. Each
is now inlined into its own override, replacing the ref-parameter dance with a
direct return:

    foreach (var item in ContentItems)
    {
        if (item.Name == name || item.ContainerId == name)
        {
            return item;
        }
    }

IgbTabs keeps its override for the ActualTabsCollection lookup, minus the hook.

Covered by the interop contracts: DropdownTests asserts a {"refType":"name"}
payload resolves to the .NET IgbDropdownItem instance through the inlined path,
and ExpansionPanelTests asserts the same for a self-reference through the base
implementation.

Follow-up to the `On<Prop>Changing`, `OnEventUpdating*` and `SerializeCoreIgb*`
sweeps and to #316.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
121 declarations, 121 call sites, 2 implementations. Each event setter wrapped
the hook in an onArgs lambda passed to SetHandler:

    this.SetHandler<TArgs>(this.Name, "Opening", value, (args) =>
    {
        OnHandlingOpening(args);
    });

104 of those lambdas contained nothing but the dead hook call. onArgs is an
optional parameter that SetHandler null-checks before invoking, so the argument
is dropped outright rather than left as an empty lambda -- one less closure
allocated per wired event. The other 15 lambdas carry two-way binding write-back
and keep their bodies, minus the hook call.

The 2 implementations become ordinary private methods named for what they do,
called from the same point:

  IgbTabs.OnHandlingChange           -> SyncSelectedTab
  IgbInputBase.OnHandlingInputOcurred -> RaiseValueChanging

Both stay in componentsBase/WebInputs beside the EnsureXHandled helpers they
belong with; only the codegen-hook indirection is gone.

TabsTests asserts the selection sync through SyncSelectedTab. ValueChanging has
no test coverage, so RaiseValueChanging was kept a pure rename with an unchanged
body and call point.

Follow-up to the `FindByName<X>`, `On<Prop>Changing`, `OnEventUpdating*` and
`SerializeCoreIgb*` sweeps and to #316.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clears the remainder of the hook surface, leaving zero `partial void` members in
src/.

IgbTab.OnIgbTabInitializing / OnIgbTabDisposing: declared and called in
OnInitializedAsync and Dispose, never implemented. Both gone.

IgbTabs.GetSerializableTabsCollection: declared and implemented in the same
generated file, so the partial bought nothing. Its body assigned
ActualTabsCollection unconditionally, which made the caller's read of
_tabsCollection dead:

    { var coll = this._tabsCollection; GetSerializableTabsCollection(ref coll); ser.AddCollectionProp("tabsCollection", coll); }

collapses to

    { ser.AddCollectionProp("tabsCollection", ActualTabsCollection); }

Verified on net8.0 and net10.0: 929 passed, 0 failed.

Follow-up to the `OnHandling<Event>`, `FindByName<X>`, `On<Prop>Changing`,
`OnEventUpdating*` and `SerializeCoreIgb*` sweeps and to #316.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 6, 2026 15:37
@damyanpetev damyanpetev changed the title Dead code cleanup: generated partial hooks refactor: dead code cleanup: generated partial hooks Aug 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@damyanpetev
damyanpetev requested a review from dkamburov August 6, 2026 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants