Skip to content

feat(IEditorItem): add EditorItem generic type#7642

Merged
ArgoZhang merged 7 commits intomainfrom
feat-editor-item
Feb 9, 2026
Merged

feat(IEditorItem): add EditorItem generic type#7642
ArgoZhang merged 7 commits intomainfrom
feat-editor-item

Conversation

@ArgoZhang
Copy link
Copy Markdown
Member

@ArgoZhang ArgoZhang commented Feb 9, 2026

Link issues

fixes #7641

Summary By Copilot

Regression?

  • Yes
  • No

Risk

  • High
  • Medium
  • Low

Verification

  • Manual (required)
  • Automated

Packaging changes reviewed?

  • Yes
  • No
  • N/A

☑️ Self Check before Merge

⚠️ Please check all items below before review. ⚠️

  • Doc is updated/provided or not needed
  • Demo is updated/provided or not needed
  • Merge the latest code from the main branch

Summary by Sourcery

Introduce a strongly-typed generic EditorItem model and enhance dynamic component rendering support for model-bound edit templates.

New Features:

  • Add a generic EditorItem implementation of IEditorItem to represent form field metadata and behavior.
  • Extend BootstrapDynamicComponent with RenderEditTemplate to render components bound to a model parameter when available.

Enhancements:

  • Improve PropertyInfoExtensions with clearer English XML comments and a helper to detect Parameter attributes compatible with a given type.

Tests:

  • Add unit tests covering EditorItem behavior, default values, and edit template rendering, as well as tests for the new HasParameterAttribute helper.

Copilot AI review requested due to automatic review settings February 9, 2026 04:05
@bb-auto bb-auto bot added the enhancement New feature or request label Feb 9, 2026
@bb-auto bb-auto bot added this to the v10.3.0 milestone Feb 9, 2026
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Feb 9, 2026

Reviewer's Guide

Introduce a generic EditorItem implementation and supporting utilities for strongly-typed edit templates and parameter binding, including a RenderEditTemplate helper on BootstrapDynamicComponent and a PropertyInfo extension for typed [Parameter] detection, with unit tests covering the new behavior.

Sequence diagram for BootstrapDynamicComponent.RenderEditTemplate_TModel_

sequenceDiagram
    actor Caller
    participant BootstrapDynamicComponent
    participant RenderFragment_TModel_ as RenderFragment_TModel_
    participant RenderTreeBuilder as Builder
    participant ComponentType as componentType
    participant PropertyInfoExtensions

    Caller->>BootstrapDynamicComponent: RenderEditTemplate_TModel_(modelParameterName)
    activate BootstrapDynamicComponent
    BootstrapDynamicComponent-->>Caller: RenderFragment_TModel_
    deactivate BootstrapDynamicComponent

    Caller->>RenderFragment_TModel_: invoke with model
    activate RenderFragment_TModel_
    RenderFragment_TModel_->>Builder: build fragment
    activate Builder
    Builder->>Builder: index = 0
    Builder->>componentType: OpenComponent(index, componentType)

    alt parameters not null
        RenderFragment_TModel_->>RenderFragment_TModel_: modelParameterName ??= Model
        RenderFragment_TModel_->>componentType: GetPropertyByName(modelParameterName)
        componentType-->>RenderFragment_TModel_: modelProperty
        RenderFragment_TModel_->>PropertyInfoExtensions: HasParameterAttribute(modelProperty, typeof(TModel))
        PropertyInfoExtensions-->>RenderFragment_TModel_: bool hasParameter
        alt hasParameter
            RenderFragment_TModel_->>Builder: AddAttribute(index, modelParameterName, model)
        end
        loop for each p in parameters
            RenderFragment_TModel_->>Builder: AddAttribute(index, p.Key, p.Value)
        end
    end

    Builder-->>Builder: CloseComponent()
    deactivate Builder
    deactivate RenderFragment_TModel_
Loading

Class diagram for the new generic EditorItem_TModel implementation

classDiagram
    direction TB

    class IEditorItem {
        <<interface>>
        bool SkipValidate
        bool? Ignore
        bool? Readonly
        bool? Required
        string? RequiredErrorMessage
        bool? ShowLabelTooltip
        string? PlaceHolder
        Type PropertyType
        bool Editable
        string? Step
        int Rows
        int Cols
        string? Text
        RenderFragment_object_ EditTemplate
        Type? ComponentType
        IEnumerable_KeyValuePair_string_object__? ComponentParameters
        IEnumerable_SelectedItem_? Items
        int Order
        IEnumerable_SelectedItem_? Lookup
        bool ShowSearchWhenSelect
        bool IsFixedSearchWhenSelect
        bool IsPopover
        StringComparison LookupStringComparison
        string? LookupServiceKey
        object? LookupServiceData
        ILookupService? LookupService
        Action_TableCellArgs_? OnCellRender
        List_IValidator_? ValidateRules
        string? GroupName
        int GroupOrder
        string GetDisplayName()
        string GetFieldName()
    }

    class ILookup {
        <<interface>>
        IEnumerable_SelectedItem_? Lookup
        StringComparison LookupStringComparison
        string? LookupServiceKey
        object? LookupServiceData
        ILookupService? LookupService
    }

    class ITableColumn {
        <<interface>>
        Action_TableCellArgs_? OnCellRender
    }

    class EditorItem_TModel_ {
        - string FieldName
        + bool SkipValidate
        + bool? Ignore
        + bool? Readonly
        + bool? Required
        + string? RequiredErrorMessage
        + bool? ShowLabelTooltip
        + string? PlaceHolder
        + Type PropertyType
        + string? Step
        + int Rows
        + int Cols
        + string? Text
        + RenderFragment_TModel_? EditTemplate
        + Type? ComponentType
        + IEnumerable_KeyValuePair_string_object__? ComponentParameters
        + IEnumerable_SelectedItem_? Items
        + int Order
        + IEnumerable_SelectedItem_? Lookup
        + bool ShowSearchWhenSelect
        + bool IsPopover
        + StringComparison LookupStringComparison
        + string? LookupServiceKey
        + object? LookupServiceData
        + ILookupService? LookupService
        + Action_TableCellArgs_? OnCellRender
        + List_IValidator_? ValidateRules
        + string? GroupName
        + int GroupOrder
        + EditorItem_TModel_(string fieldName, Type fieldType, string? fieldText)
        + string GetDisplayName()
        + string GetFieldName()
        + RenderFragment_object_ IEditorItem_EditTemplate
    }

    class RenderFragment_TModel_ {
        <<delegate>>
    }

    class RenderFragment_object_ {
        <<delegate>>
    }

    class ILookupService {
        <<interface>>
    }

    class SelectedItem
    class TableCellArgs
    class IValidator {
        <<interface>>
    }

    EditorItem_TModel_ ..|> IEditorItem
    EditorItem_TModel_ ..|> ILookup
    EditorItem_TModel_ ..|> ITableColumn

    EditorItem_TModel_ --> RenderFragment_TModel_ : uses
    EditorItem_TModel_ --> RenderFragment_object_ : explicit implementation
    EditorItem_TModel_ --> SelectedItem
    EditorItem_TModel_ --> ILookupService
    EditorItem_TModel_ --> TableCellArgs
    EditorItem_TModel_ --> IValidator
Loading

Flow diagram for PropertyInfoExtensions.HasParameterAttribute

flowchart TD
    A[Start HasParameterAttribute
modelProperty, type] --> B{modelProperty is null}
    B -->|yes| C[Return false]
    B -->|no| D{Has ParameterAttribute
on modelProperty}
    D -->|no| C
    D -->|yes| E[Get propertyType
= underlying type of
modelProperty.PropertyType
or PropertyType]
    E --> F[Get targetType
= underlying type of
input type
or type]
    F --> G{targetType
IsAssignableFrom
propertyType}
    G -->|yes| H[Return true]
    G -->|no| C
    C --> I[End]
    H --> I
Loading

File-Level Changes

Change Details Files
Add strongly-typed EditorItem implementation that adapts to the existing IEditorItem interface.
  • Implement EditorItem with constructor-based field metadata, backing FieldName, and PropertyType initialization
  • Map all IEditorItem/ILookup/ITableColumn properties to concrete auto-properties with appropriate defaults
  • Add generic RenderFragment? EditTemplate property and explicit IEditorItem.EditTemplate adapter that wraps it into RenderFragment
  • Provide GetDisplayName and GetFieldName implementations based on Text and FieldName
src/BootstrapBlazor/Misc/EditorItem.cs
Extend BootstrapDynamicComponent with a helper to render edit templates bound to a model parameter.
  • Add RenderEditTemplate(string? modelParameterName = null) that returns RenderFragment
  • Use reflection to find the component's model property by name (default "Model") and verify it has a compatible [Parameter] using HasParameterAttribute
  • Bind the model instance and any pre-specified parameters when constructing the component
src/BootstrapBlazor/Utils/BootstrapDynamicComponent.cs
Enhance PropertyInfoExtensions with better documentation and a helper for detecting [Parameter] properties of a given type.
  • Fix and clarify English XML documentation for IsStatic and IsCanWrite
  • Add HasParameterAttribute(this PropertyInfo?, Type) to check for [Parameter] presence and type assignability, handling nullable types
  • Use the new extension in dynamic component rendering logic
src/BootstrapBlazor/Extensions/PropertyInfoExtensions.cs
Add unit tests to validate the new EditorItem behavior and the HasParameterAttribute extension.
  • Create EditorItemTest to validate property defaults, value assignment, GetFieldName/GetDisplayName, and EditTemplate rendering via BootstrapDynamicComponent + MockEditor
  • Add HasParameterAttribute_Ok test verifying that non-[Parameter] properties return false for the extension
test/UnitTest/Misc/EditorItemTest.cs
test/UnitTest/Extensions/ObjectExtensionsTest.cs

Assessment against linked issues

Issue Objective Addressed Explanation
#7641 Add a generic EditorItem type that implements IEditorItem to support strongly-typed editor items.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In EditorItem<TModel>.IEditorItem.EditTemplate the cast (TModel)item assumes the runtime model type always matches TModel; consider adding a type check or safer cast path to avoid potential InvalidCastException when used with loosely-typed callers.
  • PropertyInfoExtensions.HasParameterAttribute currently requires modelProperty.PropertyType == type; if the intent is to support derived or assignable types, consider using type.IsAssignableFrom(modelProperty.PropertyType) instead.
  • BootstrapDynamicComponent.RenderEditTemplate<TModel> performs reflection (GetPropertyByName and HasParameterAttribute) on each render; if this is used frequently, consider caching the resolved property info per componentType/modelParameterName to reduce overhead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `EditorItem<TModel>.IEditorItem.EditTemplate` the cast `(TModel)item` assumes the runtime model type always matches `TModel`; consider adding a type check or safer cast path to avoid potential `InvalidCastException` when used with loosely-typed callers.
- `PropertyInfoExtensions.HasParameterAttribute` currently requires `modelProperty.PropertyType == type`; if the intent is to support derived or assignable types, consider using `type.IsAssignableFrom(modelProperty.PropertyType)` instead.
- `BootstrapDynamicComponent.RenderEditTemplate<TModel>` performs reflection (`GetPropertyByName` and `HasParameterAttribute`) on each render; if this is used frequently, consider caching the resolved property info per `componentType`/`modelParameterName` to reduce overhead.

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor/Extensions/PropertyInfoExtensions.cs:55-57` </location>
<code_context>
+    /// </summary>
+    /// <param name="modelProperty"></param>
+    /// <param name="type"></param>
+    public static bool HasParameterAttribute(this PropertyInfo? modelProperty, Type type) => modelProperty != null
+        && modelProperty.IsDefined(typeof(ParameterAttribute), true)
+        && modelProperty.PropertyType == type;
 }
</code_context>

<issue_to_address>
**suggestion (bug_risk):** Using `PropertyType == type` is likely too restrictive for many real-world Parameter scenarios.

This currently returns true only for exact type matches, which will miss valid cases like nullable vs non-nullable, base/derived types, and interface vs implementation (e.g., `IEnumerable<T>` vs a concrete collection). If the goal is type compatibility, consider `type.IsAssignableFrom(modelProperty.PropertyType)` with nullable unwrapping so this behaves as expected at call sites like `RenderEditTemplate`.

Suggested implementation:

```csharp
    /// <summary>
    /// <para lang="zh">判断属性是否有指定类型的 Parameter 特性</para>
    /// <para lang="en">Determines whether the property has a Parameter attribute of the specified type</para>
    /// </summary>
    /// <param name="modelProperty"></param>
    /// <param name="type"></param>
    public static bool HasParameterAttribute(this PropertyInfo? modelProperty, Type type)
    {
        if (modelProperty is null)
        {
            return false;
        }

        // 必须带有 Parameter 特性
        if (!modelProperty.IsDefined(typeof(ParameterAttribute), inherit: true))
        {
            return false;
        }

        // 处理可空类型,并使用可赋值性检查类型兼容性
        var propertyType = Nullable.GetUnderlyingType(modelProperty.PropertyType) ?? modelProperty.PropertyType;
        var targetType = Nullable.GetUnderlyingType(type) ?? type;

        return targetType.IsAssignableFrom(propertyType);
    }

    /// <summary>

```

If `ParameterAttribute` is not already in scope in this file, you will need to add the appropriate using at the top of the file, typically:
- `using Microsoft.AspNetCore.Components;`

This implementation assumes that the intended semantics are "the property type is compatible with the requested type" after unwrapping nullable types. If your call sites need the opposite relationship, adjust the last line accordingly (e.g., `propertyType.IsAssignableFrom(targetType)`).
</issue_to_address>

### Comment 2
<location> `src/BootstrapBlazor/Misc/EditorItem.cs:98-100` </location>
<code_context>
+    /// </summary>
+    public RenderFragment<TModel>? EditTemplate { get; set; }
+
+    RenderFragment<object>? IEditorItem.EditTemplate
+    {
+        get
+        {
+            return EditTemplate == null ? null : new RenderFragment<object>(item => builder =>
+            {
+                builder.AddContent(0, EditTemplate((TModel)item));
+            });
+        }
+        set
+        {
+        }
</code_context>

<issue_to_address>
**suggestion (bug_risk):** The explicit interface EditTemplate setter is a silent no-op, which may hide misuses.

The `IEditorItem.EditTemplate` setter currently ignores the value, which can confuse consumers who expect it to work. Consider either mapping the setter to the strongly-typed `EditTemplate` (with an appropriate cast/check) or throwing `NotSupportedException` so incorrect usage fails fast instead of silently doing nothing.

```suggestion
        set
        {
            if (value is null)
            {
                EditTemplate = null;
            }
            else
            {
                EditTemplate = model => value(model!);
            }
        }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 9, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (6dbff8f) to head (475e456).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main     #7642   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          749       750    +1     
  Lines        33109     33179   +70     
  Branches      4593      4602    +9     
=========================================
+ Hits         33109     33179   +70     
Flag Coverage Δ
BB 100.00% <100.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a generic EditorItem<TModel> model-aware edit-template pathway to support strongly-typed editor rendering scenarios (per #7641), alongside utility updates to dynamically pass a model into dynamically-rendered components.

Changes:

  • Added BootstrapDynamicComponent.RenderEditTemplate<TModel>() to render a dynamic component while attempting to pass a model parameter.
  • Introduced new EditorItem<TModel> (entity/config class) implementing IEditorItem, including a strongly-typed RenderFragment<TModel> edit template.
  • Extended PropertyInfoExtensions with HasParameterAttribute(...) and improved some XML documentation text.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
src/BootstrapBlazor/Utils/BootstrapDynamicComponent.cs Adds a new RenderEditTemplate<TModel> API intended to pass a model parameter when rendering a dynamic component.
src/BootstrapBlazor/Misc/EditorItem.cs Adds new EditorItem<TModel> implementation of IEditorItem with typed EditTemplate.
src/BootstrapBlazor/Extensions/PropertyInfoExtensions.cs Updates XML docs and adds HasParameterAttribute helper used by RenderEditTemplate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@ArgoZhang
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The explicit IEditorItem.EditTemplate setter in EditorItem<TModel> is a no-op, which is surprising for consumers working via the interface; consider either wiring it to the strongly-typed EditTemplate or throwing/notifying to make the behavior explicit.
  • BootstrapDynamicComponent.RenderEditTemplate<TModel> currently assumes a default parameter name of "Model"; you might want to optionally fall back to discovering a [Parameter] property by assignable type when modelParameterName is null or not found to make it more robust to different component conventions.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The explicit `IEditorItem.EditTemplate` setter in `EditorItem<TModel>` is a no-op, which is surprising for consumers working via the interface; consider either wiring it to the strongly-typed `EditTemplate` or throwing/notifying to make the behavior explicit.
- `BootstrapDynamicComponent.RenderEditTemplate<TModel>` currently assumes a default parameter name of `"Model"`; you might want to optionally fall back to discovering a `[Parameter]` property by assignable type when `modelParameterName` is null or not found to make it more robust to different component conventions.

## Individual Comments

### Comment 1
<location> `src/BootstrapBlazor/Extensions/PropertyInfoExtensions.cs:55-64` </location>
<code_context>
+    /// </summary>
+    /// <param name="modelProperty"></param>
+    /// <param name="type"></param>
+    public static bool HasParameterAttribute(this PropertyInfo? modelProperty, Type type)
+    {
+        if (modelProperty is null)
+        {
+            return false;
+        }
+
+        // 必须带有 Parameter 特性
+        if (!modelProperty.IsDefined(typeof(ParameterAttribute), inherit: true))
+        {
+            return false;
+        }
+
+        // 处理可空类型,并使用可赋值性检查类型兼容性
+        var propertyType = Nullable.GetUnderlyingType(modelProperty.PropertyType) ?? modelProperty.PropertyType;
+        var targetType = Nullable.GetUnderlyingType(type) ?? type;
+
+        return targetType.IsAssignableFrom(propertyType);
+    }
 }
</code_context>

<issue_to_address>
**issue (bug_risk):** The type compatibility check in HasParameterAttribute is likely reversed, which can cause false negatives.

Because this is later called as `HasParameterAttribute(typeof(TModel))` and a `TModel` instance is assigned to a `propertyType` property, the assignability check needs to reflect that the property must accept `TModel`. That means the condition should be `propertyType.IsAssignableFrom(targetType)`. As written, scenarios like `propertyType = object` and `TModel = string` return false even though the assignment is valid, leading to false negatives.
</issue_to_address>

### Comment 2
<location> `src/BootstrapBlazor/Misc/EditorItem.cs:81-90` </location>
<code_context>
+    [NotNull]
</code_context>

<issue_to_address>
**issue (bug_risk):** Text can be null at runtime but GetDisplayName assumes it is non-null, which may lead to null-related issues.

`Text` is `string?` (from `fieldText` and future assignments), but `GetDisplayName()` returns `string` and just returns `Text`. `[NotNull]` only informs static analysis, so callers may still receive null at runtime. Either enforce `Text` as non-null (e.g., default to `FieldName` when null) or update `GetDisplayName()` to handle null explicitly.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(IEditorItem): add EditorItem generic type

2 participants